diff --git a/Sources/TSCBasic/FileSystem.swift b/Sources/TSCBasic/FileSystem.swift index 8667130d..0fe6a36c 100644 --- a/Sources/TSCBasic/FileSystem.swift +++ b/Sources/TSCBasic/FileSystem.swift @@ -450,7 +450,7 @@ private struct LocalFileSystem: FileSystem { var tempDirectory: AbsolutePath { get throws { - let override = ProcessEnv.vars["TMPDIR"] ?? ProcessEnv.vars["TEMP"] ?? ProcessEnv.vars["TMP"] + let override = ProcessEnv.block["TMPDIR"] ?? ProcessEnv.block["TEMP"] ?? ProcessEnv.block["TMP"] if let path = override.flatMap({ try? AbsolutePath(validating: $0) }) { return path } diff --git a/Sources/TSCBasic/Process/Process.swift b/Sources/TSCBasic/Process/Process.swift index 971d7855..0bec7f46 100644 --- a/Sources/TSCBasic/Process/Process.swift +++ b/Sources/TSCBasic/Process/Process.swift @@ -53,7 +53,16 @@ public struct ProcessResult: CustomStringConvertible, Sendable { public let arguments: [String] /// The environment with which the process was launched. - public let environment: [String: String] + public let environmentBlock: ProcessEnvironmentBlock + + @available(*, deprecated, renamed: "env") + public var environment: [String:String] { + #if os(Windows) + Dictionary(uniqueKeysWithValues: self.environmentBlock.map { ($0.key.value, $0.value) }) + #else + self.env + #endif + } /// The exit status of the process. public let exitStatus: ExitStatus @@ -71,7 +80,7 @@ public struct ProcessResult: CustomStringConvertible, Sendable { /// See `waitpid(2)` for information on the exit status code. public init( arguments: [String], - environment: [String: String], + environmentBlock: ProcessEnvironmentBlock, exitStatusCode: Int32, normal: Bool, output: Result<[UInt8], Swift.Error>, @@ -92,25 +101,60 @@ public struct ProcessResult: CustomStringConvertible, Sendable { exitStatus = .terminated(code: WEXITSTATUS(exitStatusCode)) } #endif - self.init(arguments: arguments, environment: environment, exitStatus: exitStatus, output: output, - stderrOutput: stderrOutput) + self.init(arguments: arguments, environmentBlock: environmentBlock, exitStatus: exitStatus, output: output, stderrOutput: stderrOutput) + } + + @available(*, deprecated, message: "use `init(arguments:environmentBlock:exitStatusCode:output:stderrOutput:)`") + public init( + arguments: [String], + environment: [String:String], + exitStatusCode: Int32, + normal: Bool, + output: Result<[UInt8], Swift.Error>, + stderrOutput: Result<[UInt8], Swift.Error> + ) { + self.init( + arguments: arguments, + environmentBlock: .init(environment), + exitStatusCode: exitStatusCode, + normal: normal, + output: output, + stderrOutput: stderrOutput + ) } /// Create an instance using an exit status and output result. public init( arguments: [String], - environment: [String: String], + environmentBlock: ProcessEnvironmentBlock, exitStatus: ExitStatus, output: Result<[UInt8], Swift.Error>, stderrOutput: Result<[UInt8], Swift.Error> ) { self.arguments = arguments - self.environment = environment + self.environmentBlock = environmentBlock self.output = output self.stderrOutput = stderrOutput self.exitStatus = exitStatus } + @available(*, deprecated, message: "use `init(arguments:environmentBlock:exitStatus:output:stderrOutput:)`") + public init( + arguments: [String], + environment: [String:String], + exitStatus: ExitStatus, + output: Result<[UInt8], Swift.Error>, + stderrOutput: Result<[UInt8], Swift.Error> + ) { + self.init( + arguments: arguments, + environmentBlock: .init(environment), + exitStatus: exitStatus, + output: output, + stderrOutput: stderrOutput + ) + } + /// Converts stdout output bytes to string, assuming they're UTF8. public func utf8Output() throws -> String { return String(decoding: try output.get(), as: Unicode.UTF8.self) @@ -245,14 +289,23 @@ public final class Process { /// The current environment. @available(*, deprecated, message: "use ProcessEnv.vars instead") static public var env: [String: String] { - return ProcessInfo.processInfo.environment + ProcessEnv.vars } /// The arguments to execute. public let arguments: [String] /// The environment with which the process was executed. - public let environment: [String: String] + @available(*, deprecated, message: "use `environmentBlock` instead") + public var environment: [String:String] { + #if os(Windows) + Dictionary(uniqueKeysWithValues: environmentBlock.map { ($0.key.value, $0.value) }) + #else + environmentBlock + #endif + } + + public let environmentBlock: ProcessEnvironmentBlock /// The path to the directory under which to run the process. public let workingDirectory: AbsolutePath? @@ -323,21 +376,34 @@ public final class Process { /// continue running even if the parent is killed or interrupted. Default value is true. /// - loggingHandler: Handler for logging messages /// + public init(arguments: [String], environmentBlock: ProcessEnvironmentBlock = ProcessEnv.block, workingDirectory: AbsolutePath, outputRedirection: OutputRedirection = .collect, startNewProcessGroup: Bool = true, loggingHandler: LoggingHandler? = .none) { + self.arguments = arguments + self.environmentBlock = environmentBlock + self.workingDirectory = workingDirectory + self.outputRedirection = outputRedirection + self.startNewProcessGroup = startNewProcessGroup + self.loggingHandler = loggingHandler ?? Process.loggingHandler + } + + @_disfavoredOverload @available(macOS 10.15, *) - public init( + @available(*, deprecated, renamed: "init(arguments:environmentBlock:workingDirectory:outputRedirection:startNewProcessGroup:loggingHandler:)") + public convenience init( arguments: [String], - environment: [String: String] = ProcessEnv.vars, + environment: [String:String] = ProcessEnv.vars, workingDirectory: AbsolutePath, outputRedirection: OutputRedirection = .collect, startNewProcessGroup: Bool = true, loggingHandler: LoggingHandler? = .none ) { - self.arguments = arguments - self.environment = environment - self.workingDirectory = workingDirectory - self.outputRedirection = outputRedirection - self.startNewProcessGroup = startNewProcessGroup - self.loggingHandler = loggingHandler ?? Process.loggingHandler + self.init( + arguments: arguments, + environmentBlock: .init(environment), + workingDirectory: workingDirectory, + outputRedirection: outputRedirection, + startNewProcessGroup: startNewProcessGroup, + loggingHandler: loggingHandler + ) } /// Create a new process instance. @@ -351,21 +417,49 @@ public final class Process { /// - startNewProcessGroup: If true, a new progress group is created for the child making it /// continue running even if the parent is killed or interrupted. Default value is true. /// - loggingHandler: Handler for logging messages - public init( - arguments: [String], - environment: [String: String] = ProcessEnv.vars, - outputRedirection: OutputRedirection = .collect, - startNewProcessGroup: Bool = true, - loggingHandler: LoggingHandler? = .none - ) { + public init(arguments: [String], environmentBlock: ProcessEnvironmentBlock = ProcessEnv.block, outputRedirection: OutputRedirection = .collect, startNewProcessGroup: Bool = true, loggingHandler: LoggingHandler? = .none) { self.arguments = arguments - self.environment = environment + self.environmentBlock = environmentBlock self.workingDirectory = nil self.outputRedirection = outputRedirection self.startNewProcessGroup = startNewProcessGroup self.loggingHandler = loggingHandler ?? Process.loggingHandler } + @_disfavoredOverload + @available(*, deprecated, renamed: "init(arguments:environmentBlock:outputRedirection:startNewProcessGroup:loggingHandler:)") + public convenience init( + arguments: [String], + environment: [String:String] = ProcessEnv.vars, + outputRedirection: OutputRedirection = .collect, + startNewProcessGroup: Bool = true, + loggingHandler: LoggingHandler? = .none + ) { + self.init( + arguments: arguments, + environmentBlock: .init(environment), + outputRedirection: outputRedirection, + startNewProcessGroup: startNewProcessGroup, + loggingHandler: loggingHandler + ) + } + + public convenience init( + args: String..., + environmentBlock: ProcessEnvironmentBlock = ProcessEnv.block, + outputRedirection: OutputRedirection = .collect, + loggingHandler: LoggingHandler? = .none + ) { + self.init( + arguments: args, + environmentBlock: environmentBlock, + outputRedirection: outputRedirection, + loggingHandler: loggingHandler + ) + } + + @_disfavoredOverload + @available(*, deprecated, renamed: "init(args:environmentBlock:outputRedirection:loggingHandler:)") public convenience init( args: String..., environment: [String: String] = ProcessEnv.vars, @@ -374,7 +468,7 @@ public final class Process { ) { self.init( arguments: args, - environment: environment, + environmentBlock: .init(environment), outputRedirection: outputRedirection, loggingHandler: loggingHandler ) @@ -460,7 +554,7 @@ public final class Process { process.currentDirectoryURL = workingDirectory.asURL } process.executableURL = executablePath.asURL - process.environment = environment + process.environment = Dictionary(uniqueKeysWithValues: environmentBlock.map { ($0.key.value, $0.value) }) let stdinPipe = Pipe() process.standardInput = stdinPipe @@ -817,7 +911,7 @@ public final class Process { // Construct the result. let executionResult = ProcessResult( arguments: arguments, - environment: environment, + environmentBlock: environmentBlock, exitStatusCode: exitStatusCode, normal: normalExit, output: stdoutResult, @@ -900,15 +994,10 @@ extension Process { /// - environment: The environment to pass to subprocess. By default the current process environment /// will be inherited. /// - loggingHandler: Handler for logging messages - @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) - static public func popen( - arguments: [String], - environment: [String: String] = ProcessEnv.vars, - loggingHandler: LoggingHandler? = .none - ) async throws -> ProcessResult { + static public func popen(arguments: [String], environmentBlock: ProcessEnvironmentBlock = ProcessEnv.block, loggingHandler: LoggingHandler? = .none) async throws -> ProcessResult { let process = Process( arguments: arguments, - environment: environment, + environmentBlock: environmentBlock, outputRedirection: .collect, loggingHandler: loggingHandler ) @@ -916,6 +1005,17 @@ extension Process { return try await process.waitUntilExit() } + @_disfavoredOverload + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) + @available(*, deprecated, renamed: "popen(arguments:environmentBlock:loggingHandler:)") + static public func popen( + arguments: [String], + environment: [String:String] = ProcessEnv.vars, + loggingHandler: LoggingHandler? = .none + ) async throws -> ProcessResult { + try await popen(arguments: arguments, environmentBlock: .init(environment), loggingHandler: loggingHandler) + } + /// Execute a subprocess and returns the result when it finishes execution /// /// - Parameters: @@ -923,13 +1023,19 @@ extension Process { /// - environment: The environment to pass to subprocess. By default the current process environment /// will be inherited. /// - loggingHandler: Handler for logging messages + static public func popen(args: String..., environmentBlock: ProcessEnvironmentBlock = ProcessEnv.block, loggingHandler: LoggingHandler? = .none) async throws -> ProcessResult { + try await popen(arguments: args, environmentBlock: environmentBlock, loggingHandler: loggingHandler) + } + + @_disfavoredOverload @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) + @available(*, deprecated, renamed: "popen(args:environmentBlock:loggingHandler:)") static public func popen( args: String..., environment: [String: String] = ProcessEnv.vars, loggingHandler: LoggingHandler? = .none ) async throws -> ProcessResult { - try await popen(arguments: args, environment: environment, loggingHandler: loggingHandler) + try await popen(arguments: args, environmentBlock: .init(environment), loggingHandler: loggingHandler) } /// Execute a subprocess and get its (UTF-8) output if it has a non zero exit. @@ -940,19 +1046,26 @@ extension Process { /// will be inherited. /// - loggingHandler: Handler for logging messages /// - Returns: The process output (stdout + stderr). + @discardableResult + static public func checkNonZeroExit(arguments: [String], environmentBlock: ProcessEnvironmentBlock = ProcessEnv.block, loggingHandler: LoggingHandler? = .none) async throws -> String { + let result = try await popen(arguments: arguments, environmentBlock: environmentBlock, loggingHandler: loggingHandler) + // Throw if there was a non zero termination. + guard result.exitStatus == .terminated(code: 0) else { + throw ProcessResult.Error.nonZeroExit(result) + } + return try result.utf8Output() + } + + @_disfavoredOverload @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) + @available(*, deprecated, renamed: "checkNonZeroExit(arguments:environmentBlock:loggingHandler:)") @discardableResult static public func checkNonZeroExit( arguments: [String], environment: [String: String] = ProcessEnv.vars, loggingHandler: LoggingHandler? = .none ) async throws -> String { - let result = try await popen(arguments: arguments, environment: environment, loggingHandler: loggingHandler) - // Throw if there was a non zero termination. - guard result.exitStatus == .terminated(code: 0) else { - throw ProcessResult.Error.nonZeroExit(result) - } - return try result.utf8Output() + try await checkNonZeroExit(arguments: arguments, environmentBlock: .init(environment), loggingHandler: loggingHandler) } /// Execute a subprocess and get its (UTF-8) output if it has a non zero exit. @@ -963,14 +1076,21 @@ extension Process { /// will be inherited. /// - loggingHandler: Handler for logging messages /// - Returns: The process output (stdout + stderr). + @discardableResult + static public func checkNonZeroExit(args: String..., environmentBlock: ProcessEnvironmentBlock = ProcessEnv.block, loggingHandler: LoggingHandler? = .none) async throws -> String { + try await checkNonZeroExit(arguments: args, environmentBlock: environmentBlock, loggingHandler: loggingHandler) + } + + @_disfavoredOverload @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) + @available(*, deprecated, renamed: "checkNonZeroExit(args:environmentBlock:loggingHandler:)") @discardableResult static public func checkNonZeroExit( args: String..., environment: [String: String] = ProcessEnv.vars, loggingHandler: LoggingHandler? = .none ) async throws -> String { - try await checkNonZeroExit(arguments: args, environment: environment, loggingHandler: loggingHandler) + try await checkNonZeroExit(arguments: args, environmentBlock: .init(environment), loggingHandler: loggingHandler) } } @@ -989,7 +1109,7 @@ extension Process { // #endif static public func popen( arguments: [String], - environment: [String: String] = ProcessEnv.vars, + environmentBlock: ProcessEnvironmentBlock = ProcessEnv.block, loggingHandler: LoggingHandler? = .none, queue: DispatchQueue? = nil, completion: @escaping (Result) -> Void @@ -999,7 +1119,7 @@ extension Process { do { let process = Process( arguments: arguments, - environment: environment, + environmentBlock: environmentBlock, outputRedirection: .collect, loggingHandler: loggingHandler ) @@ -1013,6 +1133,24 @@ extension Process { } } + @_disfavoredOverload + @available(*, deprecated, renamed: "popen(arguments:environmentBlock:loggingHandler:queue:completion:)") + static public func popen( + arguments: [String], + environment: [String:String] = ProcessEnv.vars, + loggingHandler: LoggingHandler? = .none, + queue: DispatchQueue? = nil, + completion: @escaping (Result) -> Void + ) { + popen( + arguments: arguments, + environmentBlock: .init(environment), + loggingHandler: loggingHandler, + queue: queue, + completion: completion + ) + } + /// Execute a subprocess and block until it finishes execution /// /// - Parameters: @@ -1027,12 +1165,12 @@ extension Process { @discardableResult static public func popen( arguments: [String], - environment: [String: String] = ProcessEnv.vars, + environmentBlock: ProcessEnvironmentBlock = ProcessEnv.block, loggingHandler: LoggingHandler? = .none ) throws -> ProcessResult { let process = Process( arguments: arguments, - environment: environment, + environmentBlock: environmentBlock, outputRedirection: .collect, loggingHandler: loggingHandler ) @@ -1040,6 +1178,17 @@ extension Process { return try process.waitUntilExit() } + @_disfavoredOverload + @available(*, deprecated, renamed: "popen(arguments:environmentBlock:loggingHandler:)") + @discardableResult + static public func popen( + arguments: [String], + environment: [String:String] = ProcessEnv.vars, + loggingHandler: LoggingHandler? = .none + ) throws -> ProcessResult { + try popen(arguments: arguments, environmentBlock: .init(environment), loggingHandler: loggingHandler) + } + /// Execute a subprocess and block until it finishes execution /// /// - Parameters: @@ -1054,10 +1203,21 @@ extension Process { @discardableResult static public func popen( args: String..., - environment: [String: String] = ProcessEnv.vars, + environmentBlock: ProcessEnvironmentBlock = ProcessEnv.block, + loggingHandler: LoggingHandler? = .none + ) throws -> ProcessResult { + return try Process.popen(arguments: args, environmentBlock: environmentBlock, loggingHandler: loggingHandler) + } + + @_disfavoredOverload + @available(*, deprecated, renamed: "popen(args:environmentBlock:loggingHandler:)") + @discardableResult + static public func popen( + args: String..., + environment: [String:String] = ProcessEnv.vars, loggingHandler: LoggingHandler? = .none ) throws -> ProcessResult { - return try Process.popen(arguments: args, environment: environment, loggingHandler: loggingHandler) + return try Process.popen(arguments: args, environmentBlock: .init(environment), loggingHandler: loggingHandler) } /// Execute a subprocess and get its (UTF-8) output if it has a non zero exit. @@ -1074,12 +1234,12 @@ extension Process { @discardableResult static public func checkNonZeroExit( arguments: [String], - environment: [String: String] = ProcessEnv.vars, + environmentBlock: ProcessEnvironmentBlock = ProcessEnv.block, loggingHandler: LoggingHandler? = .none ) throws -> String { let process = Process( arguments: arguments, - environment: environment, + environmentBlock: environmentBlock, outputRedirection: .collect, loggingHandler: loggingHandler ) @@ -1092,6 +1252,17 @@ extension Process { return try result.utf8Output() } + @_disfavoredOverload + @available(*, deprecated, renamed: "checkNonZeroExit(arguments:environmentBlock:loggingHandler:)") + @discardableResult + static public func checkNonZeroExit( + arguments: [String], + environment: [String:String] = ProcessEnv.vars, + loggingHandler: LoggingHandler? = .none + ) throws -> String { + try checkNonZeroExit(arguments: arguments, environmentBlock: .init(environment), loggingHandler: loggingHandler) + } + /// Execute a subprocess and get its (UTF-8) output if it has a non zero exit. /// /// - Parameters: @@ -1106,10 +1277,21 @@ extension Process { @discardableResult static public func checkNonZeroExit( args: String..., - environment: [String: String] = ProcessEnv.vars, + environmentBlock: ProcessEnvironmentBlock = ProcessEnv.block, + loggingHandler: LoggingHandler? = .none + ) throws -> String { + return try checkNonZeroExit(arguments: args, environmentBlock: environmentBlock, loggingHandler: loggingHandler) + } + + @_disfavoredOverload + @available(*, deprecated, renamed: "checkNonZeroExit(args:environmentBlock:loggingHandler:)") + @discardableResult + static public func checkNonZeroExit( + args: String..., + environment: [String:String] = ProcessEnv.vars, loggingHandler: LoggingHandler? = .none ) throws -> String { - return try checkNonZeroExit(arguments: args, environment: environment, loggingHandler: loggingHandler) + try checkNonZeroExit(arguments: args, environmentBlock: .init(environment), loggingHandler: loggingHandler) } } diff --git a/Sources/TSCBasic/Process/ProcessEnv.swift b/Sources/TSCBasic/Process/ProcessEnv.swift index 226406aa..6d01d032 100644 --- a/Sources/TSCBasic/Process/ProcessEnv.swift +++ b/Sources/TSCBasic/Process/ProcessEnv.swift @@ -11,23 +11,124 @@ import Foundation import TSCLibc +public struct CaseInsensitiveString { + public let value: String + public init(_ value: String) { + self.value = value + } +} + +extension CaseInsensitiveString: Equatable { + public static func == (_ lhs: Self, _ rhs: Self) -> Bool { + // TODO: is this any faster than just doing a lowercased conversion and compare? + return lhs.value.caseInsensitiveCompare(rhs.value) == .orderedSame + } +} + +extension CaseInsensitiveString: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self.init(value) + } +} + +extension CaseInsensitiveString: Hashable { + public func hash(into hasher: inout Hasher) { + self.value.lowercased().hash(into: &hasher) + } +} + +extension CaseInsensitiveString: Sendable {} + +#if os(Windows) +public typealias ProcessEnvironmentBlock = [CaseInsensitiveString:String] +extension ProcessEnvironmentBlock { + public init(_ dictionary: [String:String]) { + self.init(uniqueKeysWithValues: dictionary.map { (CaseInsensitiveString($0.key), $0.value) }) + } +} +#else +public typealias ProcessEnvironmentBlock = [String:String] +#endif + +extension ProcessEnvironmentBlock: Sendable {} + /// Provides functionality related a process's environment. public enum ProcessEnv { + @available(*, deprecated, message: "Use `block` instead") + public static var vars: [String:String] { + #if os(Windows) + Dictionary(uniqueKeysWithValues: _vars.map { ($0.key.value, $0.value) }) + #else + _vars + #endif + } + /// Returns a dictionary containing the current environment. - public static var vars: [String: String] { _vars } - private static var _vars = ProcessInfo.processInfo.environment + public static var block: ProcessEnvironmentBlock { _vars } + +#if os(Windows) + private static var _vars: ProcessEnvironmentBlock = { + guard let lpwchEnvironment = GetEnvironmentStringsW() else { return [:] } + defer { FreeEnvironmentStringsW(lpwchEnvironment) } + var environment: ProcessEnvironmentBlock = [:] + var pVariable = UnsafePointer(lpwchEnvironment) + while let entry = String.decodeCString(pVariable, as: UTF16.self) { + if entry.result.isEmpty { break } + let parts = entry.result.split(separator: "=", maxSplits: 1, omittingEmptySubsequences: false) + if parts.count == 2 { + environment[CaseInsensitiveString(String(parts[0]))] = String(parts[1]) + } + pVariable = pVariable.advanced(by: entry.result.utf16.count + 1) + } + return environment + }() +#else + private static var _vars = ProcessEnvironmentBlock( + uniqueKeysWithValues: ProcessInfo.processInfo.environment.map { + (ProcessEnvironmentBlock.Key($0.key), $0.value) + } + ) +#endif /// Invalidate the cached env. public static func invalidateEnv() { - _vars = ProcessInfo.processInfo.environment +#if os(Windows) + guard let lpwchEnvironment = GetEnvironmentStringsW() else { + _vars = [:] + return + } + defer { FreeEnvironmentStringsW(lpwchEnvironment) } + + var environment: ProcessEnvironmentBlock = [:] + var pVariable = UnsafePointer(lpwchEnvironment) + while let entry = String.decodeCString(pVariable, as: UTF16.self) { + if entry.result.isEmpty { break } + let parts = entry.result.split(separator: "=", maxSplits: 1, omittingEmptySubsequences: false) + if parts.count == 2 { + environment[CaseInsensitiveString(String(parts[0]))] = String(parts[1]) + } + pVariable = pVariable.advanced(by: entry.result.utf16.count + 1) + } + _vars = environment +#else + _vars = ProcessEnvironmentBlock( + uniqueKeysWithValues: ProcessInfo.processInfo.environment.map { + (CaseInsensitiveString($0.key), $0.value) + } + ) +#endif } /// Set the given key and value in the process's environment. public static func setVar(_ key: String, value: String) throws { #if os(Windows) - guard TSCLibc._putenv("\(key)=\(value)") == 0 else { - throw SystemError.setenv(Int32(GetLastError()), key) + try key.withCString(encodedAs: UTF16.self) { pwszKey in + try value.withCString(encodedAs: UTF16.self) { pwszValue in + guard SetEnvironmentVariableW(pwszKey, pwszValue) else { + throw SystemError.setenv(Int32(GetLastError()), key) + } + } } #else guard TSCLibc.setenv(key, value, 1) == 0 else { @@ -40,7 +141,9 @@ public enum ProcessEnv { /// Unset the give key in the process's environment. public static func unsetVar(_ key: String) throws { #if os(Windows) - guard TSCLibc._putenv("\(key)=") == 0 else { + guard key.withCString(encodedAs: UTF16.self, { + SetEnvironmentVariableW($0, nil) + }) else { throw SystemError.unsetenv(Int32(GetLastError()), key) } #else @@ -53,12 +156,7 @@ public enum ProcessEnv { /// `PATH` variable in the process's environment (`Path` under Windows). public static var path: String? { -#if os(Windows) - let pathArg = "Path" -#else - let pathArg = "PATH" -#endif - return vars[pathArg] + return block["PATH"] } /// The current working directory of the process. @@ -70,9 +168,7 @@ public enum ProcessEnv { public static func chdir(_ path: AbsolutePath) throws { let path = path.pathString #if os(Windows) - guard path.withCString(encodedAs: UTF16.self, { - SetCurrentDirectoryW($0) - }) else { + guard path.withCString(encodedAs: UTF16.self, SetCurrentDirectoryW) else { throw SystemError.chdir(Int32(GetLastError()), path) } #else diff --git a/Tests/TSCBasicTests/ProcessEnvTests.swift b/Tests/TSCBasicTests/ProcessEnvTests.swift index aa54742f..013dc012 100644 --- a/Tests/TSCBasicTests/ProcessEnvTests.swift +++ b/Tests/TSCBasicTests/ProcessEnvTests.swift @@ -12,6 +12,9 @@ import XCTest import TSCBasic import TSCTestSupport +#if os(Windows) +import WinSDK +#endif class ProcessEnvTests: XCTestCase { @@ -55,4 +58,42 @@ class ProcessEnvTests: XCTestCase { } XCTAssertNil(ProcessEnv.vars[key]) } + + func testWin32API() throws { + #if os(Windows) + let variable: String = "SWIFT_TOOLS_SUPPORT_CORE_VARIABLE" + let value: String = "1" + + try variable.withCString(encodedAs: UTF16.self) { pwszVariable in + try value.withCString(encodedAs: UTF16.self) { pwszValue in + guard SetEnvironmentVariableW(pwszVariable, pwszValue) else { + throw XCTSkip("Failed to set environment variable") + } + } + } + + // Ensure that libc does not see the variable. + XCTAssertNil(getenv(variable)) + variable.withCString(encodedAs: UTF16.self) { pwszVariable in + XCTAssertNil(_wgetenv(pwszVariable)) + } + + // Ensure that we can read the variable + ProcessEnv.invalidateEnv() + XCTAssertEqual(ProcessEnv.block[ProcessEnvironmentBlock.Key(variable)], value) + + // Ensure that we can read the variable using the Win32 API. + variable.withCString(encodedAs: UTF16.self) { pwszVariable in + let dwLength = GetEnvironmentVariableW(pwszVariable, nil, 0) + withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength + 1)) { + let dwLength = GetEnvironmentVariableW(pwszVariable, $0.baseAddress, dwLength + 1) + XCTAssertEqual(dwLength, 1) + XCTAssertEqual(String(decodingCString: $0.baseAddress!, as: UTF16.self), value) + } + } + #else + throw XCTSkip("Win32 API is only available on Windows") + #endif + + } }