Skip to content

Add ability to temporarily unlink swiftly #315

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Apr 30, 2025
Merged
78 changes: 78 additions & 0 deletions Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -545,3 +545,81 @@ The script will receive the argument '+abcde' followed by '+xyz'.



## link

Link swiftly so it resumes management of the active toolchain.

```
swiftly link [<toolchain-selector>] [--assume-yes] [--verbose] [--version] [--help]
```

**toolchain-selector:**

*Links swiftly if it has been disabled.*


Links swiftly if it has been disabled.


**--assume-yes:**

*Disable confirmation prompts by assuming 'yes'*


**--verbose:**

*Enable verbose reporting from swiftly*


**--version:**

*Show the version.*


**--help:**

*Show help information.*




## unlink

Unlinks swiftly so it no longer manages the active toolchain.

```
swiftly unlink [<toolchain-selector>] [--assume-yes] [--verbose] [--version] [--help]
```

**toolchain-selector:**

*Unlinks swiftly, allowing the system default toolchain to be used.*


Unlinks swiftly until swiftly is linked again with:

$ swiftly link


**--assume-yes:**

*Disable confirmation prompts by assuming 'yes'*


**--verbose:**

*Enable verbose reporting from swiftly*


**--version:**

*Show the version.*


**--help:**

*Show help information.*




9 changes: 1 addition & 8 deletions Sources/Swiftly/Init.swift
Original file line number Diff line number Diff line change
Expand Up @@ -297,14 +297,7 @@ struct Init: SwiftlyCommand {
}

