Skip to content

Commit 010a72f

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 4d539ff commit 010a72f

File tree

2 files changed

+88
-27
lines changed

2 files changed

+88
-27
lines changed

Sources/TSCBasic/Process.swift

+31-18
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ import Dispatch
2323

2424
import _Concurrency
2525

26+
#if os(Windows)
27+
public typealias ProcessEnvironmentBlock = Dictionary<CaseInsensitiveString, String>
28+
#else
29+
public typealias ProcessEnvironmentBlock = Dictionary<String, String>
30+
#endif
31+
32+
2633
/// Process result data which is available after process termination.
2734
public struct ProcessResult: CustomStringConvertible, Sendable {
2835

@@ -53,7 +60,7 @@ public struct ProcessResult: CustomStringConvertible, Sendable {
5360
public let arguments: [String]
5461

5562
/// The environment with which the process was launched.
56-
public let environment: [String: String]
63+
public let environment: ProcessEnvironmentBlock
5764

5865
/// The exit status of the process.
5966
public let exitStatus: ExitStatus
@@ -71,7 +78,7 @@ public struct ProcessResult: CustomStringConvertible, Sendable {
7178
/// See `waitpid(2)` for information on the exit status code.
7279
public init(
7380
arguments: [String],
74-
environment: [String: String],
81+
environment: ProcessEnvironmentBlock,
7582
exitStatusCode: Int32,
7683
normal: Bool,
7784
output: Result<[UInt8], Swift.Error>,
@@ -99,7 +106,7 @@ public struct ProcessResult: CustomStringConvertible, Sendable {
99106
/// Create an instance using an exit status and output result.
100107
public init(
101108
arguments: [String],
102-
environment: [String: String],
109+
environment: ProcessEnvironmentBlock,
103110
exitStatus: ExitStatus,
104111
output: Result<[UInt8], Swift.Error>,
105112
stderrOutput: Result<[UInt8], Swift.Error>
@@ -285,7 +292,7 @@ public final class Process {
285292
public let arguments: [String]
286293

287294
/// The environment with which the process was executed.
288-
public let environment: [String: String]
295+
public let environment: ProcessEnvironmentBlock
289296

290297
/// The path to the directory under which to run the process.
291298
public let workingDirectory: AbsolutePath?
@@ -359,7 +366,7 @@ public final class Process {
359366
@available(macOS 10.15, *)
360367
public init(
361368
arguments: [String],
362-
environment: [String: String] = ProcessEnv.vars,
369+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
363370
workingDirectory: AbsolutePath,
364371
outputRedirection: OutputRedirection = .collect,
365372
startNewProcessGroup: Bool = true,
@@ -379,7 +386,7 @@ public final class Process {
379386
@available(macOS 10.15, *)
380387
public convenience init(
381388
arguments: [String],
382-
environment: [String: String] = ProcessEnv.vars,
389+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
383390
workingDirectory: AbsolutePath,
384391
outputRedirection: OutputRedirection = .collect,
385392
verbose: Bool,
@@ -411,7 +418,7 @@ public final class Process {
411418
/// - loggingHandler: Handler for logging messages
412419
public init(
413420
arguments: [String],
414-
environment: [String: String] = ProcessEnv.vars,
421+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
415422
outputRedirection: OutputRedirection = .collect,
416423
startNewProcessGroup: Bool = true,
417424
loggingHandler: LoggingHandler? = .none
@@ -428,7 +435,7 @@ public final class Process {
428435
@available(*, deprecated, message: "use version without verbosity flag")
429436
public convenience init(
430437
arguments: [String],
431-
environment: [String: String] = ProcessEnv.vars,
438+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
432439
outputRedirection: OutputRedirection = .collect,
433440
verbose: Bool = Process.verbose,
434441
startNewProcessGroup: Bool = true
@@ -444,7 +451,7 @@ public final class Process {
444451

445452
public convenience init(
446453
args: String...,
447-
environment: [String: String] = ProcessEnv.vars,
454+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
448455
outputRedirection: OutputRedirection = .collect,
449456
loggingHandler: LoggingHandler? = .none
450457
) {
@@ -536,7 +543,13 @@ public final class Process {
536543
process.currentDirectoryURL = workingDirectory.asURL
537544
}
538545
process.executableURL = executablePath.asURL
546+
#if os(Windows)
547+
process.environment = .init(uniqueKeysWithValues: environment.map {
548+
($0.value, $1)
549+
})
550+
#else
539551
process.environment = environment
552+
#endif
540553

541554
let stdinPipe = Pipe()
542555
process.standardInput = stdinPipe
@@ -989,7 +1002,7 @@ extension Process {
9891002
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
9901003
static public func popen(
9911004
arguments: [String],
992-
environment: [String: String] = ProcessEnv.vars,
1005+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
9931006
loggingHandler: LoggingHandler? = .none
9941007
) async throws -> ProcessResult {
9951008
let process = Process(
@@ -1012,7 +1025,7 @@ extension Process {
10121025
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
10131026
static public func popen(
10141027
args: String...,
1015-
environment: [String: String] = ProcessEnv.vars,
1028+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
10161029
loggingHandler: LoggingHandler? = .none
10171030
) async throws -> ProcessResult {
10181031
try await popen(arguments: args, environment: environment, loggingHandler: loggingHandler)
@@ -1030,7 +1043,7 @@ extension Process {
10301043
@discardableResult
10311044
static public func checkNonZeroExit(
10321045
arguments: [String],
1033-
environment: [String: String] = ProcessEnv.vars,
1046+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
10341047
loggingHandler: LoggingHandler? = .none
10351048
) async throws -> String {
10361049
let result = try await popen(arguments: arguments, environment: environment, loggingHandler: loggingHandler)
@@ -1053,7 +1066,7 @@ extension Process {
10531066
@discardableResult
10541067
static public func checkNonZeroExit(
10551068
args: String...,
1056-
environment: [String: String] = ProcessEnv.vars,
1069+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
10571070
loggingHandler: LoggingHandler? = .none
10581071
) async throws -> String {
10591072
try await checkNonZeroExit(arguments: args, environment: environment, loggingHandler: loggingHandler)
@@ -1075,7 +1088,7 @@ extension Process {
10751088
// #endif
10761089
static public func popen(
10771090
arguments: [String],
1078-
environment: [String: String] = ProcessEnv.vars,
1091+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
10791092
loggingHandler: LoggingHandler? = .none,
10801093
queue: DispatchQueue? = nil,
10811094
completion: @escaping (Result<ProcessResult, Swift.Error>) -> Void
@@ -1113,7 +1126,7 @@ extension Process {
11131126
@discardableResult
11141127
static public func popen(
11151128
arguments: [String],
1116-
environment: [String: String] = ProcessEnv.vars,
1129+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
11171130
loggingHandler: LoggingHandler? = .none
11181131
) throws -> ProcessResult {
11191132
let process = Process(
@@ -1140,7 +1153,7 @@ extension Process {
11401153
@discardableResult
11411154
static public func popen(
11421155
args: String...,
1143-
environment: [String: String] = ProcessEnv.vars,
1156+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
11441157
loggingHandler: LoggingHandler? = .none
11451158
) throws -> ProcessResult {
11461159
return try Process.popen(arguments: args, environment: environment, loggingHandler: loggingHandler)
@@ -1160,7 +1173,7 @@ extension Process {
11601173
@discardableResult
11611174
static public func checkNonZeroExit(
11621175
arguments: [String],
1163-
environment: [String: String] = ProcessEnv.vars,
1176+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
11641177
loggingHandler: LoggingHandler? = .none
11651178
) throws -> String {
11661179
let process = Process(
@@ -1192,7 +1205,7 @@ extension Process {
11921205
@discardableResult
11931206
static public func checkNonZeroExit(
11941207
args: String...,
1195-
environment: [String: String] = ProcessEnv.vars,
1208+
environment: ProcessEnvironmentBlock = ProcessEnv.vars,
11961209
loggingHandler: LoggingHandler? = .none
11971210
) throws -> String {
11981211
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)