Skip to content

Commit e78f87f

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 e78f87f

File tree

2 files changed

+108
-34
lines changed

2 files changed

+108
-34
lines changed

Sources/TSCBasic/Process.swift

+63-19
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,50 @@ 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+
#if os(Windows)
62+
return storage[CaseInsensitiveString(key)]
63+
#else
64+
return storage[key]
65+
#endif
66+
}
67+
}
68+
69+
2670
/// Process result data which is available after process termination.
2771
public struct ProcessResult: CustomStringConvertible, Sendable {
2872

@@ -53,7 +97,7 @@ public struct ProcessResult: CustomStringConvertible, Sendable {
5397
public let arguments: [String]
5498

5599
/// The environment with which the process was launched.
56-
public let environment: [String: String]
100+
public let environment: ProcessEnvironmentBlock
57101

58102
/// The exit status of the process.
59103
public let exitStatus: ExitStatus
@@ -71,7 +115,7 @@ public struct ProcessResult: CustomStringConvertible, Sendable {
71115
/// See `waitpid(2)` for information on the exit status code.
72116
public init(
73117
arguments: [String],
74-
environment: [String: String],
118+
environment: ProcessEnvironmentBlock,
75119
exitStatusCode: Int32,
76120
normal: Bool,
77121
output: Result<[UInt8], Swift.Error>,
@@ -99,7 +143,7 @@ public struct ProcessResult: CustomStringConvertible, Sendable {
99143
/// Create an instance using an exit status and output result.
100144
public init(
101145
arguments: [String],
102-
environment: [String: String],
146+
environment: ProcessEnvironmentBlock,
103147
exitStatus: ExitStatus,
104148
output: Result<[UInt8], Swift.Error>,
105149
stderrOutput: Result<[UInt8], Swift.Error>
@@ -285,7 +329,7 @@ public final class Process {
285329
public let arguments: [String]
286330

287331
/// The environment with which the process was executed.
288-
public let environment: [String: String]
332+
public let environment: ProcessEnvironmentBlock
289333

290334
/// The path to the directory under which to run the process.
291335
public let workingDirectory: AbsolutePath?
@@ -359,7 +403,7 @@ public final class Process {
359403
@available(macOS 10.15, *)
360404
public init(
361405
arguments: [String],
362-
environment: [String: String] = ProcessEnv.vars,
406+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
363407
workingDirectory: AbsolutePath,
364408
outputRedirection: OutputRedirection = .collect,
365409
startNewProcessGroup: Bool = true,
@@ -379,7 +423,7 @@ public final class Process {
379423
@available(macOS 10.15, *)
380424
public convenience init(
381425
arguments: [String],
382-
environment: [String: String] = ProcessEnv.vars,
426+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
383427
workingDirectory: AbsolutePath,
384428
outputRedirection: OutputRedirection = .collect,
385429
verbose: Bool,
@@ -411,7 +455,7 @@ public final class Process {
411455
/// - loggingHandler: Handler for logging messages
412456
public init(
413457
arguments: [String],
414-
environment: [String: String] = ProcessEnv.vars,
458+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
415459
outputRedirection: OutputRedirection = .collect,
416460
startNewProcessGroup: Bool = true,
417461
loggingHandler: LoggingHandler? = .none
@@ -428,7 +472,7 @@ public final class Process {
428472
@available(*, deprecated, message: "use version without verbosity flag")
429473
public convenience init(
430474
arguments: [String],
431-
environment: [String: String] = ProcessEnv.vars,
475+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
432476
outputRedirection: OutputRedirection = .collect,
433477
verbose: Bool = Process.verbose,
434478
startNewProcessGroup: Bool = true
@@ -444,7 +488,7 @@ public final class Process {
444488

445489
public convenience init(
446490
args: String...,
447-
environment: [String: String] = ProcessEnv.vars,
491+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
448492
outputRedirection: OutputRedirection = .collect,
449493
loggingHandler: LoggingHandler? = .none
450494
) {
@@ -536,7 +580,7 @@ public final class Process {
536580
process.currentDirectoryURL = workingDirectory.asURL
537581
}
538582
process.executableURL = executablePath.asURL
539-
process.environment = environment
583+
process.environment = environment.dictionary
540584

541585
let stdinPipe = Pipe()
542586
process.standardInput = stdinPipe
@@ -989,7 +1033,7 @@ extension Process {
9891033
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
9901034
static public func popen(
9911035
arguments: [String],
992-
environment: [String: String] = ProcessEnv.vars,
1036+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
9931037
loggingHandler: LoggingHandler? = .none
9941038
) async throws -> ProcessResult {
9951039
let process = Process(
@@ -1012,7 +1056,7 @@ extension Process {
10121056
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
10131057
static public func popen(
10141058
args: String...,
1015-
environment: [String: String] = ProcessEnv.vars,
1059+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
10161060
loggingHandler: LoggingHandler? = .none
10171061
) async throws -> ProcessResult {
10181062
try await popen(arguments: args, environment: environment, loggingHandler: loggingHandler)
@@ -1030,7 +1074,7 @@ extension Process {
10301074
@discardableResult
10311075
static public func checkNonZeroExit(
10321076
arguments: [String],
1033-
environment: [String: String] = ProcessEnv.vars,
1077+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
10341078
loggingHandler: LoggingHandler? = .none
10351079
) async throws -> String {
10361080
let result = try await popen(arguments: arguments, environment: environment, loggingHandler: loggingHandler)
@@ -1053,7 +1097,7 @@ extension Process {
10531097
@discardableResult
10541098
static public func checkNonZeroExit(
10551099
args: String...,
1056-
environment: [String: String] = ProcessEnv.vars,
1100+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
10571101
loggingHandler: LoggingHandler? = .none
10581102
) async throws -> String {
10591103
try await checkNonZeroExit(arguments: args, environment: environment, loggingHandler: loggingHandler)
@@ -1075,7 +1119,7 @@ extension Process {
10751119
// #endif
10761120
static public func popen(
10771121
arguments: [String],
1078-
environment: [String: String] = ProcessEnv.vars,
1122+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
10791123
loggingHandler: LoggingHandler? = .none,
10801124
queue: DispatchQueue? = nil,
10811125
completion: @escaping (Result<ProcessResult, Swift.Error>) -> Void
@@ -1113,7 +1157,7 @@ extension Process {
11131157
@discardableResult
11141158
static public func popen(
11151159
arguments: [String],
1116-
environment: [String: String] = ProcessEnv.vars,
1160+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
11171161
loggingHandler: LoggingHandler? = .none
11181162
) throws -> ProcessResult {
11191163
let process = Process(
@@ -1140,7 +1184,7 @@ extension Process {
11401184
@discardableResult
11411185
static public func popen(
11421186
args: String...,
1143-
environment: [String: String] = ProcessEnv.vars,
1187+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
11441188
loggingHandler: LoggingHandler? = .none
11451189
) throws -> ProcessResult {
11461190
return try Process.popen(arguments: args, environment: environment, loggingHandler: loggingHandler)
@@ -1160,7 +1204,7 @@ extension Process {
11601204
@discardableResult
11611205
static public func checkNonZeroExit(
11621206
arguments: [String],
1163-
environment: [String: String] = ProcessEnv.vars,
1207+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
11641208
loggingHandler: LoggingHandler? = .none
11651209
) throws -> String {
11661210
let process = Process(
@@ -1192,7 +1236,7 @@ extension Process {
11921236
@discardableResult
11931237
static public func checkNonZeroExit(
11941238
args: String...,
1195-
environment: [String: String] = ProcessEnv.vars,
1239+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
11961240
loggingHandler: LoggingHandler? = .none
11971241
) throws -> String {
11981242
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)