diff --git a/Sources/Swiftly/Run.swift b/Sources/Swiftly/Run.swift index 056617d9..91a5f1c5 100644 --- a/Sources/Swiftly/Run.swift +++ b/Sources/Swiftly/Run.swift @@ -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 diff --git a/Sources/SwiftlyCore/Platform.swift b/Sources/SwiftlyCore/Platform.swift index 0d28f3fe..885b828c 100644 --- a/Sources/SwiftlyCore/Platform.swift +++ b/Sources/SwiftlyCore/Platform.swift @@ -164,10 +164,9 @@ 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( @@ -175,14 +174,18 @@ extension Platform { "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 } @@ -192,15 +195,22 @@ 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. @@ -208,11 +218,17 @@ 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 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. diff --git a/Tests/SwiftlyTests/PlatformTests.swift b/Tests/SwiftlyTests/PlatformTests.swift index 6a548010..c402f84d 100644 --- a/Tests/SwiftlyTests/PlatformTests.swift +++ b/Tests/SwiftlyTests/PlatformTests.swift @@ -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 }