Skip to content

Commit 64429c8

Browse files
committed
TSCBasic: avoid using C library functions for environment
Prefer to use the Win32 APIs for environment management. Note that this is going to break any user of libc on Windows. Setting environment variables through this function will not be reflected in the C library only the Win32 APIs. Furthermore, we use the unicode variants always as the unicode and ANSI environment may diverge as not all unicode (UTF-16) is translatable to ANSI, which is documented at [1]. [1] https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/getenv-wgetenv?view=msvc-170&viewFallbackFrom=vs-2019.
1 parent f57674f commit 64429c8

File tree

2 files changed

+89
-6
lines changed

2 files changed

+89
-6
lines changed

Sources/TSCBasic/Process/ProcessEnv.swift

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,26 +67,68 @@ public enum ProcessEnv {
6767
/// Returns a dictionary containing the current environment.
6868
public static var block: ProcessEnvironmentBlock { _vars }
6969

70+
#if os(Windows)
71+
private static var _vars: ProcessEnvironmentBlock = {
72+
guard let lpwchEnvironment = GetEnvironmentStringsW() else { return [:] }
73+
defer { FreeEnvironmentStringsW(lpwchEnvironment) }
74+
var environment: ProcessEnvironmentBlock = [:]
75+
var pVariable = UnsafePointer<WCHAR>(lpwchEnvironment)
76+
while let entry = String.decodeCString(pVariable, as: UTF16.self) {
77+
if entry.result.isEmpty { break }
78+
let parts = entry.result.split(separator: "=", maxSplits: 1, omittingEmptySubsequences: false)
79+
if parts.count == 2 {
80+
environment[CaseInsensitiveString(String(parts[0]))] = String(parts[1])
81+
}
82+
pVariable = pVariable.advanced(by: entry.result.utf16.count + 1)
83+
}
84+
return environment
85+
}()
86+
#else
7087
private static var _vars = ProcessEnvironmentBlock(
7188
uniqueKeysWithValues: ProcessInfo.processInfo.environment.map {
7289
(ProcessEnvironmentBlock.Key($0.key), $0.value)
7390
}
7491
)
92+
#endif
7593

7694
/// Invalidate the cached env.
7795
public static func invalidateEnv() {
96+
#if os(Windows)
97+
guard let lpwchEnvironment = GetEnvironmentStringsW() else {
98+
_vars = [:]
99+
return
100+
}
101+
defer { FreeEnvironmentStringsW(lpwchEnvironment) }
102+
103+
var environment: ProcessEnvironmentBlock = [:]
104+
var pVariable = UnsafePointer<WCHAR>(lpwchEnvironment)
105+
while let entry = String.decodeCString(pVariable, as: UTF16.self) {
106+
if entry.result.isEmpty { break }
107+
let parts = entry.result.split(separator: "=", maxSplits: 1, omittingEmptySubsequences: false)
108+
if parts.count == 2 {
109+
environment[CaseInsensitiveString(String(parts[0]))] = String(parts[1])
110+
}
111+
pVariable = pVariable.advanced(by: entry.result.utf16.count + 1)
112+
}
113+
_vars = environment
114+
#else
78115
_vars = ProcessEnvironmentBlock(
79116
uniqueKeysWithValues: ProcessInfo.processInfo.environment.map {
80117
(CaseInsensitiveString($0.key), $0.value)
81118
}
82119
)
120+
#endif
83121
}
84122

85123
/// Set the given key and value in the process's environment.
86124
public static func setVar(_ key: String, value: String) throws {
87125
#if os(Windows)
88-
guard TSCLibc._putenv("\(key)=\(value)") == 0 else {
89-
throw SystemError.setenv(Int32(GetLastError()), key)
126+
try key.withCString(encodedAs: UTF16.self) { pwszKey in
127+
try value.withCString(encodedAs: UTF16.self) { pwszValue in
128+
guard SetEnvironmentVariableW(pwszKey, pwszValue) else {
129+
throw SystemError.setenv(Int32(GetLastError()), key)
130+
}
131+
}
90132
}
91133
#else
92134
guard TSCLibc.setenv(key, value, 1) == 0 else {
@@ -99,7 +141,9 @@ public enum ProcessEnv {
99141
/// Unset the give key in the process's environment.
100142
public static func unsetVar(_ key: String) throws {
101143
#if os(Windows)
102-
guard TSCLibc._putenv("\(key)=") == 0 else {
144+
guard key.withCString(encodedAs: UTF16.self, {
145+
SetEnvironmentVariableW($0, nil)
146+
}) else {
103147
throw SystemError.unsetenv(Int32(GetLastError()), key)
104148
}
105149
#else
@@ -124,9 +168,7 @@ public enum ProcessEnv {
124168
public static func chdir(_ path: AbsolutePath) throws {
125169
let path = path.pathString
126170
#if os(Windows)
127-
guard path.withCString(encodedAs: UTF16.self, {
128-
SetCurrentDirectoryW($0)
129-
}) else {
171+
guard path.withCString(encodedAs: UTF16.self, SetCurrentDirectoryW) else {
130172
throw SystemError.chdir(Int32(GetLastError()), path)
131173
}
132174
#else

Tests/TSCBasicTests/ProcessEnvTests.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import XCTest
1212

1313
import TSCBasic
1414
import TSCTestSupport
15+
#if os(Windows)
16+
import WinSDK
17+
#endif
1518

1619
class ProcessEnvTests: XCTestCase {
1720

@@ -55,4 +58,42 @@ class ProcessEnvTests: XCTestCase {
5558
}
5659
XCTAssertNil(ProcessEnv.vars[key])
5760
}
61+
62+
func testWin32API() throws {
63+
#if os(Windows)
64+
let variable: String = "SWIFT_TOOLS_SUPPORT_CORE_VARIABLE"
65+
let value: String = "1"
66+
67+
try variable.withCString(encodedAs: UTF16.self) { pwszVariable in
68+
try value.withCString(encodedAs: UTF16.self) { pwszValue in
69+
guard SetEnvironmentVariableW(pwszVariable, pwszValue) else {
70+
throw XCTSkip("Failed to set environment variable")
71+
}
72+
}
73+
}
74+
75+
// Ensure that libc does not see the variable.
76+
XCTAssertNil(getenv(variable))
77+
variable.withCString(encodedAs: UTF16.self) { pwszVariable in
78+
XCTAssertNil(_wgetenv(pwszVariable))
79+
}
80+
81+
// Ensure that we can read the variable
82+
ProcessEnv.invalidateEnv()
83+
XCTAssertEqual(ProcessEnv.vars[variable], value)
84+
85+
// Ensure that we can read the variable using the Win32 API.
86+
variable.withCString(encodedAs: UTF16.self) { pwszVariable in
87+
let dwLength = GetEnvironmentVariableW(pwszVariable, nil, 0)
88+
withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength + 1)) {
89+
let dwLength = GetEnvironmentVariableW(pwszVariable, $0.baseAddress, dwLength + 1)
90+
XCTAssertEqual(dwLength, 1)
91+
XCTAssertEqual(String(decodingCString: $0.baseAddress!, as: UTF16.self), value)
92+
}
93+
}
94+
#else
95+
throw XCTSkip("Win32 API is only available on Windows")
96+
#endif
97+
98+
}
5899
}

0 commit comments

Comments
 (0)