Skip to content

Commit fa6aae5

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: #446
1 parent 8732961 commit fa6aae5

File tree

2 files changed

+158
-34
lines changed

2 files changed

+158
-34
lines changed

Sources/TSCBasic/Process.swift

+110-19
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,97 @@ import Dispatch
2323

2424
import _Concurrency
2525

26+
public struct ProcessEnvironmentBlock {
27+
#if os(Windows)
28+
public typealias Key = CaseInsensitiveString
29+
#else
30+
public typealias Key = String
31+
#endif
32+
public typealias Value = String
33+
34+
private var storage: Dictionary<Key, Value>
35+
36+
private init(storage: Dictionary<Key, Value>) {
37+
self.storage = storage
38+
}
39+
40+
public init(dictionary: Dictionary<String, String> = [:]) {
41+
#if os(Windows)
42+
self.storage = .init(uniqueKeysWithValues: dictionary.map {
43+
(CaseInsensitiveString($0), $1)
44+
})
45+
#else
46+
self.storage = dictionary
47+
#endif
48+
}
49+
50+
internal init<S: Sequence>(uniqueKeysWithValues keysAndValues: S)
51+
where S.Element == (Key, Value) {
52+
storage = .init(uniqueKeysWithValues: keysAndValues)
53+
}
54+
55+
public var dictionary: Dictionary<String, String> {
56+
#if os(Windows)
57+
return Dictionary<String, String>(uniqueKeysWithValues: storage.map {
58+
($0.value, $1)
59+
})
60+
#else
61+
return storage
62+
#endif
63+
}
64+
65+
public var isEmpty: Bool {
66+
storage.isEmpty
67+
}
68+
69+
public subscript(_ key: String) -> Value? {
70+
get {
71+
return storage[Key(key)]
72+
}
73+
set {
74+
storage[Key(key)] = newValue
75+
}
76+
}
77+
78+
public subscript(_ key: String, default value: @autoclosure () -> Value) -> Value {
79+
return storage[Key(key), default: value()]
80+
}
81+
82+
public func contains(_ key: String) -> Bool {
83+
return storage.keys.contains(Key(key))
84+
}
85+
86+
public mutating func merge<S: Sequence>(_ other: S,
87+
uniquingKeysWith combine: (Value, Value) throws -> Value)
88+
rethrows where S.Element == (Key, Value) {
89+
try storage.merge(other, uniquingKeysWith: combine)
90+
}
91+
92+
public __consuming func merging<S: Sequence>(_ other: __owned S,
93+
uniquingKeysWith combine:
94+
(Value, Value) throws -> Value)
95+
rethrows -> ProcessEnvironmentBlock where S.Element == (Key, Value) {
96+
return try ProcessEnvironmentBlock(storage: storage.merging(other, uniquingKeysWith: combine))
97+
}
98+
}
99+
100+
extension ProcessEnvironmentBlock: Codable {
101+
}
102+
103+
extension ProcessEnvironmentBlock: Equatable {
104+
}
105+
106+
extension ProcessEnvironmentBlock: Hashable {
107+
}
108+
109+
extension ProcessEnvironmentBlock: Sequence {
110+
public typealias Iterator = Dictionary<Key, Value>.Iterator
111+
112+
public __consuming func makeIterator() -> Self.Iterator {
113+
storage.makeIterator()
114+
}
115+
}
116+
26117
/// Process result data which is available after process termination.
27118
public struct ProcessResult: CustomStringConvertible, Sendable {
28119

@@ -53,7 +144,7 @@ public struct ProcessResult: CustomStringConvertible, Sendable {
53144
public let arguments: [String]
54145

55146
/// The environment with which the process was launched.
56-
public let environment: [String: String]
147+
public let environment: ProcessEnvironmentBlock
57148

58149
/// The exit status of the process.
59150
public let exitStatus: ExitStatus
@@ -71,7 +162,7 @@ public struct ProcessResult: CustomStringConvertible, Sendable {
71162
/// See `waitpid(2)` for information on the exit status code.
72163
public init(
73164
arguments: [String],
74-
environment: [String: String],
165+
environment: ProcessEnvironmentBlock,
75166
exitStatusCode: Int32,
76167
normal: Bool,
77168
output: Result<[UInt8], Swift.Error>,
@@ -99,7 +190,7 @@ public struct ProcessResult: CustomStringConvertible, Sendable {
99190
/// Create an instance using an exit status and output result.
100191
public init(
101192
arguments: [String],
102-
environment: [String: String],
193+
environment: ProcessEnvironmentBlock,
103194
exitStatus: ExitStatus,
104195
output: Result<[UInt8], Swift.Error>,
105196
stderrOutput: Result<[UInt8], Swift.Error>
@@ -285,7 +376,7 @@ public final class Process {
285376
public let arguments: [String]
286377

287378
/// The environment with which the process was executed.
288-
public let environment: [String: String]
379+
public let environment: ProcessEnvironmentBlock
289380

290381
/// The path to the directory under which to run the process.
291382
public let workingDirectory: AbsolutePath?
@@ -359,7 +450,7 @@ public final class Process {
359450
@available(macOS 10.15, *)
360451
public init(
361452
arguments: [String],
362-
environment: [String: String] = ProcessEnv.vars,
453+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
363454
workingDirectory: AbsolutePath,
364455
outputRedirection: OutputRedirection = .collect,
365456
startNewProcessGroup: Bool = true,
@@ -379,7 +470,7 @@ public final class Process {
379470
@available(macOS 10.15, *)
380471
public convenience init(
381472
arguments: [String],
382-
environment: [String: String] = ProcessEnv.vars,
473+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
383474
workingDirectory: AbsolutePath,
384475
outputRedirection: OutputRedirection = .collect,
385476
verbose: Bool,
@@ -411,7 +502,7 @@ public final class Process {
411502
/// - loggingHandler: Handler for logging messages
412503
public init(
413504
arguments: [String],
414-
environment: [String: String] = ProcessEnv.vars,
505+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
415506
outputRedirection: OutputRedirection = .collect,
416507
startNewProcessGroup: Bool = true,
417508
loggingHandler: LoggingHandler? = .none
@@ -428,7 +519,7 @@ public final class Process {
428519
@available(*, deprecated, message: "use version without verbosity flag")
429520
public convenience init(
430521
arguments: [String],
431-
environment: [String: String] = ProcessEnv.vars,
522+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
432523
outputRedirection: OutputRedirection = .collect,
433524
verbose: Bool = Process.verbose,
434525
startNewProcessGroup: Bool = true
@@ -444,7 +535,7 @@ public final class Process {
444535

445536
public convenience init(
446537
args: String...,
447-
environment: [String: String] = ProcessEnv.vars,
538+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
448539
outputRedirection: OutputRedirection = .collect,
449540
loggingHandler: LoggingHandler? = .none
450541
) {
@@ -536,7 +627,7 @@ public final class Process {
536627
process.currentDirectoryURL = workingDirectory.asURL
537628
}
538629
process.executableURL = executablePath.asURL
539-
process.environment = environment
630+
process.environment = environment.dictionary
540631

541632
let stdinPipe = Pipe()
542633
process.standardInput = stdinPipe
@@ -989,7 +1080,7 @@ extension Process {
9891080
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
9901081
static public func popen(
9911082
arguments: [String],
992-
environment: [String: String] = ProcessEnv.vars,
1083+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
9931084
loggingHandler: LoggingHandler? = .none
9941085
) async throws -> ProcessResult {
9951086
let process = Process(
@@ -1012,7 +1103,7 @@ extension Process {
10121103
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
10131104
static public func popen(
10141105
args: String...,
1015-
environment: [String: String] = ProcessEnv.vars,
1106+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
10161107
loggingHandler: LoggingHandler? = .none
10171108
) async throws -> ProcessResult {
10181109
try await popen(arguments: args, environment: environment, loggingHandler: loggingHandler)
@@ -1030,7 +1121,7 @@ extension Process {
10301121
@discardableResult
10311122
static public func checkNonZeroExit(
10321123
arguments: [String],
1033-
environment: [String: String] = ProcessEnv.vars,
1124+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
10341125
loggingHandler: LoggingHandler? = .none
10351126
) async throws -> String {
10361127
let result = try await popen(arguments: arguments, environment: environment, loggingHandler: loggingHandler)
@@ -1053,7 +1144,7 @@ extension Process {
10531144
@discardableResult
10541145
static public func checkNonZeroExit(
10551146
args: String...,
1056-
environment: [String: String] = ProcessEnv.vars,
1147+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
10571148
loggingHandler: LoggingHandler? = .none
10581149
) async throws -> String {
10591150
try await checkNonZeroExit(arguments: args, environment: environment, loggingHandler: loggingHandler)
@@ -1075,7 +1166,7 @@ extension Process {
10751166
// #endif
10761167
static public func popen(
10771168
arguments: [String],
1078-
environment: [String: String] = ProcessEnv.vars,
1169+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
10791170
loggingHandler: LoggingHandler? = .none,
10801171
queue: DispatchQueue? = nil,
10811172
completion: @escaping (Result<ProcessResult, Swift.Error>) -> Void
@@ -1113,7 +1204,7 @@ extension Process {
11131204
@discardableResult
11141205
static public func popen(
11151206
arguments: [String],
1116-
environment: [String: String] = ProcessEnv.vars,
1207+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
11171208
loggingHandler: LoggingHandler? = .none
11181209
) throws -> ProcessResult {
11191210
let process = Process(
@@ -1140,7 +1231,7 @@ extension Process {
11401231
@discardableResult
11411232
static public func popen(
11421233
args: String...,
1143-
environment: [String: String] = ProcessEnv.vars,
1234+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
11441235
loggingHandler: LoggingHandler? = .none
11451236
) throws -> ProcessResult {
11461237
return try Process.popen(arguments: args, environment: environment, loggingHandler: loggingHandler)
@@ -1160,7 +1251,7 @@ extension Process {
11601251
@discardableResult
11611252
static public func checkNonZeroExit(
11621253
arguments: [String],
1163-
environment: [String: String] = ProcessEnv.vars,
1254+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
11641255
loggingHandler: LoggingHandler? = .none
11651256
) throws -> String {
11661257
let process = Process(
@@ -1192,7 +1283,7 @@ extension Process {
11921283
@discardableResult
11931284
static public func checkNonZeroExit(
11941285
args: String...,
1195-
environment: [String: String] = ProcessEnv.vars,
1286+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
11961287
loggingHandler: LoggingHandler? = .none
11971288
) throws -> String {
11981289
return try checkNonZeroExit(arguments: args, environment: environment, loggingHandler: loggingHandler)

Sources/TSCBasic/ProcessEnv.swift

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

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

1447
/// Provides functionality related a process's enviorment.
1548
public enum ProcessEnv {
1649

1750
/// Returns a dictionary containing the current environment.
18-
public static var vars: [String: String] { _vars }
19-
private static var _vars = ProcessInfo.processInfo.environment
51+
public static var vars: ProcessEnvironmentBlock { _vars }
52+
private static var _vars =
53+
ProcessEnvironmentBlock(dictionary: ProcessInfo.processInfo.environment)
2054

2155
/// Invalidate the cached env.
2256
public static func invalidateEnv() {
23-
_vars = ProcessInfo.processInfo.environment
57+
_vars = ProcessEnvironmentBlock(dictionary: ProcessInfo.processInfo.environment)
2458
}
2559

2660
/// Set the given key and value in the process's environment.
2761
public static func setVar(_ key: String, value: String) throws {
2862
#if os(Windows)
29-
guard TSCLibc._putenv("\(key)=\(value)") == 0 else {
30-
throw SystemError.setenv(Int32(GetLastError()), key)
63+
try key.withCString(encodedAs: UTF16.self) { pwszKey in
64+
try value.withCString(encodedAs: UTF16.self) { pwszValue in
65+
guard SetEnvironmentVariableW(pwszKey, pwszValue) else {
66+
throw SystemError.setenv(Int32(GetLastError()), key)
67+
}
68+
}
3169
}
3270
#else
3371
guard TSCLibc.setenv(key, value, 1) == 0 else {
@@ -40,7 +78,9 @@ public enum ProcessEnv {
4078
/// Unset the give key in the process's environment.
4179
public static func unsetVar(_ key: String) throws {
4280
#if os(Windows)
43-
guard TSCLibc._putenv("\(key)=") == 0 else {
81+
guard (key.withCString(encodedAs: UTF16.self) {
82+
SetEnvironmentVariableW($0, nil)
83+
}) else {
4484
throw SystemError.unsetenv(Int32(GetLastError()), key)
4585
}
4686
#else
@@ -53,12 +93,7 @@ public enum ProcessEnv {
5393

5494
/// `PATH` variable in the process's environment (`Path` under Windows).
5595
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]
96+
return vars["PATH"]
6297
}
6398

6499
/// The current working directory of the process.
@@ -70,9 +105,7 @@ public enum ProcessEnv {
70105
public static func chdir(_ path: AbsolutePath) throws {
71106
let path = path.pathString
72107
#if os(Windows)
73-
guard path.withCString(encodedAs: UTF16.self, {
74-
SetCurrentDirectoryW($0)
75-
}) else {
108+
guard path.withCString(encodedAs: UTF16.self, SetCurrentDirectoryW) else {
76109
throw SystemError.chdir(Int32(GetLastError()), path)
77110
}
78111
#else

0 commit comments

Comments
 (0)