Skip to content
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