Skip to content

Commit 1fbfb9f

Browse files
committed
TSCBasic: support case insensitivity for environment
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: swiftlang#446
1 parent 8732961 commit 1fbfb9f

File tree

2 files changed

+117
-34
lines changed

2 files changed

+117
-34
lines changed

Sources/TSCBasic/Process.swift

+72-19
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,59 @@ import Dispatch
2323

2424
import _Concurrency
2525

26+
public struct ProcessEnvironmentBlock {
27+
#if os(Windows)
28+
internal typealias Key = CaseInsensitiveString
29+
#else
30+
internal typealias Key = String
31+
#endif
32+
33+
private var storage: Dictionary<Key, String>
34+
35+
public init(dictionary: Dictionary<String, String> = [:]) {
36+
#if os(Windows)
37+
self.storage = .init(uniqueKeysWithValues: dictionary.map {
38+
(CaseInsensitiveString($0), $1)
39+
})
40+
#else
41+
self.storage = dictionary
42+
#endif
43+
}
44+
45+
internal init<S: Sequence>(uniqueKeysWithValues keysAndValues: S)
46+
where S.Element == (Key, String) {
47+
storage = .init(uniqueKeysWithValues: keysAndValues)
48+
}
49+
50+
public var dictionary: Dictionary<String, String> {
51+
#if os(Windows)
52+
return Dictionary<String, String>(uniqueKeysWithValues: storage.map {
53+
($0.value, $1)
54+
})
55+
#else
56+
return storage
57+
#endif
58+
}
59+
60+
public subscript(_ key: String) -> String? {
61+
get {
62+
return storage[Key(key)]
63+
}
64+
set {
65+
storage[Key(key)] = newValue
66+
}
67+
}
68+
69+
public subscript(_ key: String, default value: String) -> String {
70+
return storage[Key(key), default: value]
71+
}
72+
73+
public func contains(_ key: String) -> Bool {
74+
return storage.keys.contains(Key(key))
75+
}
76+
}
77+
78+
2679
/// Process result data which is available after process termination.
2780
public struct ProcessResult: CustomStringConvertible, Sendable {
2881

@@ -53,7 +106,7 @@ public struct ProcessResult: CustomStringConvertible, Sendable {
53106
public let arguments: [String]
54107

55108
/// The environment with which the process was launched.
56-
public let environment: [String: String]
109+
public let environment: ProcessEnvironmentBlock
57110

58111
/// The exit status of the process.
59112
public let exitStatus: ExitStatus
@@ -71,7 +124,7 @@ public struct ProcessResult: CustomStringConvertible, Sendable {
71124
/// See `waitpid(2)` for information on the exit status code.
72125
public init(
73126
arguments: [String],
74-
environment: [String: String],
127+
environment: ProcessEnvironmentBlock,
75128
exitStatusCode: Int32,
76129
normal: Bool,
77130
output: Result<[UInt8], Swift.Error>,
@@ -99,7 +152,7 @@ public struct ProcessResult: CustomStringConvertible, Sendable {
99152
/// Create an instance using an exit status and output result.
100153
public init(
101154
arguments: [String],
102-
environment: [String: String],
155+
environment: ProcessEnvironmentBlock,
103156
exitStatus: ExitStatus,
104157
output: Result<[UInt8], Swift.Error>,
105158
stderrOutput: Result<[UInt8], Swift.Error>
@@ -285,7 +338,7 @@ public final class Process {
285338
public let arguments: [String]
286339

287340
/// The environment with which the process was executed.
288-
public let environment: [String: String]
341+
public let environment: ProcessEnvironmentBlock
289342

290343
/// The path to the directory under which to run the process.
291344
public let workingDirectory: AbsolutePath?
@@ -359,7 +412,7 @@ public final class Process {
359412
@available(macOS 10.15, *)
360413
public init(
361414
arguments: [String],
362-
environment: [String: String] = ProcessEnv.vars,
415+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
363416
workingDirectory: AbsolutePath,
364417
outputRedirection: OutputRedirection = .collect,
365418
startNewProcessGroup: Bool = true,
@@ -379,7 +432,7 @@ public final class Process {
379432
@available(macOS 10.15, *)
380433
public convenience init(
381434
arguments: [String],
382-
environment: [String: String] = ProcessEnv.vars,
435+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
383436
workingDirectory: AbsolutePath,
384437
outputRedirection: OutputRedirection = .collect,
385438
verbose: Bool,
@@ -411,7 +464,7 @@ public final class Process {
411464
/// - loggingHandler: Handler for logging messages
412465
public init(
413466
arguments: [String],
414-
environment: [String: String] = ProcessEnv.vars,
467+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
415468
outputRedirection: OutputRedirection = .collect,
416469
startNewProcessGroup: Bool = true,
417470
loggingHandler: LoggingHandler? = .none
@@ -428,7 +481,7 @@ public final class Process {
428481
@available(*, deprecated, message: "use version without verbosity flag")
429482
public convenience init(
430483
arguments: [String],
431-
environment: [String: String] = ProcessEnv.vars,
484+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
432485
outputRedirection: OutputRedirection = .collect,
433486
verbose: Bool = Process.verbose,
434487
startNewProcessGroup: Bool = true
@@ -444,7 +497,7 @@ public final class Process {
444497

445498
public convenience init(
446499
args: String...,
447-
environment: [String: String] = ProcessEnv.vars,
500+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
448501
outputRedirection: OutputRedirection = .collect,
449502
loggingHandler: LoggingHandler? = .none
450503
) {
@@ -536,7 +589,7 @@ public final class Process {
536589
process.currentDirectoryURL = workingDirectory.asURL
537590
}
538591
process.executableURL = executablePath.asURL
539-
process.environment = environment
592+
process.environment = environment.dictionary
540593

541594
let stdinPipe = Pipe()
542595
process.standardInput = stdinPipe
@@ -989,7 +1042,7 @@ extension Process {
9891042
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
9901043
static public func popen(
9911044
arguments: [String],
992-
environment: [String: String] = ProcessEnv.vars,
1045+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
9931046
loggingHandler: LoggingHandler? = .none
9941047
) async throws -> ProcessResult {
9951048
let process = Process(
@@ -1012,7 +1065,7 @@ extension Process {
10121065
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
10131066
static public func popen(
10141067
args: String...,
1015-
environment: [String: String] = ProcessEnv.vars,
1068+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
10161069
loggingHandler: LoggingHandler? = .none
10171070
) async throws -> ProcessResult {
10181071
try await popen(arguments: args, environment: environment, loggingHandler: loggingHandler)
@@ -1030,7 +1083,7 @@ extension Process {
10301083
@discardableResult
10311084
static public func checkNonZeroExit(
10321085
arguments: [String],
1033-
environment: [String: String] = ProcessEnv.vars,
1086+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
10341087
loggingHandler: LoggingHandler? = .none
10351088
) async throws -> String {
10361089
let result = try await popen(arguments: arguments, environment: environment, loggingHandler: loggingHandler)
@@ -1053,7 +1106,7 @@ extension Process {
10531106
@discardableResult
10541107
static public func checkNonZeroExit(
10551108
args: String...,
1056-
environment: [String: String] = ProcessEnv.vars,
1109+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
10571110
loggingHandler: LoggingHandler? = .none
10581111
) async throws -> String {
10591112
try await checkNonZeroExit(arguments: args, environment: environment, loggingHandler: loggingHandler)
@@ -1075,7 +1128,7 @@ extension Process {
10751128
// #endif
10761129
static public func popen(
10771130
arguments: [String],
1078-
environment: [String: String] = ProcessEnv.vars,
1131+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
10791132
loggingHandler: LoggingHandler? = .none,
10801133
queue: DispatchQueue? = nil,
10811134
completion: @escaping (Result<ProcessResult, Swift.Error>) -> Void
@@ -1113,7 +1166,7 @@ extension Process {
11131166
@discardableResult
11141167
static public func popen(
11151168
arguments: [String],
1116-
environment: [String: String] = ProcessEnv.vars,
1169+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
11171170
loggingHandler: LoggingHandler? = .none
11181171
) throws -> ProcessResult {
11191172
let process = Process(
@@ -1140,7 +1193,7 @@ extension Process {
11401193
@discardableResult
11411194
static public func popen(
11421195
args: String...,
1143-
environment: [String: String] = ProcessEnv.vars,
1196+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
11441197
loggingHandler: LoggingHandler? = .none
11451198
) throws -> ProcessResult {
11461199
return try Process.popen(arguments: args, environment: environment, loggingHandler: loggingHandler)
@@ -1160,7 +1213,7 @@ extension Process {
11601213
@discardableResult
11611214
static public func checkNonZeroExit(
11621215
arguments: [String],
1163-
environment: [String: String] = ProcessEnv.vars,
1216+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
11641217
loggingHandler: LoggingHandler? = .none
11651218
) throws -> String {
11661219
let process = Process(
@@ -1192,7 +1245,7 @@ extension Process {
11921245
@discardableResult
11931246
static public func checkNonZeroExit(
11941247
args: String...,
1195-
environment: [String: String] = ProcessEnv.vars,
1248+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
11961249
loggingHandler: LoggingHandler? = .none
11971250
) throws -> String {
11981251
return try checkNonZeroExit(arguments: args, environment: environment, loggingHandler: loggingHandler)

Sources/TSCBasic/ProcessEnv.swift

+45-15
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,60 @@
99
*/
1010

1111
import Foundation
12+
#if os(Windows)
13+
import WinSDK
14+
#else
1215
import TSCLibc
16+
#endif
17+
18+
internal struct CaseInsensitiveString {
19+
internal var value: String
20+
21+
internal init(_ value: String) {
22+
self.value = value
23+
}
24+
}
25+
26+
extension CaseInsensitiveString: ExpressibleByStringLiteral {
27+
internal init(stringLiteral value: String) {
28+
self.value = value
29+
}
30+
}
31+
32+
extension CaseInsensitiveString: Equatable {
33+
internal static func == (_ lhs: Self, _ rhs: Self) -> Bool {
34+
return lhs.value.lowercased() == rhs.value.lowercased()
35+
}
36+
}
37+
38+
extension CaseInsensitiveString: Hashable {
39+
internal func hash(into hasher: inout Hasher) {
40+
self.value.lowercased().hash(into: &hasher)
41+
}
42+
}
1343

1444
/// Provides functionality related a process's enviorment.
1545
public enum ProcessEnv {
1646

1747
/// Returns a dictionary containing the current environment.
18-
public static var vars: [String: String] { _vars }
19-
private static var _vars = ProcessInfo.processInfo.environment
48+
public static var vars: ProcessEnvironmentBlock { _vars }
49+
private static var _vars =
50+
ProcessEnvironmentBlock(dictionary: ProcessInfo.processInfo.environment)
2051

2152
/// Invalidate the cached env.
2253
public static func invalidateEnv() {
23-
_vars = ProcessInfo.processInfo.environment
54+
_vars = ProcessEnvironmentBlock(dictionary: ProcessInfo.processInfo.environment)
2455
}
2556

2657
/// Set the given key and value in the process's environment.
2758
public static func setVar(_ key: String, value: String) throws {
2859
#if os(Windows)
29-
guard TSCLibc._putenv("\(key)=\(value)") == 0 else {
30-
throw SystemError.setenv(Int32(GetLastError()), key)
60+
try key.withCString(encodedAs: UTF16.self) { pwszKey in
61+
try value.withCString(encodedAs: UTF16.self) { pwszValue in
62+
guard SetEnvironmentVariableW(pwszKey, pwszValue) else {
63+
throw SystemError.setenv(Int32(GetLastError()), key)
64+
}
65+
}
3166
}
3267
#else
3368
guard TSCLibc.setenv(key, value, 1) == 0 else {
@@ -40,7 +75,9 @@ public enum ProcessEnv {
4075
/// Unset the give key in the process's environment.
4176
public static func unsetVar(_ key: String) throws {
4277
#if os(Windows)
43-
guard TSCLibc._putenv("\(key)=") == 0 else {
78+
guard (key.withCString(encodedAs: UTF16.self) {
79+
SetEnvironmentVariableW($0, nil)
80+
}) else {
4481
throw SystemError.unsetenv(Int32(GetLastError()), key)
4582
}
4683
#else
@@ -53,12 +90,7 @@ public enum ProcessEnv {
5390

5491
/// `PATH` variable in the process's environment (`Path` under Windows).
5592
public static var path: String? {
56-
#if os(Windows)
57-
let pathArg = "Path"
58-
#else
59-
let pathArg = "PATH"
60-
#endif
61-
return vars[pathArg]
93+
return vars["PATH"]
6294
}
6395

6496
/// The current working directory of the process.
@@ -70,9 +102,7 @@ public enum ProcessEnv {
70102
public static func chdir(_ path: AbsolutePath) throws {
71103
let path = path.pathString
72104
#if os(Windows)
73-
guard path.withCString(encodedAs: UTF16.self, {
74-
SetCurrentDirectoryW($0)
75-
}) else {
105+
guard path.withCString(encodedAs: UTF16.self, SetCurrentDirectoryW) else {
76106
throw SystemError.chdir(Int32(GetLastError()), path)
77107
}
78108
#else

0 commit comments

Comments
 (0)