Skip to content

Commit d15d77a

Browse files
authored
Add ability to temporarily unlink swiftly (#315)
Adds two new commands, `swiftly unlink` and `swiftly link`, which will disable and reenable swiftly's management of the active toolchain. The `unlink` command removes the symlinks to toolchain binaries in the swiftly bin directory that is in the user's path. This allows the rest of the `$PATH` to be searched for available toolchain installations, falling back to the system default. On macOS with Xcode installed this has the effect of falling back to the toolchain in the user's installed Xcode. The `link` command reinstates the symlinks to the `inUse` toolchain, which allows swiftly to resume management of the active toolchain. * Print message to reload shell * Return swiftly bin path, even in tests * Refactor config loading, use in use toolchain when linking * Notify swiftly is unlinked when performing use, install and update * Mock out toolchain in link/unlink tests
1 parent 102a5c3 commit d15d77a

File tree

15 files changed

+462
-105
lines changed

15 files changed

+462
-105
lines changed

Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,3 +545,81 @@ The script will receive the argument '+abcde' followed by '+xyz'.
545545

546546

547547

548+
## link
549+
550+
Link swiftly so it resumes management of the active toolchain.
551+
552+
```
553+
swiftly link [<toolchain-selector>] [--assume-yes] [--verbose] [--version] [--help]
554+
```
555+
556+
**toolchain-selector:**
557+
558+
*Links swiftly if it has been disabled.*
559+
560+
561+
Links swiftly if it has been disabled.
562+
563+
564+
**--assume-yes:**
565+
566+
*Disable confirmation prompts by assuming 'yes'*
567+
568+
569+
**--verbose:**
570+
571+
*Enable verbose reporting from swiftly*
572+
573+
574+
**--version:**
575+
576+
*Show the version.*
577+
578+
579+
**--help:**
580+
581+
*Show help information.*
582+
583+
584+
585+
586+
## unlink
587+
588+
Unlinks swiftly so it no longer manages the active toolchain.
589+
590+
```
591+
swiftly unlink [<toolchain-selector>] [--assume-yes] [--verbose] [--version] [--help]
592+
```
593+
594+
**toolchain-selector:**
595+
596+
*Unlinks swiftly, allowing the system default toolchain to be used.*
597+
598+
599+
Unlinks swiftly until swiftly is linked again with:
600+
601+
$ swiftly link
602+
603+
604+
**--assume-yes:**
605+
606+
*Disable confirmation prompts by assuming 'yes'*
607+
608+
609+
**--verbose:**
610+
611+
*Enable verbose reporting from swiftly*
612+
613+
614+
**--version:**
615+
616+
*Show the version.*
617+
618+
619+
**--help:**
620+
621+
*Show help information.*
622+
623+
624+
625+

Sources/Swiftly/Init.swift

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -297,14 +297,7 @@ struct Init: SwiftlyCommand {
297297
}
298298

299299
if let postInstall {
300-
await ctx.print("""
301-
There are some dependencies that should be installed before using this toolchain.
302-
You can run the following script as the system administrator (e.g. root) to prepare
303-
your system:
304-
305-
\(postInstall)
306-
307-
""")
300+
await ctx.print(Messages.postInstall(postInstall))
308301
}
309302
}
310303
}

Sources/Swiftly/Install.swift

