Skip to content

Commit

Permalink
TSCBasic: support case insensitivity for environment
Browse files Browse the repository at this point in the history
Windows is case insensitive for the environment control block, which we
did not honour.  This causes issues when Swift is used with programs
which are incorrectly cased (e.g. emacs).  Introduce an explicit wrapper
type for Windows to make the lookup case insensitive, canonicalising the
name to lowercase.  This allows us to treat `Path` and `PATH`
identically (along with any other environment variable and case
matching) which respects the Windows behaviour.  Additionally, migrate
away from the POSIX variants which do not handle the case properly to
the Windows version which does.

Fixes: #446
  • Loading branch information
compnerd committed Dec 9, 2023
1 parent 8732961 commit e78f87f
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 34 deletions.
82 changes: 63 additions & 19 deletions Sources/TSCBasic/Process.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Key, String>

public init(dictionary: Dictionary<String, String>) {
#if os(Windows)
self.storage = .init(uniqueKeysWithValues: dictionary.map {
(CaseInsensitiveString($0), $1)
})
#else
self.storage = dictionary
#endif
}

internal init<S: Sequence>(uniqueKeysWithValues keysAndValues: S)
where S.Element == (Key, String) {
storage = .init(uniqueKeysWithValues: keysAndValues)
}

public var dictionary: Dictionary<String, String> {
#if os(Windows)
return Dictionary<String, String>(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 {

Expand Down Expand Up @@ -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
Expand All @@ -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>,
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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?
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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<ProcessResult, Swift.Error>) -> Void
Expand Down Expand Up @@ -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(
Expand All @@ -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)
Expand All @@ -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(
Expand Down Expand Up @@ -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)
Expand Down
60 changes: 45 additions & 15 deletions Sources/TSCBasic/ProcessEnv.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,60 @@
*/

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 }
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 {
Expand All @@ -40,7 +75,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
Expand All @@ -53,12 +90,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.
Expand All @@ -70,9 +102,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
Expand Down

0 comments on commit e78f87f

Please sign in to comment.