Skip to content

Commit 416729d

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 4d539ff commit 416729d

File tree

2 files changed

+118
-31
lines changed

2 files changed

+118
-31
lines changed

Sources/TSCBasic/Process.swift

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

2424
import _Concurrency
2525

26+
public struct ProcessEnvironmentBlock {
27+
#if os(Windows)
28+
internal typealias Key = CaseInsensitiveString
29+
private var storage: Dictionary<CaseInsensitiveString, String>
30+
#else
31+
internal typealias Key = String
32+
private var storage: Dictionary<String, String>
33+
#endif
34+
35+
#if !os(Windows)
36+
internal init(dictionary: Dictionary<String, String>) {
37+
self.storage = dictionary
38+
}
39+
#endif
40+
41+
internal init<S: Sequence>(uniqueKeysWithValues keysAndValues: S)
42+
where S.Element == (Key, String) {
43+
storage = .init(uniqueKeysWithValues: keysAndValues)
44+
}
45+
46+
internal var dictionary: Dictionary<String, String> {
47+
#if os(Windows)
48+
return Dictionary<String, String>(uniqueKeysWithValues: storage.map {
49+
($0.value, $1)
50+
})
51+
#else
52+
return storage
53+
#endif
54+
}
55+
56+
public subscript(_ key: String) -> String? {
57+
#if os(Windows)
58+
return storage[CaseInsensitiveString(key)]
59+
#else
60+
return storage[key]
61+
#endif
62+
}
63+
}
64+
65+
2666
/// Process result data which is available after process termination.
2767
public struct ProcessResult: CustomStringConvertible, Sendable {
2868

@@ -53,7 +93,7 @@ public struct ProcessResult: CustomStringConvertible, Sendable {
5393
public let arguments: [String]
5494

5595
/// The environment with which the process was launched.
56-
public let environment: [String: String]
96+
public let environment: ProcessEnvironmentBlock
5797

5898
/// The exit status of the process.
5999
public let exitStatus: ExitStatus
@@ -71,7 +111,7 @@ public struct ProcessResult: CustomStringConvertible, Sendable {
71111
/// See `waitpid(2)` for information on the exit status code.
72112
public init(
73113
arguments: [String],
74-
environment: [String: String],
114+
environment: ProcessEnvironmentBlock,
75115
exitStatusCode: Int32,
76116
normal: Bool,
77117
output: Result<[UInt8], Swift.Error>,
@@ -99,7 +139,7 @@ public struct ProcessResult: CustomStringConvertible, Sendable {
99139
/// Create an instance using an exit status and output result.
100140
public init(
101141
arguments: [String],
102-
environment: [String: String],
142+
environment: ProcessEnvironmentBlock,
103143
exitStatus: ExitStatus,
104144
output: Result<[UInt8], Swift.Error>,
105145
stderrOutput: Result<[UInt8], Swift.Error>
@@ -285,7 +325,7 @@ public final class Process {
285325
public let arguments: [String]
286326

287327
/// The environment with which the process was executed.
288-
public let environment: [String: String]
328+
public let environment: ProcessEnvironmentBlock
289329

290330
/// The path to the directory under which to run the process.
291331
public let workingDirectory: AbsolutePath?
@@ -359,7 +399,7 @@ public final class Process {
359399
@available(macOS 10.15, *)
360400
public init(
361401
arguments: [String],
362-
environment: [String: String] = ProcessEnv.vars,
402+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
363403
workingDirectory: AbsolutePath,
364404
outputRedirection: OutputRedirection = .collect,
365405
startNewProcessGroup: Bool = true,
@@ -379,7 +419,7 @@ public final class Process {
379419
@available(macOS 10.15, *)
380420
public convenience init(
381421
arguments: [String],
382-
environment: [String: String] = ProcessEnv.vars,
422+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
383423
workingDirectory: AbsolutePath,
384424
outputRedirection: OutputRedirection = .collect,
385425
verbose: Bool,
@@ -411,7 +451,7 @@ public final class Process {
411451
/// - loggingHandler: Handler for logging messages
412452
public init(
413453
arguments: [String],
414-
environment: [String: String] = ProcessEnv.vars,
454+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
415455
outputRedirection: OutputRedirection = .collect,
416456
startNewProcessGroup: Bool = true,
417457
loggingHandler: LoggingHandler? = .none
@@ -428,7 +468,7 @@ public final class Process {
428468
@available(*, deprecated, message: "use version without verbosity flag")
429469
public convenience init(
430470
arguments: [String],
431-
environment: [String: String] = ProcessEnv.vars,
471+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
432472
outputRedirection: OutputRedirection = .collect,
433473
verbose: Bool = Process.verbose,
434474
startNewProcessGroup: Bool = true
@@ -444,7 +484,7 @@ public final class Process {
444484

445485
public convenience init(
446486
args: String...,
447-
environment: [String: String] = ProcessEnv.vars,
487+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
448488
outputRedirection: OutputRedirection = .collect,
449489
loggingHandler: LoggingHandler? = .none
450490
) {
@@ -536,7 +576,7 @@ public final class Process {
536576
process.currentDirectoryURL = workingDirectory.asURL
537577
}
538578
process.executableURL = executablePath.asURL
539-
process.environment = environment
579+
process.environment = environment.dictionary
540580

541581
let stdinPipe = Pipe()
542582
process.standardInput = stdinPipe
@@ -989,7 +1029,7 @@ extension Process {
9891029
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
9901030
static public func popen(
9911031
arguments: [String],
992-
environment: [String: String] = ProcessEnv.vars,
1032+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
9931033
loggingHandler: LoggingHandler? = .none
9941034
) async throws -> ProcessResult {
9951035
let process = Process(
@@ -1012,7 +1052,7 @@ extension Process {
10121052
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
10131053
static public func popen(
10141054
args: String...,
1015-
environment: [String: String] = ProcessEnv.vars,
1055+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
10161056
loggingHandler: LoggingHandler? = .none
10171057
) async throws -> ProcessResult {
10181058
try await popen(arguments: args, environment: environment, loggingHandler: loggingHandler)
@@ -1030,7 +1070,7 @@ extension Process {
10301070
@discardableResult
10311071
static public func checkNonZeroExit(
10321072
arguments: [String],
1033-
environment: [String: String] = ProcessEnv.vars,
1073+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
10341074
loggingHandler: LoggingHandler? = .none
10351075
) async throws -> String {
10361076
let result = try await popen(arguments: arguments, environment: environment, loggingHandler: loggingHandler)
@@ -1053,7 +1093,7 @@ extension Process {
10531093
@discardableResult
10541094
static public func checkNonZeroExit(
10551095
args: String...,
1056-
environment: [String: String] = ProcessEnv.vars,
1096+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
10571097
loggingHandler: LoggingHandler? = .none
10581098
) async throws -> String {
10591099
try await checkNonZeroExit(arguments: args, environment: environment, loggingHandler: loggingHandler)
@@ -1075,7 +1115,7 @@ extension Process {
10751115
// #endif
10761116
static public func popen(
10771117
arguments: [String],
1078-
environment: [String: String] = ProcessEnv.vars,
1118+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
10791119
loggingHandler: LoggingHandler? = .none,
10801120
queue: DispatchQueue? = nil,
10811121
completion: @escaping (Result<ProcessResult, Swift.Error>) -> Void
@@ -1113,7 +1153,7 @@ extension Process {
11131153
@discardableResult
11141154
static public func popen(
11151155
arguments: [String],
1116-
environment: [String: String] = ProcessEnv.vars,
1156+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
11171157
loggingHandler: LoggingHandler? = .none
11181158
) throws -> ProcessResult {
11191159
let process = Process(
@@ -1140,7 +1180,7 @@ extension Process {
11401180
@discardableResult
11411181
static public func popen(
11421182
args: String...,
1143-
environment: [String: String] = ProcessEnv.vars,
1183+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
11441184
loggingHandler: LoggingHandler? = .none
11451185
) throws -> ProcessResult {
11461186
return try Process.popen(arguments: args, environment: environment, loggingHandler: loggingHandler)
@@ -1160,7 +1200,7 @@ extension Process {
11601200
@discardableResult
11611201
static public func checkNonZeroExit(
11621202
arguments: [String],
1163-
environment: [String: String] = ProcessEnv.vars,
1203+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
11641204
loggingHandler: LoggingHandler? = .none
11651205
) throws -> String {
11661206
let process = Process(
@@ -1192,7 +1232,7 @@ extension Process {
11921232
@discardableResult
11931233
static public func checkNonZeroExit(
11941234
args: String...,
1195-
environment: [String: String] = ProcessEnv.vars,
1235+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
11961236
loggingHandler: LoggingHandler? = .none
11971237
) throws -> String {
11981238
return try checkNonZeroExit(arguments: args, environment: environment, loggingHandler: loggingHandler)

Sources/TSCBasic/ProcessEnv.swift

+59-12
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,73 @@
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+
#if os(Windows)
50+
private static var _vars =
51+
ProcessEnvironmentBlock(uniqueKeysWithValues: ProcessInfo.processInfo.environment.map {
52+
(CaseInsensitiveString($0), $1)
53+
})
54+
#else
55+
private static var _vars =
56+
ProcessEnvironmentBlock(dictionary: ProcessInfo.processInfo.environment)
57+
#endif
2058

2159
/// Invalidate the cached env.
2260
public static func invalidateEnv() {
23-
_vars = ProcessInfo.processInfo.environment
61+
#if os(Windows)
62+
_vars = ProcessEnvironmentBlock(uniqueKeysWithValues: ProcessInfo.processInfo.environment.map {
63+
(CaseInsensitiveString($0), $1)
64+
})
65+
#else
66+
_vars = ProcessEnvironmentBlock(dictionary: ProcessInfo.processInfo.environment)
67+
#endif
2468
}
2569

2670
/// Set the given key and value in the process's environment.
2771
public static func setVar(_ key: String, value: String) throws {
2872
#if os(Windows)
29-
guard TSCLibc._putenv("\(key)=\(value)") == 0 else {
30-
throw SystemError.setenv(Int32(GetLastError()), key)
73+
try key.withCString(encodedAs: UTF16.self) { pwszKey in
74+
try value.withCString(encodedAs: UTF16.self) { pwszValue in
75+
guard SetEnvironmentVariableW(pwszKey, pwszValue) else {
76+
throw SystemError.setenv(Int32(GetLastError()), key)
77+
}
78+
}
3179
}
3280
#else
3381
guard TSCLibc.setenv(key, value, 1) == 0 else {
@@ -40,7 +88,9 @@ public enum ProcessEnv {
4088
/// Unset the give key in the process's environment.
4189
public static func unsetVar(_ key: String) throws {
4290
#if os(Windows)
43-
guard TSCLibc._putenv("\(key)=") == 0 else {
91+
guard (key.withCString(encodedAs: UTF16.self) {
92+
SetEnvironmentVariableW($0, nil)
93+
}) else {
4494
throw SystemError.unsetenv(Int32(GetLastError()), key)
4595
}
4696
#else
@@ -54,11 +104,10 @@ public enum ProcessEnv {
54104
/// `PATH` variable in the process's environment (`Path` under Windows).
55105
public static var path: String? {
56106
#if os(Windows)
57-
let pathArg = "Path"
107+
return vars["Path"]
58108
#else
59-
let pathArg = "PATH"
109+
return vas["PATH"]
60110
#endif
61-
return vars[pathArg]
62111
}
63112

64113
/// The current working directory of the process.
@@ -70,9 +119,7 @@ public enum ProcessEnv {
70119
public static func chdir(_ path: AbsolutePath) throws {
71120
let path = path.pathString
72121
#if os(Windows)
73-
guard path.withCString(encodedAs: UTF16.self, {
74-
SetCurrentDirectoryW($0)
75-
}) else {
122+
guard path.withCString(encodedAs: UTF16.self, SetCurrentDirectoryW) else {
76123
throw SystemError.chdir(Int32(GetLastError()), path)
77124
}
78125
#else

0 commit comments

Comments
 (0)