From 5eee84cf8c7b80d045abf3cec4ed08730278f7aa Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Tue, 2 Dec 2025 13:18:43 -0500 Subject: [PATCH 1/2] Follow symlinks when validating Swiftly is linked When swiftly is installed via homebrew it is placed in `/opt/homebrew/bin/swiftly`, which is a symlink to a location like `/opt/homebrew/bin/Cellar/swiftly/1.1.0/bin/swiftly`. When checking if Swiftly is linked we check to see if the `swiftly` symlink in the swiftly bin directory is pointing to the swiftly executable. However with Homebrew, the symlink is not pointing to the swiftly executable but instead to _another symlink_ that in turn points to the executable. Augment `fs.readlink` to accept a `follow` paramter, which will follow symlinks until we reach the real underlying file. By following the trail of symlinks from the swiftly bin directory to the underlying swiftly executable the isLinked check will succeed and no longer report that swiftly is unlinked when its actually linked correctly. --- Sources/Swiftly/Install.swift | 2 +- Sources/Swiftly/SelfUninstall.swift | 2 +- Sources/Swiftly/Unlink.swift | 4 ++-- Sources/Swiftly/Use.swift | 5 ++++- Sources/SwiftlyCore/FileManager+FilePath.swift | 9 +++++++-- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Sources/Swiftly/Install.swift b/Sources/Swiftly/Install.swift index f59e9f69..057b9e99 100644 --- a/Sources/Swiftly/Install.swift +++ b/Sources/Swiftly/Install.swift @@ -165,7 +165,7 @@ struct Install: SwiftlyCommand { for bin in swiftlyBinDirContents { do { - let linkTarget = try await fs.readlink(atPath: swiftlyBinDir / bin) + let linkTarget = try await fs.readlink(atPath: swiftlyBinDir / bin, follow: false) if linkTarget == proxyTo { existingProxies.append(bin) } diff --git a/Sources/Swiftly/SelfUninstall.swift b/Sources/Swiftly/SelfUninstall.swift index cc4417af..efc4d670 100644 --- a/Sources/Swiftly/SelfUninstall.swift +++ b/Sources/Swiftly/SelfUninstall.swift @@ -110,7 +110,7 @@ struct SelfUninstall: SwiftlyCommand { let fullPath = swiftlyBin / entry guard try await fs.exists(atPath: fullPath) else { continue } if try await fs.isSymLink(atPath: fullPath) { - let dest = try await fs.readlink(atPath: fullPath) + let dest = try await fs.readlink(atPath: fullPath, follow: false) if dest == swiftlyBinary { if verbose { await ctx.print("Removing symlink: \(fullPath) -> \(dest)") diff --git a/Sources/Swiftly/Unlink.swift b/Sources/Swiftly/Unlink.swift index 02d820ca..472948c9 100644 --- a/Sources/Swiftly/Unlink.swift +++ b/Sources/Swiftly/Unlink.swift @@ -55,7 +55,7 @@ struct Unlink: SwiftlyCommand { let swiftlyBinDirContents = (try? await fs.ls(atPath: swiftlyBinDir)) ?? [String]() var proxies = [String]() for file in swiftlyBinDirContents { - let linkTarget = try? await fs.readlink(atPath: swiftlyBinDir / file) + let linkTarget = try? await fs.readlink(atPath: swiftlyBinDir / file, follow: true) if linkTarget == proxyTo { proxies.append(file) } @@ -93,7 +93,7 @@ extension SwiftlyCommand { } let potentialProxyPath = swiftlyBinDir / file - if let linkTarget = try? await fs.readlink(atPath: potentialProxyPath), linkTarget == proxyTo { + if let linkTarget = try? await fs.readlink(atPath: potentialProxyPath, follow: true), linkTarget == proxyTo { return true } } diff --git a/Sources/Swiftly/Use.swift b/Sources/Swiftly/Use.swift index 1462c974..65fa01fe 100644 --- a/Sources/Swiftly/Use.swift +++ b/Sources/Swiftly/Use.swift @@ -76,7 +76,10 @@ struct Use: SwiftlyCommand { var config = try await Config.load(ctx) - try await validateLinked(ctx) + // Only validate linked state if we're not printing the location + if !printLocation { + try await validateLinked(ctx) + } // This is the bare use command where we print the selected toolchain version (or the path to it) guard let toolchain = self.toolchain else { diff --git a/Sources/SwiftlyCore/FileManager+FilePath.swift b/Sources/SwiftlyCore/FileManager+FilePath.swift index 56a03d06..9a1421e1 100644 --- a/Sources/SwiftlyCore/FileManager+FilePath.swift +++ b/Sources/SwiftlyCore/FileManager+FilePath.swift @@ -79,8 +79,13 @@ public enum FileSystem { try FileManager.default.contentsOfDir(atPath: atPath) } - public static func readlink(atPath: FilePath) async throws -> FilePath { - try FileManager.default.destinationOfSymbolicLink(atPath: atPath) + public static func readlink(atPath: FilePath, follow: Bool) async throws -> FilePath { + let path = try FileManager.default.destinationOfSymbolicLink(atPath: atPath) + if follow { + return FilePath(URL(fileURLWithPath: path.string).resolvingSymlinksInPath().path) + } else { + return path + } } public static func isSymLink(atPath: FilePath) async throws -> Bool { From 53ddf7f6df777fe54d171c950ca2aa93f8b1cedf Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Tue, 2 Dec 2025 13:35:01 -0500 Subject: [PATCH 2/2] swift format --- Sources/Swiftly/Use.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Swiftly/Use.swift b/Sources/Swiftly/Use.swift index 65fa01fe..332b9c66 100644 --- a/Sources/Swiftly/Use.swift +++ b/Sources/Swiftly/Use.swift @@ -77,7 +77,7 @@ struct Use: SwiftlyCommand { var config = try await Config.load(ctx) // Only validate linked state if we're not printing the location - if !printLocation { + if !self.printLocation { try await validateLinked(ctx) }