if let postInstall {
await ctx.print("""
There are some dependencies that should be installed before using this toolchain.
You can run the following script as the system administrator (e.g. root) to prepare
your system:

\(postInstall)

""")
await ctx.print(Messages.postInstall(postInstall))
}
}
}
182 changes: 103 additions & 79 deletions Sources/Swiftly/Install.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,33 +86,11 @@ struct Install: SwiftlyCommand {
defer {
versionUpdateReminder()
}
try await validateLinked(ctx)

var config = try await Config.load(ctx)
let toolchainVersion = try await Self.determineToolchainVersion(ctx, version: self.version, config: &config)

var selector: ToolchainSelector

if let version = self.version {
selector = try ToolchainSelector(parsing: version)
} else {
if case let (_, result) = try await selectToolchain(ctx, config: &config),
case let .swiftVersionFile(_, sel, error) = result
{
if let sel = sel {
selector = sel
} else if let error = error {
throw error
} else {
throw SwiftlyError(message: "Internal error selecting toolchain to install.")
}
} else {
throw SwiftlyError(
message:
"Swiftly couldn't determine the toolchain version to install. Please set a version like this and try again: `swiftly install latest`"
)
}
}

let toolchainVersion = try await Self.resolve(ctx, config: config, selector: selector)
let (postInstallScript, pathChanged) = try await Self.execute(
ctx,
version: toolchainVersion,
Expand Down Expand Up @@ -164,6 +142,101 @@ struct Install: SwiftlyCommand {
}
}

public static func setupProxies(
_ ctx: SwiftlyCoreContext,
version: ToolchainVersion,
verbose: Bool,
assumeYes: Bool
) async throws -> Bool {
var pathChanged = false

// Create proxies if we have a location where we can point them
if let proxyTo = try? await Swiftly.currentPlatform.findSwiftlyBin(ctx) {
// Ensure swiftly doesn't overwrite any existing executables without getting confirmation first.
let swiftlyBinDir = Swiftly.currentPlatform.swiftlyBinDir(ctx)
let swiftlyBinDirContents =
(try? await fs.ls(atPath: swiftlyBinDir)) ?? [String]()
let toolchainBinDir = Swiftly.currentPlatform.findToolchainBinDir(ctx, version)
let toolchainBinDirContents = try await fs.ls(atPath: toolchainBinDir)

var existingProxies: [String] = []

for bin in swiftlyBinDirContents {
do {
let linkTarget = try await fs.readlink(atPath: swiftlyBinDir / bin)
if linkTarget == proxyTo {
existingProxies.append(bin)
}
} catch { continue }
}

let overwrite = Set(toolchainBinDirContents).subtracting(existingProxies).intersection(
swiftlyBinDirContents)
if !overwrite.isEmpty && !assumeYes {
await ctx.print("The following existing executables will be overwritten:")

for executable in overwrite {
await ctx.print(" \(swiftlyBinDir / executable)")
}

guard await ctx.promptForConfirmation(defaultBehavior: false) else {
throw SwiftlyError(message: "Toolchain installation has been cancelled")
}
}

if verbose {
await ctx.print("Setting up toolchain proxies...")
}

let proxiesToCreate = Set(toolchainBinDirContents).subtracting(swiftlyBinDirContents).union(
overwrite)

for p in proxiesToCreate {
let proxy = Swiftly.currentPlatform.swiftlyBinDir(ctx) / p

if try await fs.exists(atPath: proxy) {
try await fs.remove(atPath: proxy)
}

try await fs.symlink(atPath: proxy, linkPath: proxyTo)

pathChanged = true
}
}
return pathChanged
}

static func determineToolchainVersion(
_ ctx: SwiftlyCoreContext,
version: String?,
config: inout Config
) async throws -> ToolchainVersion {
let selector: ToolchainSelector

if let version = version {
selector = try ToolchainSelector(parsing: version)
} else {
if case let (_, result) = try await selectToolchain(ctx, config: &config),
case let .swiftVersionFile(_, sel, error) = result
{
if let sel = sel {
selector = sel
} else if let error = error {
throw error
} else {
throw SwiftlyError(message: "Internal error selecting toolchain to install.")
}
} else {
throw SwiftlyError(
message:
"Swiftly couldn't determine the toolchain version to install. Please set a version like this and try again: `swiftly install latest`"
)
}
}

return try await Self.resolve(ctx, config: config, selector: selector)
}

public static func execute(
_ ctx: SwiftlyCoreContext,
version: ToolchainVersion,
Expand Down Expand Up @@ -275,61 +348,12 @@ struct Install: SwiftlyCommand {

try await Swiftly.currentPlatform.install(ctx, from: tmpFile, version: version, verbose: verbose)

var pathChanged = false

// Create proxies if we have a location where we can point them
if let proxyTo = try? await Swiftly.currentPlatform.findSwiftlyBin(ctx) {
// Ensure swiftly doesn't overwrite any existing executables without getting confirmation first.
let swiftlyBinDir = Swiftly.currentPlatform.swiftlyBinDir(ctx)
let swiftlyBinDirContents =
(try? await fs.ls(atPath: swiftlyBinDir)) ?? [String]()
let toolchainBinDir = Swiftly.currentPlatform.findToolchainBinDir(ctx, version)
let toolchainBinDirContents = try await fs.ls(atPath: toolchainBinDir)

var existingProxies: [String] = []

for bin in swiftlyBinDirContents {
do {
let linkTarget = try await fs.readlink(atPath: swiftlyBinDir / bin)
if linkTarget == proxyTo {
existingProxies.append(bin)
}
} catch { continue }
}

let overwrite = Set(toolchainBinDirContents).subtracting(existingProxies).intersection(
swiftlyBinDirContents)
if !overwrite.isEmpty && !assumeYes {
await ctx.print("The following existing executables will be overwritten:")

for executable in overwrite {
await ctx.print(" \(swiftlyBinDir / executable)")
}

guard await ctx.promptForConfirmation(defaultBehavior: false) else {
throw SwiftlyError(message: "Toolchain installation has been cancelled")
}
}

if verbose {
await ctx.print("Setting up toolchain proxies...")
}

let proxiesToCreate = Set(toolchainBinDirContents).subtracting(swiftlyBinDirContents).union(
overwrite)

for p in proxiesToCreate {
let proxy = Swiftly.currentPlatform.swiftlyBinDir(ctx) / p

if try await fs.exists(atPath: proxy) {
try await fs.remove(atPath: proxy)
}

try await fs.symlink(atPath: proxy, linkPath: proxyTo)

pathChanged = true
}
}
let pathChanged = try await Self.setupProxies(
ctx,
version: version,
verbose: verbose,
assumeYes: assumeYes
)

config.installedToolchains.insert(version)

Expand Down
57 changes: 57 additions & 0 deletions Sources/Swiftly/Link.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import ArgumentParser
import Foundation
import SwiftlyCore

struct Link: SwiftlyCommand {
public static let configuration = CommandConfiguration(
abstract: "Link swiftly so it resumes management of the active toolchain."
)

@Argument(help: ArgumentHelp(
"Links swiftly if it has been disabled.",
discussion: """

Links swiftly if it has been disabled.
"""
))
var toolchainSelector: String?

@OptionGroup var root: GlobalOptions

mutating func run() async throws {
try await self.run(Swiftly.createDefaultContext())
}

mutating func run(_ ctx: SwiftlyCoreContext) async throws {
let versionUpdateReminder = try await validateSwiftly(ctx)
defer {
versionUpdateReminder()
}

var config = try await Config.load(ctx)
let toolchainVersion = try await Install.determineToolchainVersion(
ctx,
version: config.inUse?.name,
config: &config
)

let pathChanged = try await Install.setupProxies(
ctx,
version: toolchainVersion,
verbose: self.root.verbose,
assumeYes: self.root.assumeYes
)

if pathChanged {
await ctx.print("""
Linked swiftly to Swift \(toolchainVersion.name).

\(Messages.refreshShell)
""")
} else {
await ctx.print("""
Swiftly is already linked to Swift \(toolchainVersion.name).
""")
}
}
}
3 changes: 1 addition & 2 deletions Sources/Swiftly/List.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,11 @@ struct List: SwiftlyCommand {
versionUpdateReminder()
}

var config = try await Config.load(ctx)
let selector = try self.toolchainSelector.map { input in
try ToolchainSelector(parsing: input)
}

var config = try await Config.load(ctx)

let toolchains = config.listInstalledToolchains(selector: selector).sorted { $0 > $1 }
let (inUse, _) = try await selectToolchain(ctx, config: &config)

Expand Down
3 changes: 1 addition & 2 deletions Sources/Swiftly/ListAvailable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,11 @@ struct ListAvailable: SwiftlyCommand {
versionUpdateReminder()
}

var config = try await Config.load(ctx)
let selector = try self.toolchainSelector.map { input in
try ToolchainSelector(parsing: input)
}

var config = try await Config.load(ctx)

let tc: [ToolchainVersion]

switch selector {
Expand Down
Loading