Lines changed: 103 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -86,33 +86,11 @@ struct Install: SwiftlyCommand {
8686
defer {
8787
versionUpdateReminder()
8888
}
89+
try await validateLinked(ctx)
8990

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

92-
var selector: ToolchainSelector
93-
94-
if let version = self.version {
95-
selector = try ToolchainSelector(parsing: version)
96-
} else {
97-
if case let (_, result) = try await selectToolchain(ctx, config: &config),
98-
case let .swiftVersionFile(_, sel, error) = result
99-
{
100-
if let sel = sel {
101-
selector = sel
102-
} else if let error = error {
103-
throw error
104-
} else {
105-
throw SwiftlyError(message: "Internal error selecting toolchain to install.")
106-
}
107-
} else {
108-
throw SwiftlyError(
109-
message:
110-
"Swiftly couldn't determine the toolchain version to install. Please set a version like this and try again: `swiftly install latest`"
111-
)
112-
}
113-
}
114-
115-
let toolchainVersion = try await Self.resolve(ctx, config: config, selector: selector)
11694
let (postInstallScript, pathChanged) = try await Self.execute(
11795
ctx,
11896
version: toolchainVersion,
@@ -164,6 +142,101 @@ struct Install: SwiftlyCommand {
164142
}
165143
}
166144

145+
public static func setupProxies(
146+
_ ctx: SwiftlyCoreContext,
147+
version: ToolchainVersion,
148+
verbose: Bool,
149+
assumeYes: Bool
150+
) async throws -> Bool {
151+
var pathChanged = false
152+
153+
// Create proxies if we have a location where we can point them
154+
if let proxyTo = try? await Swiftly.currentPlatform.findSwiftlyBin(ctx) {
155+
// Ensure swiftly doesn't overwrite any existing executables without getting confirmation first.
156+
let swiftlyBinDir = Swiftly.currentPlatform.swiftlyBinDir(ctx)
157+
let swiftlyBinDirContents =
158+
(try? await fs.ls(atPath: swiftlyBinDir)) ?? [String]()
159+
let toolchainBinDir = Swiftly.currentPlatform.findToolchainBinDir(ctx, version)
160+
let toolchainBinDirContents = try await fs.ls(atPath: toolchainBinDir)
161+
162+
var existingProxies: [String] = []
163+
164+
for bin in swiftlyBinDirContents {
165+
do {
166+
let linkTarget = try await fs.readlink(atPath: swiftlyBinDir / bin)
167+
if linkTarget == proxyTo {
168+
existingProxies.append(bin)
169+
}
170+
} catch { continue }
171+
}
172+
173+
let overwrite = Set(toolchainBinDirContents).subtracting(existingProxies).intersection(
174+
swiftlyBinDirContents)
175+
if !overwrite.isEmpty && !assumeYes {
176+
await ctx.print("The following existing executables will be overwritten:")
177+
178+
for executable in overwrite {
179+
await ctx.print(" \(swiftlyBinDir / executable)")
180+
}
181+
182+
guard await ctx.promptForConfirmation(defaultBehavior: false) else {
183+
throw SwiftlyError(message: "Toolchain installation has been cancelled")
184+
}
185+
}
186+
187+
if verbose {
188+
await ctx.print("Setting up toolchain proxies...")
189+
}
190+
191+
let proxiesToCreate = Set(toolchainBinDirContents).subtracting(swiftlyBinDirContents).union(
192+
overwrite)
193+
194+
for p in proxiesToCreate {
195+
let proxy = Swiftly.currentPlatform.swiftlyBinDir(ctx) / p
196+
197+
if try await fs.exists(atPath: proxy) {
198+
try await fs.remove(atPath: proxy)
199+
}
200+
201+
try await fs.symlink(atPath: proxy, linkPath: proxyTo)
202+
203+
pathChanged = true
204+
}
205+
}
206+
return pathChanged
207+
}
208+
209+
static func determineToolchainVersion(
210+
_ ctx: SwiftlyCoreContext,
211+
version: String?,
212+
config: inout Config
213+
) async throws -> ToolchainVersion {
214+
let selector: ToolchainSelector
215+
216+
if let version = version {
217+
selector = try ToolchainSelector(parsing: version)
218+
} else {
219+
if case let (_, result) = try await selectToolchain(ctx, config: &config),
220+
case let .swiftVersionFile(_, sel, error) = result
221+
{
222+
if let sel = sel {
223+
selector = sel
224+
} else if let error = error {
225+
throw error
226+
} else {
227+
throw SwiftlyError(message: "Internal error selecting toolchain to install.")
228+
}
229+
} else {
230+
throw SwiftlyError(
231+
message:
232+
"Swiftly couldn't determine the toolchain version to install. Please set a version like this and try again: `swiftly install latest`"
233+
)
234+
}
235+
}
236+
237+
return try await Self.resolve(ctx, config: config, selector: selector)
238+
}
239+
167240
public static func execute(
168241
_ ctx: SwiftlyCoreContext,
169242
version: ToolchainVersion,
@@ -275,61 +348,12 @@ struct Install: SwiftlyCommand {
275348

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

278-
var pathChanged = false
279-
280-
// Create proxies if we have a location where we can point them
281-
if let proxyTo = try? await Swiftly.currentPlatform.findSwiftlyBin(ctx) {
282-
// Ensure swiftly doesn't overwrite any existing executables without getting confirmation first.
283-
let swiftlyBinDir = Swiftly.currentPlatform.swiftlyBinDir(ctx)
284-
let swiftlyBinDirContents =
285-
(try? await fs.ls(atPath: swiftlyBinDir)) ?? [String]()
286-
let toolchainBinDir = Swiftly.currentPlatform.findToolchainBinDir(ctx, version)
287-
let toolchainBinDirContents = try await fs.ls(atPath: toolchainBinDir)
288-
289-
var existingProxies: [String] = []
290-
291-
for bin in swiftlyBinDirContents {
292-
do {
293-
let linkTarget = try await fs.readlink(atPath: swiftlyBinDir / bin)
294-
if linkTarget == proxyTo {
295-
existingProxies.append(bin)
296-
}
297-
} catch { continue }
298-
}
299-
300-
let overwrite = Set(toolchainBinDirContents).subtracting(existingProxies).intersection(
301-
swiftlyBinDirContents)
302-
if !overwrite.isEmpty && !assumeYes {
303-
await ctx.print("The following existing executables will be overwritten:")
304-
305-
for executable in overwrite {
306-
await ctx.print(" \(swiftlyBinDir / executable)")
307-
}
308-
309-
guard await ctx.promptForConfirmation(defaultBehavior: false) else {
310-
throw SwiftlyError(message: "Toolchain installation has been cancelled")
311-
}
312-
}
313-
314-
if verbose {
315-
await ctx.print("Setting up toolchain proxies...")
316-
}
317-
318-
let proxiesToCreate = Set(toolchainBinDirContents).subtracting(swiftlyBinDirContents).union(
319-
overwrite)
320-
321-
for p in proxiesToCreate {
322-
let proxy = Swiftly.currentPlatform.swiftlyBinDir(ctx) / p
323-
324-
if try await fs.exists(atPath: proxy) {
325-
try await fs.remove(atPath: proxy)
326-
}
327-
328-
try await fs.symlink(atPath: proxy, linkPath: proxyTo)
329-
330-
pathChanged = true
331-
}
332-
}
351+
let pathChanged = try await Self.setupProxies(
352+
ctx,
353+
version: version,
354+
verbose: verbose,
355+
assumeYes: assumeYes
356+
)
333357

334358
config.installedToolchains.insert(version)
335359

Sources/Swiftly/Link.swift

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import ArgumentParser
2+
import Foundation
3+
import SwiftlyCore
4+
5+
struct Link: SwiftlyCommand {
6+
public static let configuration = CommandConfiguration(
7+
abstract: "Link swiftly so it resumes management of the active toolchain."
8+
)
9+
10+
@Argument(help: ArgumentHelp(
11+
"Links swiftly if it has been disabled.",
12+
discussion: """
13+
14+
Links swiftly if it has been disabled.
15+
"""
16+
))
17+
var toolchainSelector: String?
18+
19+
@OptionGroup var root: GlobalOptions
20+
21+
mutating func run() async throws {
22+
try await self.run(Swiftly.createDefaultContext())
23+
}
24+
25+
mutating func run(_ ctx: SwiftlyCoreContext) async throws {
26+
let versionUpdateReminder = try await validateSwiftly(ctx)
27+
defer {
28+
versionUpdateReminder()
29+
}
30+
31+
var config = try await Config.load(ctx)
32+
let toolchainVersion = try await Install.determineToolchainVersion(
33+
ctx,
34+
version: config.inUse?.name,
35+
config: &config
36+
)
37+
38+
let pathChanged = try await Install.setupProxies(
39+
ctx,
40+
version: toolchainVersion,
41+
verbose: self.root.verbose,
42+
assumeYes: self.root.assumeYes
43+
)
44+
45+
if pathChanged {
46+
await ctx.print("""
47+
Linked swiftly to Swift \(toolchainVersion.name).
48+
49+
\(Messages.refreshShell)
50+
""")
51+
} else {
52+
await ctx.print("""
53+
Swiftly is already linked to Swift \(toolchainVersion.name).
54+
""")
55+
}
56+
}
57+
}

Sources/Swiftly/List.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,11 @@ struct List: SwiftlyCommand {
4343
versionUpdateReminder()
4444
}
4545

46+
var config = try await Config.load(ctx)
4647
let selector = try self.toolchainSelector.map { input in
4748
try ToolchainSelector(parsing: input)
4849
}
4950

50-
var config = try await Config.load(ctx)
51-
5251
let toolchains = config.listInstalledToolchains(selector: selector).sorted { $0 > $1 }
5352
let (inUse, _) = try await selectToolchain(ctx, config: &config)
5453

Sources/Swiftly/ListAvailable.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,11 @@ struct ListAvailable: SwiftlyCommand {
4949
versionUpdateReminder()
5050
}
5151

52+
var config = try await Config.load(ctx)
5253
let selector = try self.toolchainSelector.map { input in
5354
try ToolchainSelector(parsing: input)
5455
}
5556

56-
var config = try await Config.load(ctx)
57-
5857
let tc: [ToolchainVersion]
5958

6059
switch selector {

0 commit comments

Comments
 (0)