Skip to content

Commit 9e8d570

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 9e8d570

File tree

2 files changed

+105
-27
lines changed

2 files changed

+105
-27
lines changed

Sources/TSCBasic/Process.swift

+48-18
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,30 @@ import Dispatch
2323

2424
import _Concurrency
2525

26+
public struct ProcessEnvironmentBlock {
27+
#if os(Windows)
28+
public typealias Key = CaseInsensitiveString
29+
private var storage: Dictionary<CaseInsensitiveString, String>
30+
#else
31+
public typealias Key = String
32+
private var storage: Dictionary<String, String>
33+
#endif
34+
35+
public init<S: Sequence>(uniqueKeysWithValues keysAndValues: S)
36+
where S.Element == (Key, String) {
37+
storage.init(uniqueKeysWithValues: keysAndValues)
38+
}
39+
40+
public subscript(_ key: String) -> String {
41+
#if os(Windows)
42+
return stroage[CaseInsensitiveString(key)]
43+
#else
44+
return storage[key]
45+
#endif
46+
}
47+
}
48+
49+
2650
/// Process result data which is available after process termination.
2751
public struct ProcessResult: CustomStringConvertible, Sendable {
2852

@@ -53,7 +77,7 @@ public struct ProcessResult: CustomStringConvertible, Sendable {
5377
public let arguments: [String]
5478

5579
/// The environment with which the process was launched.
56-
public let environment: [String: String]
80+
public let environment: ProcessEnvironmentBlock
5781

5882
/// The exit status of the process.
5983
public let exitStatus: ExitStatus
@@ -71,7 +95,7 @@ public struct ProcessResult: CustomStringConvertible, Sendable {
7195
/// See `waitpid(2)` for information on the exit status code.
7296
public init(
7397
arguments: [String],
74-
environment: [String: String],
98+
environment: ProcessEnvironmentBlock,
7599
exitStatusCode: Int32,
76100
normal: Bool,
77101
output: Result<[UInt8], Swift.Error>,
@@ -99,7 +123,7 @@ public struct ProcessResult: CustomStringConvertible, Sendable {
99123
/// Create an instance using an exit status and output result.
100124
public init(
101125
arguments: [String],
102-
environment: [String: String],
126+
environment: ProcessEnvironmentBlock,
103127
exitStatus: ExitStatus,
104128
output: Result<[UInt8], Swift.Error>,
105129
stderrOutput: Result<[UInt8], Swift.Error>
@@ -285,7 +309,7 @@ public final class Process {
285309
public let arguments: [String]
286310

287311
/// The environment with which the process was executed.
288-
public let environment: [String: String]
312+
public let environment: ProcessEnvironmentBlock
289313

290314
/// The path to the directory under which to run the process.
291315
public let workingDirectory: AbsolutePath?
@@ -359,7 +383,7 @@ public final class Process {
359383
@available(macOS 10.15, *)
360384
public init(
361385
arguments: [String],
362-
environment: [String: String] = ProcessEnv.vars,
386+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
363387
workingDirectory: AbsolutePath,
364388
outputRedirection: OutputRedirection = .collect,
365389
startNewProcessGroup: Bool = true,
@@ -379,7 +403,7 @@ public final class Process {
379403
@available(macOS 10.15, *)
380404
public convenience init(
381405
arguments: [String],
382-
environment: [String: String] = ProcessEnv.vars,
406+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
383407
workingDirectory: AbsolutePath,
384408
outputRedirection: OutputRedirection = .collect,
385409
verbose: Bool,
@@ -411,7 +435,7 @@ public final class Process {
411435
/// - loggingHandler: Handler for logging messages
412436
public init(
413437
arguments: [String],
414-
environment: [String: String] = ProcessEnv.vars,
438+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
415439
outputRedirection: OutputRedirection = .collect,
416440
startNewProcessGroup: Bool = true,
417441
loggingHandler: LoggingHandler? = .none
@@ -428,7 +452,7 @@ public final class Process {
428452
@available(*, deprecated, message: "use version without verbosity flag")
429453
public convenience init(
430454
arguments: [String],
431-
environment: [String: String] = ProcessEnv.vars,
455+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
432456
outputRedirection: OutputRedirection = .collect,
433457
verbose: Bool = Process.verbose,
434458
startNewProcessGroup: Bool = true
@@ -444,7 +468,7 @@ public final class Process {
444468

445469
public convenience init(
446470
args: String...,
447-
environment: [String: String] = ProcessEnv.vars,
471+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
448472
outputRedirection: OutputRedirection = .collect,
449473
loggingHandler: LoggingHandler? = .none
450474
) {
@@ -536,7 +560,13 @@ public final class Process {
536560
process.currentDirectoryURL = workingDirectory.asURL
537561
}
538562
process.executableURL = executablePath.asURL
563+
#if os(Windows)
564+
process.environment = .init(uniqueKeysWithValues: environment.map {
565+
($0.value, $1)
566+
})
567+
#else
539568
process.environment = environment
569+
#endif
540570

541571
let stdinPipe = Pipe()
542572
process.standardInput = stdinPipe
@@ -989,7 +1019,7 @@ extension Process {
9891019
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
9901020
static public func popen(
9911021
arguments: [String],
992-
environment: [String: String] = ProcessEnv.vars,
1022+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
9931023
loggingHandler: LoggingHandler? = .none
9941024
) async throws -> ProcessResult {
9951025
let process = Process(
@@ -1012,7 +1042,7 @@ extension Process {
10121042
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
10131043
static public func popen(
10141044
args: String...,
1015-
environment: [String: String] = ProcessEnv.vars,
1045+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
10161046
loggingHandler: LoggingHandler? = .none
10171047
) async throws -> ProcessResult {
10181048
try await popen(arguments: args, environment: environment, loggingHandler: loggingHandler)
@@ -1030,7 +1060,7 @@ extension Process {
10301060
@discardableResult
10311061
static public func checkNonZeroExit(
10321062
arguments: [String],
1033-
environment: [String: String] = ProcessEnv.vars,
1063+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
10341064
loggingHandler: LoggingHandler? = .none
10351065
) async throws -> String {
10361066
let result = try await popen(arguments: arguments, environment: environment, loggingHandler: loggingHandler)
@@ -1053,7 +1083,7 @@ extension Process {
10531083
@discardableResult
10541084
static public func checkNonZeroExit(
10551085
args: String...,
1056-
environment: [String: String] = ProcessEnv.vars,
1086+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
10571087
loggingHandler: LoggingHandler? = .none
10581088
) async throws -> String {
10591089
try await checkNonZeroExit(arguments: args, environment: environment, loggingHandler: loggingHandler)
@@ -1075,7 +1105,7 @@ extension Process {
10751105
// #endif
10761106
static public func popen(
10771107
arguments: [String],
1078-
environment: [String: String] = ProcessEnv.vars,
1108+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
10791109
loggingHandler: LoggingHandler? = .none,
10801110
queue: DispatchQueue? = nil,
10811111
completion: @escaping (Result<ProcessResult, Swift.Error>) -> Void
@@ -1113,7 +1143,7 @@ extension Process {
11131143
@discardableResult
11141144
static public func popen(
11151145
arguments: [String],
1116-
environment: [String: String] = ProcessEnv.vars,
1146+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
11171147
loggingHandler: LoggingHandler? = .none
11181148
) throws -> ProcessResult {
11191149
let process = Process(
@@ -1140,7 +1170,7 @@ extension Process {
11401170
@discardableResult
11411171
static public func popen(
11421172
args: String...,
1143-
environment: [String: String] = ProcessEnv.vars,
1173+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
11441174
loggingHandler: LoggingHandler? = .none
11451175
) throws -> ProcessResult {
11461176
return try Process.popen(arguments: args, environment: environment, loggingHandler: loggingHandler)
@@ -1160,7 +1190,7 @@ extension Process {
11601190
@discardableResult
11611191
static public func checkNonZeroExit(
11621192
arguments: [String],
1163-
environment: [String: String] = ProcessEnv.vars,
1193+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
11641194
loggingHandler: LoggingHandler? = .none
11651195
) throws -> String {
11661196
let process = Process(
@@ -1192,7 +1222,7 @@ extension Process {
11921222
@discardableResult
11931223
static public func checkNonZeroExit(
11941224
args: String...,
1195-
environment: [String: String] = ProcessEnv.vars,
1225+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
11961226
loggingHandler: LoggingHandler? = .none
11971227
) throws -> String {
11981228
return try checkNonZeroExit(arguments: args, environment: environment, loggingHandler: loggingHandler)

Sources/TSCBasic/ProcessEnv.swift

+57-9
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,74 @@
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+
public init(_ value: String) {
22+
self.value = value
23+
}
24+
}
25+
26+
extension CaseInsensitiveString: ExpressibleByStringLiteral {
27+
public init(stringLiteral value: String) {
28+
self.value = value
29+
}
30+
}
31+
32+
extension CaseInsensitiveString: Equatable {
33+
public static func == (_ lhs: Self, _ rhs: Self) -> Bool {
34+
return lhs.value.lowercased() == rhs.value.lowercased()
35+
}
36+
}
37+
38+
extension CaseInsensitiveString: Hashable {
39+
public 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.
48+
#if os(Windows)
49+
public static var vars: [CaseInsensitiveString: String] { _vars }
50+
private static var _vars: Dictionary<CaseInsensitiveString, String> = {
51+
.init(uniqueKeysWithValues: ProcessInfo.processInfo.environment.map {
52+
(CaseInsensitiveString($0), $1)
53+
})
54+
}()
55+
#else
1856
public static var vars: [String: String] { _vars }
1957
private static var _vars = ProcessInfo.processInfo.environment
58+
#endif
2059

2160
/// Invalidate the cached env.
2261
public static func invalidateEnv() {
62+
#if os(Windows)
63+
_vars = .init(uniqueKeysWithValues: ProcessInfo.processInfo.environment.map {
64+
(CaseInsensitiveString($0), $1)
65+
})
66+
#else
2367
_vars = ProcessInfo.processInfo.environment
68+
#endif
2469
}
2570

2671
/// Set the given key and value in the process's environment.
2772
public static func setVar(_ key: String, value: String) throws {
2873
#if os(Windows)
29-
guard TSCLibc._putenv("\(key)=\(value)") == 0 else {
30-
throw SystemError.setenv(Int32(GetLastError()), key)
74+
try key.withCString(encodedAs: UTF16.self) { pwszKey in
75+
try value.withCString(encodedAs: UTF16.self) { pwszValue in
76+
guard SetEnvironmentVariableW(pwszKey, pwszValue) else {
77+
throw SystemError.setenv(Int32(GetLastError()), key)
78+
}
79+
}
3180
}
3281
#else
3382
guard TSCLibc.setenv(key, value, 1) == 0 else {
@@ -40,7 +89,9 @@ public enum ProcessEnv {
4089
/// Unset the give key in the process's environment.
4190
public static func unsetVar(_ key: String) throws {
4291
#if os(Windows)
43-
guard TSCLibc._putenv("\(key)=") == 0 else {
92+
guard (key.withCString(encodedAs: UTF16.self) {
93+
SetEnvironmentVariableW($0, nil)
94+
}) else {
4495
throw SystemError.unsetenv(Int32(GetLastError()), key)
4596
}
4697
#else
@@ -54,11 +105,10 @@ public enum ProcessEnv {
54105
/// `PATH` variable in the process's environment (`Path` under Windows).
55106
public static var path: String? {
56107
#if os(Windows)
57-
let pathArg = "Path"
108+
return vars["Path"]
58109
#else
59-
let pathArg = "PATH"
110+
return vas["PATH"]
60111
#endif
61-
return vars[pathArg]
62112
}
63113

64114
/// The current working directory of the process.
@@ -70,9 +120,7 @@ public enum ProcessEnv {
70120
public static func chdir(_ path: AbsolutePath) throws {
71121
let path = path.pathString
72122
#if os(Windows)
73-
guard path.withCString(encodedAs: UTF16.self, {
74-
SetCurrentDirectoryW($0)
75-
}) else {
123+
guard path.withCString(encodedAs: UTF16.self, SetCurrentDirectoryW) else {
76124
throw SystemError.chdir(Int32(GetLastError()), path)
77125
}
78126
#else

0 commit comments

Comments
 (0)