Skip to content

Remove swiftly from the path during proxying #311

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 5 commits into from
Apr 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Sources/Swiftly/Run.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ struct Run: SwiftlyCommand {

try await Swiftly.currentPlatform.proxy(ctx, toolchain, command[0], [String](command[1...]))
} catch let terminated as RunProgramError {
if ctx.mockedHomeDir != nil {
throw terminated
}
Foundation.exit(terminated.exitCode)
} catch {
throw error
Expand Down
58 changes: 37 additions & 21 deletions Sources/SwiftlyCore/Platform.swift
Original file line number Diff line number Diff line change
Expand Up @@ -164,25 +164,28 @@ extension Platform {
}

#if os(macOS) || os(Linux)
func proxyEnv(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) throws -> [
String:
String
] {
func proxyEnv(_ ctx: SwiftlyCoreContext, env: [String: String], toolchain: ToolchainVersion) throws -> [String: String] {
var newEnv = env

let tcPath = self.findToolchainLocation(ctx, toolchain).appendingPathComponent("usr/bin")
guard tcPath.fileExists() else {
throw SwiftlyError(
message:
"Toolchain \(toolchain) could not be located. You can try `swiftly uninstall \(toolchain)` to uninstall it and then `swiftly install \(toolchain)` to install it again."
)
}
var newEnv = ProcessInfo.processInfo.environment

var pathComponents = (newEnv["PATH"] ?? "").split(separator: ":").map { String($0) }

// The toolchain goes to the beginning of the PATH
var newPath = newEnv["PATH"] ?? ""
if !newPath.hasPrefix(tcPath.path + ":") {
newPath = "\(tcPath.path):\(newPath)"
}
newEnv["PATH"] = newPath
pathComponents.removeAll(where: { $0 == tcPath.path })
pathComponents = [tcPath.path] + pathComponents

// Remove swiftly bin directory from the PATH entirely
let swiftlyBinDir = self.swiftlyBinDir(ctx)
pathComponents.removeAll(where: { $0 == swiftlyBinDir.path })

newEnv["PATH"] = String(pathComponents.joined(by: ":"))

return newEnv
}
Expand All @@ -192,27 +195,40 @@ extension Platform {
/// In the case where the command exit with a non-zero exit code a RunProgramError is thrown with
/// the exit code and program information.
///
public func proxy(
_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, _ command: String,
_ arguments: [String], _ env: [String: String] = [:]
) async throws {
var newEnv = try self.proxyEnv(ctx, toolchain)
public func proxy(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, _ command: String, _ arguments: [String], _ env: [String: String] = [:]) async throws {
let tcPath = self.findToolchainLocation(ctx, toolchain).appendingPathComponent("usr/bin")

let commandTcPath = tcPath.appendingPathComponent(command)
let commandToRun = if FileManager.default.fileExists(atPath: commandTcPath.path) {
commandTcPath.path
} else {
command
}

var newEnv = try self.proxyEnv(ctx, env: ProcessInfo.processInfo.environment, toolchain: toolchain)
for (key, value) in env {
newEnv[key] = value
}
try self.runProgram([command] + arguments, env: newEnv)

try self.runProgram([commandToRun] + arguments, env: newEnv)
}

/// Proxy the invocation of the provided command to the chosen toolchain and capture the output.
///
/// In the case where the command exit with a non-zero exit code a RunProgramError is thrown with
/// the exit code and program information.
///
public func proxyOutput(
_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, _ command: String,
_ arguments: [String]
) async throws -> String? {
try await self.runProgramOutput(command, arguments, env: self.proxyEnv(ctx, toolchain))
public func proxyOutput(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, _ command: String, _ arguments: [String]) async throws -> String? {
let tcPath = self.findToolchainLocation(ctx, toolchain).appendingPathComponent("usr/bin")

let commandTcPath = tcPath.appendingPathComponent(command)
let commandToRun = if FileManager.default.fileExists(atPath: commandTcPath.path) {
commandTcPath.path
} else {
command
}

return try await self.runProgramOutput(commandToRun, arguments, env: self.proxyEnv(ctx, env: ProcessInfo.processInfo.environment, toolchain: toolchain))
}

/// Run a program.
Expand Down
26 changes: 26 additions & 0 deletions Tests/SwiftlyTests/PlatformTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,30 @@ import Testing
toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx), includingPropertiesForKeys: nil)
#expect(0 == toolchains.count)
}

#if os(macOS) || os(Linux)
@Test(
.mockHomeToolchains(),
arguments: [
"/a/b/c:SWIFTLY_BIN_DIR:/d/e/f",
"SWIFTLY_BIN_DIR:/abcde",
"/defgh:SWIFTLY_BIN_DIR",
"/xyzabc:/1/3/4",
"",
]
) func proxyEnv(_ path: String) async throws {
// GIVEN: a PATH that may contain the swiftly bin directory
let env = ["PATH": path.replacing("SWIFTLY_BIN_DIR", with: Swiftly.currentPlatform.swiftlyBinDir(SwiftlyTests.ctx).path)]

// WHEN: proxying to an installed toolchain
let newEnv = try Swiftly.currentPlatform.proxyEnv(SwiftlyTests.ctx, env: env, toolchain: .newStable)

// THEN: the toolchain's bin directory is added to the beginning of the PATH
#expect(newEnv["PATH"]!.hasPrefix(Swiftly.currentPlatform.findToolchainLocation(SwiftlyTests.ctx, .newStable).appendingPathComponent("usr/bin").path))

// AND: the swiftly bin directory is removed from the PATH
#expect(!newEnv["PATH"]!.contains(Swiftly.currentPlatform.swiftlyBinDir(SwiftlyTests.ctx).path))
#expect(!newEnv["PATH"]!.contains(Swiftly.currentPlatform.swiftlyBinDir(SwiftlyTests.ctx).path))
}
#endif
}