diff --git a/Sources/TSCBasic/Process.swift b/Sources/TSCBasic/Process.swift index 5a396823..f012bb63 100644 --- a/Sources/TSCBasic/Process.swift +++ b/Sources/TSCBasic/Process.swift @@ -23,6 +23,50 @@ import Dispatch import _Concurrency +public struct ProcessEnvironmentBlock { +#if os(Windows) + internal typealias Key = CaseInsensitiveString +#else + internal typealias Key = String +#endif + + private var storage: Dictionary + + public init(dictionary: Dictionary) { +#if os(Windows) + self.storage = .init(uniqueKeysWithValues: dictionary.map { + (CaseInsensitiveString($0), $1) + }) +#else + self.storage = dictionary +#endif + } + + internal init(uniqueKeysWithValues keysAndValues: S) + where S.Element == (Key, String) { + storage = .init(uniqueKeysWithValues: keysAndValues) + } + + public var dictionary: Dictionary { +#if os(Windows) + return Dictionary(uniqueKeysWithValues: storage.map { + ($0.value, $1) + }) +#else + return storage +#endif + } + + public subscript(_ key: String) -> String? { +#if os(Windows) + return storage[CaseInsensitiveString(key)] +#else + return storage[key] +#endif + } +} + + /// Process result data which is available after process termination. public struct ProcessResult: CustomStringConvertible, Sendable { @@ -53,7 +97,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 +115,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 +143,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 +329,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 +403,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 +423,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 +455,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 +472,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 +488,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 +580,7 @@ public final class Process { process.currentDirectoryURL = workingDirectory.asURL } process.executableURL = executablePath.asURL - process.environment = environment + process.environment = environment.dictionary let stdinPipe = Pipe() process.standardInput = stdinPipe @@ -989,7 +1033,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 +1056,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 +1074,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 +1097,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 +1119,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 +1157,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 +1184,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 +1204,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 +1236,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..f96fbccc 100644 --- a/Sources/TSCBasic/ProcessEnv.swift +++ b/Sources/TSCBasic/ProcessEnv.swift @@ -9,25 +9,73 @@ */ import Foundation +#if os(Windows) +import WinSDK +#else import TSCLibc +#endif + +internal struct CaseInsensitiveString { + internal var value: String + + internal init(_ value: String) { + self.value = value + } +} + +extension CaseInsensitiveString: ExpressibleByStringLiteral { + internal init(stringLiteral value: String) { + self.value = value + } +} + +extension CaseInsensitiveString: Equatable { + internal static func == (_ lhs: Self, _ rhs: Self) -> Bool { + return lhs.value.lowercased() == rhs.value.lowercased() + } +} + +extension CaseInsensitiveString: Hashable { + internal 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. - public static var vars: [String: String] { _vars } - private static var _vars = ProcessInfo.processInfo.environment + public static var vars: ProcessEnvironmentBlock { _vars } +#if os(Windows) + private static var _vars = + ProcessEnvironmentBlock(uniqueKeysWithValues: ProcessInfo.processInfo.environment.map { + (CaseInsensitiveString($0), $1) + }) +#else + private static var _vars = + ProcessEnvironmentBlock(dictionary: ProcessInfo.processInfo.environment) +#endif /// Invalidate the cached env. public static func invalidateEnv() { - _vars = ProcessInfo.processInfo.environment +#if os(Windows) + _vars = ProcessEnvironmentBlock(uniqueKeysWithValues: ProcessInfo.processInfo.environment.map { + (CaseInsensitiveString($0), $1) + }) +#else + _vars = ProcessEnvironmentBlock(dictionary: 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 +88,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 +103,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 vars["PATH"] } /// The current working directory of the process. @@ -70,9 +115,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