diff --git a/Sources/TSCBasic/Process.swift b/Sources/TSCBasic/Process.swift index 5a396823..874e0b45 100644 --- a/Sources/TSCBasic/Process.swift +++ b/Sources/TSCBasic/Process.swift @@ -23,6 +23,97 @@ import Dispatch import _Concurrency +public struct ProcessEnvironmentBlock { +#if os(Windows) + public typealias Key = CaseInsensitiveString +#else + public typealias Key = String +#endif + public typealias Value = String + + private var storage: Dictionary + + private init(storage: Dictionary) { + self.storage = storage + } + + 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, Value) { + storage = .init(uniqueKeysWithValues: keysAndValues) + } + + public var dictionary: Dictionary { +#if os(Windows) + return Dictionary(uniqueKeysWithValues: storage.map { + ($0.value, $1) + }) +#else + return storage +#endif + } + + public var isEmpty: Bool { + storage.isEmpty + } + + public subscript(_ key: String) -> Value? { + get { + return storage[Key(key)] + } + set { + storage[Key(key)] = newValue + } + } + + public subscript(_ key: String, default value: @autoclosure () -> Value) -> Value { + return storage[Key(key), default: value()] + } + + public func contains(_ key: String) -> Bool { + return storage.keys.contains(Key(key)) + } + + public mutating func merge(_ other: S, + uniquingKeysWith combine: (Value, Value) throws -> Value) + rethrows where S.Element == (Key, Value) { + try storage.merge(other, uniquingKeysWith: combine) + } + + public __consuming func merging(_ other: __owned S, + uniquingKeysWith combine: + (Value, Value) throws -> Value) + rethrows -> ProcessEnvironmentBlock where S.Element == (Key, Value) { + return try ProcessEnvironmentBlock(storage: storage.merging(other, uniquingKeysWith: combine)) + } +} + +extension ProcessEnvironmentBlock: Codable { +} + +extension ProcessEnvironmentBlock: Equatable { +} + +extension ProcessEnvironmentBlock: Hashable { +} + +extension ProcessEnvironmentBlock: Sequence { + public typealias Iterator = Dictionary.Iterator + + public __consuming func makeIterator() -> Self.Iterator { + storage.makeIterator() + } +} + /// Process result data which is available after process termination. public struct ProcessResult: CustomStringConvertible, Sendable { @@ -53,7 +144,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 +162,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 +190,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 +376,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 +450,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 +470,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 +502,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 +519,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 +535,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 +627,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 +1080,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 +1103,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 +1121,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 +1144,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 +1166,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 +1204,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 +1231,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 +1251,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 +1283,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..223a54b1 100644 --- a/Sources/TSCBasic/ProcessEnv.swift +++ b/Sources/TSCBasic/ProcessEnv.swift @@ -9,25 +9,63 @@ */ import Foundation +#if os(Windows) +import WinSDK +#else import TSCLibc +#endif + +public struct CaseInsensitiveString { + internal var value: String + + internal init(_ value: String) { + self.value = value + } +} + +extension CaseInsensitiveString: Codable { +} + +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. - public static var vars: [String: String] { _vars } - private static var _vars = ProcessInfo.processInfo.environment + public static var vars: ProcessEnvironmentBlock { _vars } + private static var _vars = + ProcessEnvironmentBlock(dictionary: ProcessInfo.processInfo.environment) /// Invalidate the cached env. public static func invalidateEnv() { - _vars = ProcessInfo.processInfo.environment + _vars = ProcessEnvironmentBlock(dictionary: ProcessInfo.processInfo.environment) } /// 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 +78,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 +93,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 +105,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