diff --git a/Sources/TSCBasic/Process.swift b/Sources/TSCBasic/Process.swift index 5a396823..d8619e98 100644 --- a/Sources/TSCBasic/Process.swift +++ b/Sources/TSCBasic/Process.swift @@ -23,6 +23,13 @@ import Dispatch import _Concurrency +#if os(Windows) +public typealias ProcessEnvironmentBlock = Dictionary +#else +public typealias ProcessEnvironmentBlock = Dictionary +#endif + + /// Process result data which is available after process termination. public struct ProcessResult: CustomStringConvertible, Sendable { @@ -53,7 +60,7 @@ public struct ProcessResult: CustomStringConvertible, Sendable { public let arguments: [String] /// The environment with which the process was launched. - public let environment: [String: String] + public let environment: ProcessEnvironmentBlock /// The exit status of the process. public let exitStatus: ExitStatus @@ -71,7 +78,7 @@ public struct ProcessResult: CustomStringConvertible, Sendable { /// See `waitpid(2)` for information on the exit status code. public init( arguments: [String], - environment: [String: String], + environment: ProcessEnvironmentBlock, exitStatusCode: Int32, normal: Bool, output: Result<[UInt8], Swift.Error>, @@ -99,7 +106,7 @@ public struct ProcessResult: CustomStringConvertible, Sendable { /// Create an instance using an exit status and output result. public init( arguments: [String], - environment: [String: String], + environment: ProcessEnvironmentBlock, exitStatus: ExitStatus, output: Result<[UInt8], Swift.Error>, stderrOutput: Result<[UInt8], Swift.Error> @@ -285,7 +292,7 @@ public final class Process { public let arguments: [String] /// The environment with which the process was executed. - public let environment: [String: String] + public let environment: ProcessEnvironmentBlock /// The path to the directory under which to run the process. public let workingDirectory: AbsolutePath? @@ -359,7 +366,7 @@ public final class Process { @available(macOS 10.15, *) public init( arguments: [String], - environment: [String: String] = ProcessEnv.vars, + environment: ProcessEnvironmentBlock = ProcessEnv.vars, workingDirectory: AbsolutePath, outputRedirection: OutputRedirection = .collect, startNewProcessGroup: Bool = true, @@ -379,7 +386,7 @@ public final class Process { @available(macOS 10.15, *) public convenience init( arguments: [String], - environment: [String: String] = ProcessEnv.vars, + environment: ProcessEnvironmentBlock = ProcessEnv.vars, workingDirectory: AbsolutePath, outputRedirection: OutputRedirection = .collect, verbose: Bool, @@ -411,7 +418,7 @@ public final class Process { /// - loggingHandler: Handler for logging messages public init( arguments: [String], - environment: [String: String] = ProcessEnv.vars, + environment: ProcessEnvironmentBlock = ProcessEnv.vars, outputRedirection: OutputRedirection = .collect, startNewProcessGroup: Bool = true, loggingHandler: LoggingHandler? = .none @@ -428,7 +435,7 @@ public final class Process { @available(*, deprecated, message: "use version without verbosity flag") public convenience init( arguments: [String], - environment: [String: String] = ProcessEnv.vars, + environment: ProcessEnvironmentBlock = ProcessEnv.vars, outputRedirection: OutputRedirection = .collect, verbose: Bool = Process.verbose, startNewProcessGroup: Bool = true @@ -444,7 +451,7 @@ public final class Process { public convenience init( args: String..., - environment: [String: String] = ProcessEnv.vars, + environment: ProcessEnvironmentBlock = ProcessEnv.vars, outputRedirection: OutputRedirection = .collect, loggingHandler: LoggingHandler? = .none ) { @@ -536,7 +543,13 @@ public final class Process { process.currentDirectoryURL = workingDirectory.asURL } process.executableURL = executablePath.asURL +#if os(Windows) + process.environment = .init(uniqueKeysWithValues: environment.map { + ($0.value, $1) + }) +#else process.environment = environment +#endif let stdinPipe = Pipe() process.standardInput = stdinPipe @@ -989,7 +1002,7 @@ extension Process { @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) static public func popen( arguments: [String], - environment: [String: String] = ProcessEnv.vars, + environment: ProcessEnvironmentBlock = ProcessEnv.vars, loggingHandler: LoggingHandler? = .none ) async throws -> ProcessResult { let process = Process( @@ -1012,7 +1025,7 @@ extension Process { @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) static public func popen( args: String..., - environment: [String: String] = ProcessEnv.vars, + environment: ProcessEnvironmentBlock = ProcessEnv.vars, loggingHandler: LoggingHandler? = .none ) async throws -> ProcessResult { try await popen(arguments: args, environment: environment, loggingHandler: loggingHandler) @@ -1030,7 +1043,7 @@ extension Process { @discardableResult static public func checkNonZeroExit( arguments: [String], - environment: [String: String] = ProcessEnv.vars, + environment: ProcessEnvironmentBlock = ProcessEnv.vars, loggingHandler: LoggingHandler? = .none ) async throws -> String { let result = try await popen(arguments: arguments, environment: environment, loggingHandler: loggingHandler) @@ -1053,7 +1066,7 @@ extension Process { @discardableResult static public func checkNonZeroExit( args: String..., - environment: [String: String] = ProcessEnv.vars, + environment: ProcessEnvironmentBlock = ProcessEnv.vars, loggingHandler: LoggingHandler? = .none ) async throws -> String { try await checkNonZeroExit(arguments: args, environment: environment, loggingHandler: loggingHandler) @@ -1075,7 +1088,7 @@ extension Process { // #endif static public func popen( arguments: [String], - environment: [String: String] = ProcessEnv.vars, + environment: ProcessEnvironmentBlock = ProcessEnv.vars, loggingHandler: LoggingHandler? = .none, queue: DispatchQueue? = nil, completion: @escaping (Result) -> Void @@ -1113,7 +1126,7 @@ extension Process { @discardableResult static public func popen( arguments: [String], - environment: [String: String] = ProcessEnv.vars, + environment: ProcessEnvironmentBlock = ProcessEnv.vars, loggingHandler: LoggingHandler? = .none ) throws -> ProcessResult { let process = Process( @@ -1140,7 +1153,7 @@ extension Process { @discardableResult static public func popen( args: String..., - environment: [String: String] = ProcessEnv.vars, + environment: ProcessEnvironmentBlock = ProcessEnv.vars, loggingHandler: LoggingHandler? = .none ) throws -> ProcessResult { return try Process.popen(arguments: args, environment: environment, loggingHandler: loggingHandler) @@ -1160,7 +1173,7 @@ extension Process { @discardableResult static public func checkNonZeroExit( arguments: [String], - environment: [String: String] = ProcessEnv.vars, + environment: ProcessEnvironmentBlock = ProcessEnv.vars, loggingHandler: LoggingHandler? = .none ) throws -> String { let process = Process( @@ -1192,7 +1205,7 @@ extension Process { @discardableResult static public func checkNonZeroExit( args: String..., - environment: [String: String] = ProcessEnv.vars, + environment: ProcessEnvironmentBlock = ProcessEnv.vars, loggingHandler: LoggingHandler? = .none ) throws -> String { return try checkNonZeroExit(arguments: args, environment: environment, loggingHandler: loggingHandler) diff --git a/Sources/TSCBasic/ProcessEnv.swift b/Sources/TSCBasic/ProcessEnv.swift index bf7e3c62..90639916 100644 --- a/Sources/TSCBasic/ProcessEnv.swift +++ b/Sources/TSCBasic/ProcessEnv.swift @@ -9,25 +9,74 @@ */ import Foundation +#if os(Windows) +import WinSDK +#else import TSCLibc +#endif + +public struct CaseInsensitiveString { + internal var value: String + + public init(_ value: String) { + self.value = value + } +} + +extension CaseInsensitiveString: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self.value = value + } +} + +extension CaseInsensitiveString: Equatable { + public static func == (_ lhs: Self, _ rhs: Self) -> Bool { + return lhs.value.lowercased() == rhs.value.lowercased() + } +} + +extension CaseInsensitiveString: Hashable { + public func hash(into hasher: inout Hasher) { + self.value.lowercased().hash(into: &hasher) + } +} /// Provides functionality related a process's enviorment. public enum ProcessEnv { /// Returns a dictionary containing the current environment. +#if os(Windows) + public static var vars: [CaseInsensitiveString: String] { _vars } + private static var _vars: Dictionary = { + .init(uniqueKeysWithValues: ProcessInfo.processInfo.environment.map { + (CaseInsensitiveString($0), $1) + }) + }() +#else public static var vars: [String: String] { _vars } private static var _vars = ProcessInfo.processInfo.environment +#endif /// Invalidate the cached env. public static func invalidateEnv() { +#if os(Windows) + _vars = .init(uniqueKeysWithValues: ProcessInfo.processInfo.environment.map { + (CaseInsensitiveString($0), $1) + }) +#else _vars = ProcessInfo.processInfo.environment +#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 +89,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 @@ -54,11 +105,10 @@ public enum ProcessEnv { /// `PATH` variable in the process's environment (`Path` under Windows). public static var path: String? { #if os(Windows) - let pathArg = "Path" + return vars["Path"] #else - let pathArg = "PATH" + return vas["PATH"] #endif - return vars[pathArg] } /// The current working directory of the process. @@ -70,9 +120,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