From 2cd4cad02491c405113fba3a638d3417f25fbe16 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Fri, 11 Apr 2025 13:44:54 -0400 Subject: [PATCH 01/10] Create a special xcode selector that will delegates to xcrun --- Sources/MacOSPlatform/MacOS.swift | 12 ++++++--- Sources/Swiftly/Config.swift | 4 +-- Sources/Swiftly/Install.swift | 6 ++++- Sources/Swiftly/List.swift | 3 +++ Sources/Swiftly/Uninstall.swift | 7 ++++- Sources/Swiftly/Update.swift | 2 ++ Sources/Swiftly/Use.swift | 2 +- Sources/SwiftlyCore/Platform.swift | 14 +++++----- Sources/SwiftlyCore/ToolchainVersion.swift | 30 ++++++++++++++++++++-- Tests/SwiftlyTests/UseTests.swift | 4 ++- 10 files changed, 66 insertions(+), 18 deletions(-) diff --git a/Sources/MacOSPlatform/MacOS.swift b/Sources/MacOSPlatform/MacOS.swift index d6b73825..c4f16bb0 100644 --- a/Sources/MacOSPlatform/MacOS.swift +++ b/Sources/MacOSPlatform/MacOS.swift @@ -207,9 +207,15 @@ public struct MacOS: Platform { return "/bin/zsh" } - public func findToolchainLocation(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) -> URL - { - self.swiftlyToolchainsDir(ctx).appendingPathComponent("\(toolchain.identifier).xctoolchain") + public func findToolchainLocation(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) async throws -> URL { + if toolchain == .xcodeVersion { + // Print the toolchain location with the help of xcrun + if let xcrunLocation = try? await self.runProgramOutput("/usr/bin/xcrun", "-f", "swift") { + return URL(filePath: xcrunLocation.replacingOccurrences(of: "\n", with: "")).deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent() + } + } + + return self.swiftlyToolchainsDir(ctx).appendingPathComponent("\(toolchain.identifier).xctoolchain") } public static let currentPlatform: any Platform = MacOS() diff --git a/Sources/Swiftly/Config.swift b/Sources/Swiftly/Config.swift index 0be2aa05..429402be 100644 --- a/Sources/Swiftly/Config.swift +++ b/Sources/Swiftly/Config.swift @@ -52,7 +52,7 @@ public struct Config: Codable, Equatable { public func listInstalledToolchains(selector: ToolchainSelector?) -> [ToolchainVersion] { guard let selector else { - return Array(self.installedToolchains) + return Array(self.installedToolchains) + [.xcodeVersion] } if case .latest = selector { @@ -63,7 +63,7 @@ public struct Config: Codable, Equatable { return ts } - return self.installedToolchains.filter { toolchain in + return (self.installedToolchains + [.xcodeVersion]).filter { toolchain in selector.matches(toolchain: toolchain) } } diff --git a/Sources/Swiftly/Install.swift b/Sources/Swiftly/Install.swift index ac056362..e2cf8dd8 100644 --- a/Sources/Swiftly/Install.swift +++ b/Sources/Swiftly/Install.swift @@ -216,6 +216,8 @@ struct Install: SwiftlyCommand { case .main: category = "development" } + case .xcode: + fatalError("unreachable: xcode toolchain cannot be installed with swiftly") } let animation = PercentProgressAnimation( @@ -282,7 +284,7 @@ struct Install: SwiftlyCommand { let swiftlyBinDir = Swiftly.currentPlatform.swiftlyBinDir(ctx) let swiftlyBinDirContents = (try? FileManager.default.contentsOfDirectory(atPath: swiftlyBinDir.path)) ?? [String]() - let toolchainBinDir = Swiftly.currentPlatform.findToolchainBinDir(ctx, version) + let toolchainBinDir = try await Swiftly.currentPlatform.findToolchainBinDir(ctx, version) let toolchainBinDirContents = try FileManager.default.contentsOfDirectory( atPath: toolchainBinDir.path) @@ -429,6 +431,8 @@ struct Install: SwiftlyCommand { } return .snapshot(firstSnapshot) + case .xcode: + throw SwiftlyError(message: "xcode toolchains are not available from swift.org") } } } diff --git a/Sources/Swiftly/List.swift b/Sources/Swiftly/List.swift index a3fd54ed..06a214e5 100644 --- a/Sources/Swiftly/List.swift +++ b/Sources/Swiftly/List.swift @@ -97,6 +97,9 @@ struct List: SwiftlyCommand { for toolchain in toolchains where toolchain.isSnapshot() { await printToolchain(toolchain) } + + await ctx.print("") + await printToolchain(ToolchainVersion.xcode) } } } diff --git a/Sources/Swiftly/Uninstall.swift b/Sources/Swiftly/Uninstall.swift index e73dc213..7c100833 100644 --- a/Sources/Swiftly/Uninstall.swift +++ b/Sources/Swiftly/Uninstall.swift @@ -51,7 +51,7 @@ struct Uninstall: SwiftlyCommand { try validateSwiftly(ctx) let startingConfig = try Config.load(ctx) - let toolchains: [ToolchainVersion] + var toolchains: [ToolchainVersion] if self.toolchain == "all" { // Sort the uninstalled toolchains such that the in-use toolchain will be uninstalled last. // This avoids printing any unnecessary output from using new toolchains while the uninstall is in progress. @@ -68,6 +68,8 @@ struct Uninstall: SwiftlyCommand { toolchains = installedToolchains } + toolchains.removeAll(where: { $0 == .xcodeVersion }) + guard !toolchains.isEmpty else { await ctx.print("No toolchains matched \"\(self.toolchain)\"") return @@ -101,6 +103,9 @@ struct Uninstall: SwiftlyCommand { case let .snapshot(s): // If a snapshot was previously in use, switch to the latest snapshot associated with that branch. selector = .snapshot(branch: s.branch, date: nil) + case .xcode: + // Xcode will not be in the list of installed toolchains, so this is only here for completeness + selector = .xcode } if let toUse = config.listInstalledToolchains(selector: selector) diff --git a/Sources/Swiftly/Update.swift b/Sources/Swiftly/Update.swift index 60247555..14fb336c 100644 --- a/Sources/Swiftly/Update.swift +++ b/Sources/Swiftly/Update.swift @@ -195,6 +195,8 @@ struct Update: SwiftlyCommand { default: fatalError("unreachable") } + case let .xcode: + throw SwiftlyError(message: "xcode cannot be updated from swiftly") } } diff --git a/Sources/Swiftly/Use.swift b/Sources/Swiftly/Use.swift index c5f146dc..1fe77d6b 100644 --- a/Sources/Swiftly/Use.swift +++ b/Sources/Swiftly/Use.swift @@ -78,7 +78,7 @@ struct Use: SwiftlyCommand { if self.printLocation { // Print the toolchain location and exit - await ctx.print("\(Swiftly.currentPlatform.findToolchainLocation(ctx, selectedVersion).path)") + await ctx.print("\(try await Swiftly.currentPlatform.findToolchainLocation(ctx, selectedVersion).path)") return } diff --git a/Sources/SwiftlyCore/Platform.swift b/Sources/SwiftlyCore/Platform.swift index 2d06ee01..12a9d6c5 100644 --- a/Sources/SwiftlyCore/Platform.swift +++ b/Sources/SwiftlyCore/Platform.swift @@ -132,10 +132,10 @@ public protocol Platform: Sendable { func getShell() async throws -> String /// Find the location where the toolchain should be installed. - func findToolchainLocation(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) -> URL + func findToolchainLocation(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) async throws -> URL /// Find the location of the toolchain binaries. - func findToolchainBinDir(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) -> URL + func findToolchainBinDir(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) async throws -> URL } extension Platform { @@ -164,11 +164,11 @@ extension Platform { } #if os(macOS) || os(Linux) - func proxyEnv(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) throws -> [ + func proxyEnv(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) async throws -> [ String: String ] { - let tcPath = self.findToolchainLocation(ctx, toolchain).appendingPathComponent("usr/bin") + let tcPath = try await self.findToolchainLocation(ctx, toolchain).appendingPathComponent("usr/bin") guard tcPath.fileExists() else { throw SwiftlyError( message: @@ -196,7 +196,7 @@ extension Platform { _ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, _ command: String, _ arguments: [String], _ env: [String: String] = [:] ) async throws { - var newEnv = try self.proxyEnv(ctx, toolchain) + var newEnv = try await self.proxyEnv(ctx, toolchain) for (key, value) in env { newEnv[key] = value } @@ -436,9 +436,9 @@ extension Platform { return FileManager.default.fileExists(atPath: swiftlyHomeBin) ? swiftlyHomeBin : nil } - public func findToolchainBinDir(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) -> URL + public func findToolchainBinDir(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) async throws -> URL { - self.findToolchainLocation(ctx, toolchain).appendingPathComponent("usr/bin") + try await self.findToolchainLocation(ctx, toolchain).appendingPathComponent("usr/bin") } #endif diff --git a/Sources/SwiftlyCore/ToolchainVersion.swift b/Sources/SwiftlyCore/ToolchainVersion.swift index 6286adc9..e99334cb 100644 --- a/Sources/SwiftlyCore/ToolchainVersion.swift +++ b/Sources/SwiftlyCore/ToolchainVersion.swift @@ -90,6 +90,7 @@ public enum ToolchainVersion: Sendable { case stable(StableRelease) case snapshot(Snapshot) + case xcode public init(major: Int, minor: Int, patch: Int) { self = .stable(StableRelease(major: major, minor: minor, patch: patch)) @@ -99,6 +100,8 @@ public enum ToolchainVersion: Sendable { self = .snapshot(Snapshot(branch: snapshotBranch, date: date)) } + public static let xcodeVersion: ToolchainVersion = .xcode + static func stableRegex() -> Regex<(Substring, Substring, Substring, Substring)> { try! Regex("^(?:Swift )?(\\d+)\\.(\\d+)\\.(\\d+)$") } @@ -132,6 +135,8 @@ public enum ToolchainVersion: Sendable { throw SwiftlyError(message: "invalid release snapshot version: \(string)") } self = ToolchainVersion(snapshotBranch: .release(major: major, minor: minor), date: String(match.output.3)) + } else if string == "xcode" { + self = ToolchainVersion.xcodeVersion } else { throw SwiftlyError(message: "invalid toolchain version: \"\(string)\"") } @@ -176,6 +181,8 @@ public enum ToolchainVersion: Sendable { case let .release(major, minor): return "\(major).\(minor)-snapshot-\(release.date)" } + case .xcode: + return "xcode" } } @@ -194,6 +201,8 @@ public enum ToolchainVersion: Sendable { case let .release(major, minor): return "swift-\(major).\(minor)-DEVELOPMENT-SNAPSHOT-\(release.date)-a" } + case .xcode: + return "xcode" } } } @@ -214,6 +223,8 @@ extension ToolchainVersion: CustomStringConvertible { return "\(release)" case let .snapshot(snapshot): return "\(snapshot)" + case .xcode: + return "xcode" } } } @@ -231,6 +242,10 @@ extension ToolchainVersion: Comparable { return false case (.stable, .snapshot): return !(rhs < lhs) + case (.xcode, .xcode): + return true + default: + return false } } } @@ -254,6 +269,9 @@ public enum ToolchainSelector: Sendable { /// associated with the given branch. case snapshot(branch: ToolchainVersion.Snapshot.Branch, date: String?) + /// Selects the Xcode of the current system. + case xcode + public init(major: Int, minor: Int? = nil, patch: Int? = nil) { self = .stable(major: major, minor: minor, patch: patch) } @@ -267,6 +285,11 @@ public enum ToolchainSelector: Sendable { return } + if input == "xcode" { + self = Self.xcode + return + } + throw SwiftlyError(message: "invalid toolchain selector: \"\(input)\"") } @@ -274,7 +297,7 @@ public enum ToolchainSelector: Sendable { switch self { case .latest, .stable: return true - case .snapshot: + default: return false } } @@ -312,7 +335,8 @@ public enum ToolchainSelector: Sendable { } } return true - + case (.xcode, .xcode): + return true default: return false } @@ -341,6 +365,8 @@ extension ToolchainSelector: CustomStringConvertible { s += "-\(date)" } return s + case .xcode: + return "xcode" } } } diff --git a/Tests/SwiftlyTests/UseTests.swift b/Tests/SwiftlyTests/UseTests.swift index 0e216b88..3af6a8f0 100644 --- a/Tests/SwiftlyTests/UseTests.swift +++ b/Tests/SwiftlyTests/UseTests.swift @@ -202,7 +202,9 @@ import Testing output = try await SwiftlyTests.runWithMockedIO(Use.self, ["use", "-g", "--print-location"]) - #expect(output.contains(where: { $0.contains(Swiftly.currentPlatform.findToolchainLocation(SwiftlyTests.ctx, toolchain).path) })) + let location = try await Swiftly.currentPlatform.findToolchainLocation(SwiftlyTests.ctx, toolchain).path + + #expect(output.contains(where: { $0.contains(location) })) } } } From 95803604780c90c0cc17859a5f85533a7b717fe6 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Fri, 11 Apr 2025 16:46:25 -0400 Subject: [PATCH 02/10] Filter out xcode toolchain from the output on non-macOS Add a little documentation for the xcode toolchain selector Add tests for the xcode toolchain selector and version --- Sources/Swiftly/Config.swift | 10 ++++++++-- Sources/Swiftly/List.swift | 2 ++ Sources/Swiftly/Uninstall.swift | 3 ++- Sources/Swiftly/Use.swift | 8 +++++++- Sources/SwiftlyCore/ToolchainVersion.swift | 4 ++++ Tests/SwiftlyTests/InstallTests.swift | 7 +++++++ Tests/SwiftlyTests/ListTests.swift | 20 ++++++++++++++++++-- Tests/SwiftlyTests/PlatformTests.swift | 15 +++++++++++++++ Tests/SwiftlyTests/UninstallTests.swift | 5 +++++ Tests/SwiftlyTests/UseTests.swift | 16 ++++++++++++++++ 10 files changed, 84 insertions(+), 6 deletions(-) diff --git a/Sources/Swiftly/Config.swift b/Sources/Swiftly/Config.swift index 429402be..dbecad96 100644 --- a/Sources/Swiftly/Config.swift +++ b/Sources/Swiftly/Config.swift @@ -51,8 +51,14 @@ public struct Config: Codable, Equatable { } public func listInstalledToolchains(selector: ToolchainSelector?) -> [ToolchainVersion] { +#if os(macOS) + let systemToolchains: [ToolchainVersion] = [.xcodeVersion] +#else + let systemToolchains = [] +#endif + guard let selector else { - return Array(self.installedToolchains) + [.xcodeVersion] + return Array(self.installedToolchains) + systemToolchains } if case .latest = selector { @@ -63,7 +69,7 @@ public struct Config: Codable, Equatable { return ts } - return (self.installedToolchains + [.xcodeVersion]).filter { toolchain in + return (self.installedToolchains + systemToolchains).filter { toolchain in selector.matches(toolchain: toolchain) } } diff --git a/Sources/Swiftly/List.swift b/Sources/Swiftly/List.swift index 06a214e5..f47ebcdf 100644 --- a/Sources/Swiftly/List.swift +++ b/Sources/Swiftly/List.swift @@ -98,8 +98,10 @@ struct List: SwiftlyCommand { await printToolchain(toolchain) } +#if os(macOS) await ctx.print("") await printToolchain(ToolchainVersion.xcode) +#endif } } } diff --git a/Sources/Swiftly/Uninstall.swift b/Sources/Swiftly/Uninstall.swift index 7c100833..fd2b89e6 100644 --- a/Sources/Swiftly/Uninstall.swift +++ b/Sources/Swiftly/Uninstall.swift @@ -68,10 +68,11 @@ struct Uninstall: SwiftlyCommand { toolchains = installedToolchains } + // Filter out the xcode toolchain here since it is not uninstallable toolchains.removeAll(where: { $0 == .xcodeVersion }) guard !toolchains.isEmpty else { - await ctx.print("No toolchains matched \"\(self.toolchain)\"") + await ctx.print("No toolchains can be uninstalled that match \"\(self.toolchain)\"") return } diff --git a/Sources/Swiftly/Use.swift b/Sources/Swiftly/Use.swift index 1fe77d6b..69712502 100644 --- a/Sources/Swiftly/Use.swift +++ b/Sources/Swiftly/Use.swift @@ -50,6 +50,12 @@ struct Use: SwiftlyCommand { $ swiftly use 5.7-snapshot $ swiftly use main-snapshot + + macOS ONLY: There is a special selector for swiftly to use your Xcode toolchain. \ + If there are multiple versions of Xcode then swiftly will use the currently selected \ + toolchain from xcode-select. + + $ swiftly use xcode """ )) var toolchain: String? @@ -240,7 +246,7 @@ public func selectToolchain(_ ctx: SwiftlyCoreContext, config: inout Config, glo // Check to ensure that the global default in use toolchain matches one of the installed toolchains, and return // no selected toolchain if it doesn't. - guard let defaultInUse = config.inUse, config.installedToolchains.contains(defaultInUse) else { + guard let defaultInUse = config.inUse, config.listInstalledToolchains(selector: nil).contains(defaultInUse) else { return (nil, .globalDefault) } diff --git a/Sources/SwiftlyCore/ToolchainVersion.swift b/Sources/SwiftlyCore/ToolchainVersion.swift index e99334cb..c615fb42 100644 --- a/Sources/SwiftlyCore/ToolchainVersion.swift +++ b/Sources/SwiftlyCore/ToolchainVersion.swift @@ -243,6 +243,10 @@ extension ToolchainVersion: Comparable { case (.stable, .snapshot): return !(rhs < lhs) case (.xcode, .xcode): + return false + case (.xcode, _): + return false + case (_, .xcode): return true default: return false diff --git a/Tests/SwiftlyTests/InstallTests.swift b/Tests/SwiftlyTests/InstallTests.swift index ebee9c2b..c4f511ec 100644 --- a/Tests/SwiftlyTests/InstallTests.swift +++ b/Tests/SwiftlyTests/InstallTests.swift @@ -262,4 +262,11 @@ import Testing try await SwiftlyTests.installMockedToolchain(selector: ToolchainVersion.newStable.name, args: ["--use"]) try await SwiftlyTests.validateInUse(expected: .newStable) } + + /// Verify that xcode can't be installed like regular toolchains + @Test(.testHomeMockedToolchain()) func installXcode() async throws { + try await #expect(throws: SwiftlyError.self) { + try await SwiftlyTests.runCommand(Install.self, ["install", "xcode", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + } + } } diff --git a/Tests/SwiftlyTests/ListTests.swift b/Tests/SwiftlyTests/ListTests.swift index d1d28ee0..7c43e9d5 100644 --- a/Tests/SwiftlyTests/ListTests.swift +++ b/Tests/SwiftlyTests/ListTests.swift @@ -44,13 +44,19 @@ import Testing let output = try await SwiftlyTests.runWithMockedIO(List.self, args) let parsedToolchains = output.compactMap { outputLine in +#if !os(macOS) Set.allToolchains().first { outputLine.contains(String(describing: $0)) } +#else + (Set.allToolchains() + [.xcodeVersion]).first { + outputLine.contains(String(describing: $0)) + } +#endif } // Ensure extra toolchains weren't accidentally included in the output. - guard parsedToolchains.count == output.filter({ $0.hasPrefix("Swift") || $0.contains("-snapshot") }).count else { + guard parsedToolchains.count == output.filter({ $0.hasPrefix("Swift") || $0.contains("-snapshot") || $0.contains("xcode") }).count else { throw SwiftlyTestError(message: "unexpected listed toolchains in \(output)") } @@ -62,7 +68,11 @@ import Testing @Test func list() async throws { try await self.runListTest { let toolchains = try await self.runList(selector: nil) +#if !os(macOS) #expect(toolchains == Self.sortedReleaseToolchains + Self.sortedSnapshotToolchains) +#else + #expect(toolchains == Self.sortedReleaseToolchains + Self.sortedSnapshotToolchains + [.xcodeVersion]) +#endif } } @@ -155,8 +165,14 @@ import Testing /// Tests that `list` properly handles the case where no toolchains have been installed yet. @Test(.testHome(Self.homeName)) func listEmpty() async throws { +#if !os(macOS) + let systemToolchains: [ToolchainVersion] = [] +#else + let systemToolchains: [ToolchainVersion] = [.xcodeVersion] +#endif + var toolchains = try await self.runList(selector: nil) - #expect(toolchains == []) + #expect(toolchains == systemToolchains) toolchains = try await self.runList(selector: "5") #expect(toolchains == []) diff --git a/Tests/SwiftlyTests/PlatformTests.swift b/Tests/SwiftlyTests/PlatformTests.swift index d0e14400..b7331632 100644 --- a/Tests/SwiftlyTests/PlatformTests.swift +++ b/Tests/SwiftlyTests/PlatformTests.swift @@ -85,4 +85,19 @@ import Testing toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx), includingPropertiesForKeys: nil) #expect(0 == toolchains.count) } + +#if os(macOS) + @Test(.testHome()) func findXcodeToolchainLocation() async throws { + // GIVEN: the xcode toolchain + // AND there is xcode installed + guard let xcodeLocation = try? await Swiftly.currentPlatform.runProgramOutput("xcode-select", "-p"), xcodeLocation != "" else { + return + } + + // WHEN: the location of the xcode toolchain can be found + let toolchainLocation = try await Swiftly.currentPlatform.findToolchainLocation(SwiftlyTests.ctx, .xcodeVersion) + + #expect(toolchainLocation.path.hasPrefix(xcodeLocation.replacingOccurrences(of: "\n", with: ""))) + } +#endif } diff --git a/Tests/SwiftlyTests/UninstallTests.swift b/Tests/SwiftlyTests/UninstallTests.swift index 2c4ff401..71100579 100644 --- a/Tests/SwiftlyTests/UninstallTests.swift +++ b/Tests/SwiftlyTests/UninstallTests.swift @@ -280,4 +280,9 @@ import Testing description: "uninstall did not uninstall all toolchains" ) } + + @Test(.mockHomeToolchains(Self.homeName, toolchains: [])) func uninstallXcode() async throws { + let output = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", "-y", ToolchainVersion.xcodeVersion.name]) + #expect(!output.filter { $0.contains("No toolchains can be uninstalled that match \"xcode\"") }.isEmpty) + } } diff --git a/Tests/SwiftlyTests/UseTests.swift b/Tests/SwiftlyTests/UseTests.swift index 3af6a8f0..5cf716e5 100644 --- a/Tests/SwiftlyTests/UseTests.swift +++ b/Tests/SwiftlyTests/UseTests.swift @@ -148,6 +148,13 @@ import Testing ) } +#if os(macOS) + /// Tests that the xcode toolchain can be used on macOS + @Test(.mockHomeToolchains()) func useXcode() async throws { + try await self.useAndValidate(argument: ToolchainVersion.xcodeVersion.name, expectedVersion: .xcode) + } +#endif + /// Tests that the `use` command gracefully exits when executed before any toolchains have been installed. @Test(.mockHomeToolchains(toolchains: [])) func useNoInstalledToolchains() async throws { try await SwiftlyTests.runCommand(Use.self, ["use", "-g", "latest"]) @@ -209,6 +216,15 @@ import Testing } } +#if os(macOS) + /// Tests that running a use command without an argument prints the xcode toolchain when it is in use. + @Test(.mockHomeToolchains()) func printInUseXcode() async throws { + try await SwiftlyTests.runCommand(Use.self, ["use", "-g", "xcode"]) + var output = try await SwiftlyTests.runWithMockedIO(Use.self, ["use", "-g"]) + #expect(output.contains(where: { $0.contains("xcode") })) + } +#endif + /// Tests in-use toolchain selected by the .swift-version file. @Test func swiftVersionFile() async throws { let toolchains = [ From 124821be26697104ccbb03b7f76b7179ea1314e4 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Fri, 11 Apr 2025 16:55:33 -0400 Subject: [PATCH 03/10] Fix Linux compile error in Config --- Sources/Swiftly/Config.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Swiftly/Config.swift b/Sources/Swiftly/Config.swift index dbecad96..edac08cc 100644 --- a/Sources/Swiftly/Config.swift +++ b/Sources/Swiftly/Config.swift @@ -54,7 +54,7 @@ public struct Config: Codable, Equatable { #if os(macOS) let systemToolchains: [ToolchainVersion] = [.xcodeVersion] #else - let systemToolchains = [] + let systemToolchains: [ToolchainVersion] = [] #endif guard let selector else { From a3db0a432cfb636b487c0ff7cfa9dda49864c2aa Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Fri, 11 Apr 2025 17:05:42 -0400 Subject: [PATCH 04/10] Update the generated documentation --- Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md b/Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md index 553b4a92..4080174f 100644 --- a/Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md +++ b/Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md @@ -206,6 +206,10 @@ Likewise, the latest snapshot associated with a given development branch can be $ swiftly use 5.7-snapshot $ swiftly use main-snapshot +macOS ONLY: There is a special selector for swiftly to use your Xcode toolchain. If there are multiple versions of Xcode then swiftly will use the currently selected toolchain from xcode-select. + + $ swiftly use xcode + **--version:** From b5e95b8064cd05e346718ecd5a449fb4d7a38a8e Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Thu, 26 Jun 2025 09:01:18 -0400 Subject: [PATCH 05/10] Fix up the tests and system toolchain list output --- Sources/Swiftly/List.swift | 10 ---------- Sources/Swiftly/OutputSchema.swift | 9 +++++++++ Tests/SwiftlyTests/InstallTests.swift | 2 +- Tests/SwiftlyTests/ListTests.swift | 23 +++++++++++++++++------ Tests/SwiftlyTests/PlatformTests.swift | 2 +- Tests/SwiftlyTests/SwiftlyTests.swift | 16 ++++++++-------- Tests/SwiftlyTests/UninstallTests.swift | 2 +- Tests/SwiftlyTests/UseTests.swift | 6 +++--- 8 files changed, 40 insertions(+), 30 deletions(-) diff --git a/Sources/Swiftly/List.swift b/Sources/Swiftly/List.swift index 238a9853..986cd06a 100644 --- a/Sources/Swiftly/List.swift +++ b/Sources/Swiftly/List.swift @@ -63,16 +63,6 @@ struct List: SwiftlyCommand { ) } -#if os(macOS) - installedToolchainInfos.append( - InstallToolchainInfo( - version: ToolchainVersion.xcode, - inUse: inUse == ToolchainVersion.xcode, - isDefault: config.inUse == ToolchainVersion.xcode - ) - ) -#endif - try await ctx.output(InstalledToolchainsListInfo(toolchains: installedToolchainInfos, selector: selector)) } } diff --git a/Sources/Swiftly/OutputSchema.swift b/Sources/Swiftly/OutputSchema.swift index 31cf671e..dcd8385e 100644 --- a/Sources/Swiftly/OutputSchema.swift +++ b/Sources/Swiftly/OutputSchema.swift @@ -320,6 +320,8 @@ struct InstalledToolchainsListInfo: OutputData { "main development snapshot" case let .snapshot(.release(major, minor), nil): "\(major).\(minor) development snapshot" + case .xcode: + "xcode" default: "matching" } @@ -340,6 +342,13 @@ struct InstalledToolchainsListInfo: OutputData { lines.append("Installed snapshot toolchains") lines.append("-----------------------------") lines.append(contentsOf: snapshotToolchains.map(\.description)) + +#if os(macOS) + lines.append("") + lines.append("Available system toolchains") + lines.append("---------------------------") + lines.append(ToolchainVersion.xcode.description) +#endif } return lines.joined(separator: "\n") diff --git a/Tests/SwiftlyTests/InstallTests.swift b/Tests/SwiftlyTests/InstallTests.swift index 45c476e4..007697d6 100644 --- a/Tests/SwiftlyTests/InstallTests.swift +++ b/Tests/SwiftlyTests/InstallTests.swift @@ -256,7 +256,7 @@ import Testing } /// Verify that the installed toolchain will be marked as in-use if the --use flag is specified. - @Test(.testHomeMockedToolchain()) func installUseFlag() async throws { + @Test(.mockedSwiftlyVersion(), .testHomeMockedToolchain()) func installUseFlag() async throws { try await SwiftlyTests.installMockedToolchain(toolchain: .oldStable) try await SwiftlyTests.runCommand(Use.self, ["use", ToolchainVersion.oldStable.name]) try await SwiftlyTests.validateInUse(expected: .oldStable) diff --git a/Tests/SwiftlyTests/ListTests.swift b/Tests/SwiftlyTests/ListTests.swift index 767273b1..d828b173 100644 --- a/Tests/SwiftlyTests/ListTests.swift +++ b/Tests/SwiftlyTests/ListTests.swift @@ -51,17 +51,17 @@ import Testing let parsedToolchains = lines.compactMap { outputLine in #if !os(macOS) Set.allToolchains().first { - outputLine.contains(String(describing: $0)) + outputLine.hasPrefix(String(describing: $0)) } #else (Set.allToolchains() + [.xcodeVersion]).first { - outputLine.contains(String(describing: $0)) + outputLine.hasPrefix(String(describing: $0)) } #endif } // Ensure extra toolchains weren't accidentally included in the output. - guard parsedToolchains.count == lines.filter({ $0.hasPrefix("Swift") || $0.contains("-snapshot") || $0.contains("xcode") }).count else { + guard parsedToolchains.count == lines.filter({ $0.hasPrefix("Swift") || $0.contains("-snapshot") || $0.hasPrefix("xcode") }).count else { throw SwiftlyTestError(message: "unexpected listed toolchains in \(output)") } @@ -182,13 +182,18 @@ import Testing #expect(toolchains == systemToolchains) toolchains = try await self.runList(selector: "5") - #expect(toolchains == systemToolchains) + #expect(toolchains == []) toolchains = try await self.runList(selector: "main-snapshot") - #expect(toolchains == systemToolchains) + #expect(toolchains == []) toolchains = try await self.runList(selector: "5.7-snapshot") + #expect(toolchains == []) + +#if os(macOS) + toolchains = try await self.runList(selector: "xcode") #expect(toolchains == systemToolchains) +#endif } } @@ -204,7 +209,13 @@ import Testing from: output[0].data(using: .utf8)! ) - #expect(listInfo.toolchains.count == Set.allToolchains().count) +#if !os(macOS) + let systemToolchains: [ToolchainVersion] = [] +#else + let systemToolchains: [ToolchainVersion] = [.xcodeVersion] +#endif + + #expect(listInfo.toolchains.count == Set.allToolchains().count + systemToolchains.count) for toolchain in listInfo.toolchains { #expect(toolchain.version.name.isEmpty == false) diff --git a/Tests/SwiftlyTests/PlatformTests.swift b/Tests/SwiftlyTests/PlatformTests.swift index b3203f42..b4d32631 100644 --- a/Tests/SwiftlyTests/PlatformTests.swift +++ b/Tests/SwiftlyTests/PlatformTests.swift @@ -88,7 +88,7 @@ import Testing } #if os(macOS) - @Test(.testHome()) func findXcodeToolchainLocation() async throws { + @Test(.mockedSwiftlyVersion(), .testHome()) func findXcodeToolchainLocation() async throws { // GIVEN: the xcode toolchain // AND there is xcode installed guard let xcodeLocation = try? await Swiftly.currentPlatform.runProgramOutput("xcode-select", "-p"), xcodeLocation != "" else { diff --git a/Tests/SwiftlyTests/SwiftlyTests.swift b/Tests/SwiftlyTests/SwiftlyTests.swift index af9b5311..e0de4a37 100644 --- a/Tests/SwiftlyTests/SwiftlyTests.swift +++ b/Tests/SwiftlyTests/SwiftlyTests.swift @@ -61,15 +61,15 @@ actor InputProviderFail: InputProvider { } struct HTTPRequestExecutorFail: HTTPRequestExecutor { - func getCurrentSwiftlyRelease() async throws -> SwiftlyWebsiteAPI.Components.Schemas.SwiftlyRelease { fatalError(unmockedMsg + "3") } - func getReleaseToolchains() async throws -> [Components.Schemas.Release] { fatalError(unmockedMsg + "4") } - func getSnapshotToolchains(branch _: SwiftlyWebsiteAPI.Components.Schemas.SourceBranch, platform _: SwiftlyWebsiteAPI.Components.Schemas.PlatformIdentifier) async throws -> SwiftlyWebsiteAPI.Components.Schemas.DevToolchains { fatalError(unmockedMsg + "5") } - func getGpgKeys() async throws -> OpenAPIRuntime.HTTPBody { fatalError(unmockedMsg + "6") } - func getSwiftlyRelease(url _: URL) async throws -> OpenAPIRuntime.HTTPBody { fatalError(unmockedMsg + "7") } - func getSwiftlyReleaseSignature(url _: URL) async throws -> OpenAPIRuntime.HTTPBody { fatalError(unmockedMsg + "8") } - func getSwiftToolchainFile(_: ToolchainFile) async throws -> OpenAPIRuntime.HTTPBody { fatalError(unmockedMsg + "9") } + func getCurrentSwiftlyRelease() async throws -> SwiftlyWebsiteAPI.Components.Schemas.SwiftlyRelease { fatalError(unmockedMsg) } + func getReleaseToolchains() async throws -> [Components.Schemas.Release] { fatalError(unmockedMsg) } + func getSnapshotToolchains(branch _: SwiftlyWebsiteAPI.Components.Schemas.SourceBranch, platform _: SwiftlyWebsiteAPI.Components.Schemas.PlatformIdentifier) async throws -> SwiftlyWebsiteAPI.Components.Schemas.DevToolchains { fatalError(unmockedMsg) } + func getGpgKeys() async throws -> OpenAPIRuntime.HTTPBody { fatalError(unmockedMsg) } + func getSwiftlyRelease(url _: URL) async throws -> OpenAPIRuntime.HTTPBody { fatalError(unmockedMsg) } + func getSwiftlyReleaseSignature(url _: URL) async throws -> OpenAPIRuntime.HTTPBody { fatalError(unmockedMsg) } + func getSwiftToolchainFile(_: ToolchainFile) async throws -> OpenAPIRuntime.HTTPBody { fatalError(unmockedMsg) } func getSwiftToolchainFileSignature(_: ToolchainFile) async throws - -> OpenAPIRuntime.HTTPBody { fatalError(unmockedMsg + "10") } + -> OpenAPIRuntime.HTTPBody { fatalError(unmockedMsg) } } // Convenience extensions to common Swiftly and SwiftlyCore types to set the correct context diff --git a/Tests/SwiftlyTests/UninstallTests.swift b/Tests/SwiftlyTests/UninstallTests.swift index cb651026..711cd7b3 100644 --- a/Tests/SwiftlyTests/UninstallTests.swift +++ b/Tests/SwiftlyTests/UninstallTests.swift @@ -281,7 +281,7 @@ import Testing ) } - @Test(.mockHomeToolchains(Self.homeName, toolchains: [])) func uninstallXcode() async throws { + @Test(.mockedSwiftlyVersion(), .mockHomeToolchains(Self.homeName, toolchains: [])) func uninstallXcode() async throws { let output = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", "-y", ToolchainVersion.xcodeVersion.name]) #expect(!output.filter { $0.contains("No toolchains can be uninstalled that match \"xcode\"") }.isEmpty) } diff --git a/Tests/SwiftlyTests/UseTests.swift b/Tests/SwiftlyTests/UseTests.swift index 17175cdc..f23232ac 100644 --- a/Tests/SwiftlyTests/UseTests.swift +++ b/Tests/SwiftlyTests/UseTests.swift @@ -193,9 +193,9 @@ import Testing #if os(macOS) /// Tests that the xcode toolchain can be used on macOS - /*@Test(.mockHomeToolchains()) func useXcode() async throws { - try await self.useAndValidate(argument: ToolchainVersion.xcodeVersion.name, expectedVersion: .xcode) - }*/ + @Test(.mockedSwiftlyVersion(), .mockHomeToolchains()) func useXcode() async throws { + try await self.useAndValidate(argument: ToolchainVersion.xcodeVersion.name, expectedVersion: .xcode) + } #endif /// Tests that the `use` command gracefully exits when executed before any toolchains have been installed. From a67faeb64869de8f12ba3ea9b419f1cfe1b4afb3 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Thu, 26 Jun 2025 09:24:49 -0400 Subject: [PATCH 06/10] Fix formatting --- Tests/SwiftlyTests/UseTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/SwiftlyTests/UseTests.swift b/Tests/SwiftlyTests/UseTests.swift index f23232ac..720ec2de 100644 --- a/Tests/SwiftlyTests/UseTests.swift +++ b/Tests/SwiftlyTests/UseTests.swift @@ -194,8 +194,8 @@ import Testing #if os(macOS) /// Tests that the xcode toolchain can be used on macOS @Test(.mockedSwiftlyVersion(), .mockHomeToolchains()) func useXcode() async throws { - try await self.useAndValidate(argument: ToolchainVersion.xcodeVersion.name, expectedVersion: .xcode) - } + try await self.useAndValidate(argument: ToolchainVersion.xcodeVersion.name, expectedVersion: .xcode) + } #endif /// Tests that the `use` command gracefully exits when executed before any toolchains have been installed. From 28e54e8ecf46a763414d017022a3f06d9d3825e0 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Thu, 26 Jun 2025 12:04:10 -0400 Subject: [PATCH 07/10] Revise the findXcodeToolchain test case to work with more macOS systems --- Tests/SwiftlyTests/PlatformTests.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Tests/SwiftlyTests/PlatformTests.swift b/Tests/SwiftlyTests/PlatformTests.swift index b4d32631..8b3c5a8b 100644 --- a/Tests/SwiftlyTests/PlatformTests.swift +++ b/Tests/SwiftlyTests/PlatformTests.swift @@ -91,14 +91,15 @@ import Testing @Test(.mockedSwiftlyVersion(), .testHome()) func findXcodeToolchainLocation() async throws { // GIVEN: the xcode toolchain // AND there is xcode installed - guard let xcodeLocation = try? await Swiftly.currentPlatform.runProgramOutput("xcode-select", "-p"), xcodeLocation != "" else { + guard let swiftLocation = try? await Swiftly.currentPlatform.runProgramOutput("xcrun", "-f", "swift"), swiftLocation != "" else { return } // WHEN: the location of the xcode toolchain can be found let toolchainLocation = try await Swiftly.currentPlatform.findToolchainLocation(SwiftlyTests.ctx, .xcodeVersion) - #expect(toolchainLocation.string.hasPrefix(xcodeLocation.replacingOccurrences(of: "\n", with: ""))) + // THEN: the xcode toolchain matches the currently selected xcode toolchain + #expect(toolchainLocation.string == swiftLocation.replacingOccurrences(of: "\n", with: "").replacingOccurrences(of: "/usr/bin/swift", with: "")) } #endif From 91af22758e8617d4efacf7627ffc08f405f858fa Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Thu, 26 Jun 2025 12:13:33 -0400 Subject: [PATCH 08/10] Categorize the xcode toolchain version as a system toolchain type --- Sources/Swiftly/OutputSchema.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/Swiftly/OutputSchema.swift b/Sources/Swiftly/OutputSchema.swift index dcd8385e..9a9dd440 100644 --- a/Sources/Swiftly/OutputSchema.swift +++ b/Sources/Swiftly/OutputSchema.swift @@ -126,7 +126,7 @@ struct AvailableToolchainInfo: OutputData { try versionContainer.encode(minor, forKey: .minor) } case .xcode: - try versionContainer.encode("xcode", forKey: .type) + try versionContainer.encode("system", forKey: .type) } } } @@ -235,8 +235,8 @@ struct InstallToolchainInfo: OutputData { try versionContainer.encode(major, forKey: .major) try versionContainer.encode(minor, forKey: .minor) } - case let .xcode: - try versionContainer.encode("xcode", forKey: .type) + case .xcode: + try versionContainer.encode("system", forKey: .type) } } @@ -283,7 +283,8 @@ struct InstallToolchainInfo: OutputData { branch: branch, date: date )) - case "xcode": + case "system": + // The only system toolchain that exists at the moment is the xcode version self.version = .xcode default: throw DecodingError.dataCorruptedError( From d63c30ee8566cb1a8e56102e8592a3c4ff717324 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Tue, 29 Jul 2025 11:41:02 -0400 Subject: [PATCH 09/10] Setup proxies on use of a toolchain --- Sources/Swiftly/Init.swift | 14 ++------------ Sources/Swiftly/Install.swift | 27 +++++---------------------- Sources/Swiftly/Swiftly.swift | 24 ++++++++++++++++++++++++ Sources/Swiftly/Uninstall.swift | 5 ++++- Sources/Swiftly/Use.swift | 18 +++++++++++++++--- 5 files changed, 50 insertions(+), 38 deletions(-) diff --git a/Sources/Swiftly/Init.swift b/Sources/Swiftly/Init.swift index 78a00b65..adecf848 100644 --- a/Sources/Swiftly/Init.swift +++ b/Sources/Swiftly/Init.swift @@ -281,18 +281,8 @@ struct Init: SwiftlyCommand { """) } - // Fish doesn't have path caching, so this might only be needed for bash/zsh - if pathChanged && !quietShellFollowup && !shell.hasSuffix("fish") { - await ctx.message(""" - Your shell caches items on your path for better performance. Swiftly has added - items to your path that may not get picked up right away. You can update your - shell's environment by running - - hash -r - - or restarting your shell. - - """) + if pathChanged { + try await Self.handlePathChange(ctx) } if let postInstall { diff --git a/Sources/Swiftly/Install.swift b/Sources/Swiftly/Install.swift index a9813d85..a003ca96 100644 --- a/Sources/Swiftly/Install.swift +++ b/Sources/Swiftly/Install.swift @@ -121,26 +121,8 @@ struct Install: SwiftlyCommand { progressFile: self.progressFile ) - let shell = - if let s = ProcessInfo.processInfo.environment["SHELL"] - { - s - } else { - try await Swiftly.currentPlatform.getShell() - } - - // Fish doesn't cache its path, so this instruction is not necessary. - if pathChanged && !shell.hasSuffix("fish") { - await ctx.message( - """ - NOTE: Swiftly has updated some elements in your path and your shell may not yet be - aware of the changes. You can update your shell's environment by running - - hash -r - - or restarting your shell. - - """) + if pathChanged { + try await Self.handlePathChange(ctx) } if let postInstallScript { @@ -410,7 +392,7 @@ struct Install: SwiftlyCommand { verbose: verbose ) - let pathChanged = try await Self.setupProxies( + var pathChanged = try await Self.setupProxies( ctx, version: version, verbose: verbose, @@ -424,7 +406,8 @@ struct Install: SwiftlyCommand { // If this is the first installed toolchain, mark it as in-use regardless of whether the // --use argument was provided. if useInstalledToolchain { - try await Use.execute(ctx, version, globalDefault: false, &config) + let pc = try await Use.execute(ctx, version, globalDefault: false, verbose: verbose, &config) + pathChanged = pathChanged || pc } // We always update the global default toolchain if there is none set. This could diff --git a/Sources/Swiftly/Swiftly.swift b/Sources/Swiftly/Swiftly.swift index 6660b9ab..62ea6634 100644 --- a/Sources/Swiftly/Swiftly.swift +++ b/Sources/Swiftly/Swiftly.swift @@ -132,4 +132,28 @@ extension SwiftlyCommand { } } } + + public static func handlePathChange(_ ctx: SwiftlyCoreContext) async throws { + let shell = + if let s = ProcessInfo.processInfo.environment["SHELL"] + { + s + } else { + try await Swiftly.currentPlatform.getShell() + } + + // Fish doesn't cache its path, so this instruction is not necessary. + if !shell.hasSuffix("fish") { + await ctx.message( + """ + NOTE: Swiftly has updated some elements in your path and your shell may not yet be + aware of the changes. You can update your shell's environment by running + + hash -r + + or restarting your shell. + + """) + } + } } diff --git a/Sources/Swiftly/Uninstall.swift b/Sources/Swiftly/Uninstall.swift index 9eb22b9b..2054d314 100644 --- a/Sources/Swiftly/Uninstall.swift +++ b/Sources/Swiftly/Uninstall.swift @@ -119,7 +119,10 @@ struct Uninstall: SwiftlyCommand { ?? config.listInstalledToolchains(selector: .latest).filter({ !toolchains.contains($0) }).max() ?? config.installedToolchains.filter({ !toolchains.contains($0) }).max() { - try await Use.execute(ctx, toUse, globalDefault: true, &config) + let pathChanged = try await Use.execute(ctx, toUse, globalDefault: true, verbose: self.root.verbose, &config) + if pathChanged { + try await Self.handlePathChange(ctx) + } } else { // If there are no more toolchains installed, just unuse the currently active toolchain. config.inUse = nil diff --git a/Sources/Swiftly/Use.swift b/Sources/Swiftly/Use.swift index 9264049d..1462c974 100644 --- a/Sources/Swiftly/Use.swift +++ b/Sources/Swiftly/Use.swift @@ -121,11 +121,14 @@ struct Use: SwiftlyCommand { throw SwiftlyError(message: "No installed toolchains match \"\(toolchain)\"") } - try await Self.execute(ctx, toolchain, globalDefault: self.globalDefault, assumeYes: self.root.assumeYes, &config) + let pathChanged = try await Self.execute(ctx, toolchain, globalDefault: self.globalDefault, verbose: self.root.verbose, assumeYes: self.root.assumeYes, &config) + if pathChanged { + try await Self.handlePathChange(ctx) + } } /// Use a toolchain. This method can modify and save the input config and also create/modify a `.swift-version` file. - static func execute(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, globalDefault: Bool, assumeYes: Bool = true, _ config: inout Config) async throws { + static func execute(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, globalDefault: Bool, verbose: Bool, assumeYes: Bool = true, _ config: inout Config) async throws -> Bool { let (selectedVersion, result) = try await selectToolchain(ctx, config: &config, globalDefault: globalDefault) let isGlobal: Bool @@ -142,7 +145,7 @@ struct Use: SwiftlyCommand { guard await ctx.promptForConfirmation(defaultBehavior: true) else { await ctx.message("Aborting setting in-use toolchain") - return + return false } } @@ -156,12 +159,21 @@ struct Use: SwiftlyCommand { configFile = nil } + let pathChanged = try await Install.setupProxies( + ctx, + version: toolchain, + verbose: verbose, + assumeYes: assumeYes + ) + try await ctx.output(ToolchainSetInfo( version: toolchain, previousVersion: selectedVersion, isGlobal: isGlobal, versionFile: configFile )) + + return pathChanged } static func findNewVersionFile(_ ctx: SwiftlyCoreContext) async throws -> FilePath? { From ecbd7f3357ba40f62d050cfd0e5aba3d88f30da7 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Tue, 29 Jul 2025 11:46:01 -0400 Subject: [PATCH 10/10] Fix init quiet shell followup case --- Sources/Swiftly/Init.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Swiftly/Init.swift b/Sources/Swiftly/Init.swift index adecf848..4fc9c211 100644 --- a/Sources/Swiftly/Init.swift +++ b/Sources/Swiftly/Init.swift @@ -281,7 +281,7 @@ struct Init: SwiftlyCommand { """) } - if pathChanged { + if pathChanged && !quietShellFollowup { try await Self.handlePathChange(ctx) }