Skip to content

Commit fb5d22a

Browse files
rauhulMaxDesiatov
authored andcommitted
Fix environment related warnings (#7684)
Adds a new `Environment` type to replace TSCBasics EnvironmentVariables and ProcessEnvironmentBlock types. Updates points of use of the older APIs with the newer API and adds unit tests for `Environment`. (cherry picked from commit 74aea87)
1 parent 25793b3 commit fb5d22a

File tree

85 files changed

+1150
-440
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

85 files changed

+1150
-440
lines changed

CONTRIBUTORS.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ needs to be listed here.
291291
- Quinn McHenry <[email protected]>
292292
- Rahul Malik <[email protected]>
293293
- Randy Becker <[email protected]>
294-
- Rauhul Varma <rauhul@users.noreply.github.com>
294+
- Rauhul Varma <rauhul@apple.com>
295295
- Renzo Crisóstomo <[email protected]>
296296
- Rich Ellis <[email protected]>
297297
- Rick Ballard <[email protected]>

Sources/Basics/Archiver/TarArchiver.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ public struct TarArchiver: Archiver {
9393

9494
let process = TSCBasic.Process(
9595
arguments: [self.tarCommand, "acf", destinationPath.pathString, directory.basename],
96+
environment: .current,
9697
workingDirectory: directory.parentDirectory.underlying
9798
)
9899

Sources/Basics/CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ add_library(Basics
2525
Concurrency/ThreadSafeKeyValueStore.swift
2626
Concurrency/TokenBucket.swift
2727
DispatchTimeInterval+Extensions.swift
28-
EnvironmentVariables.swift
28+
Environment/Environment.swift
29+
Environment/EnvironmentKey.swift
30+
Environment/EnvironmentShims.swift
2931
Errors.swift
3032
FileSystem/AbsolutePath.swift
3133
FileSystem/FileSystem+Extensions.swift

Sources/Basics/Concurrency/ConcurrencyHelpers.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,11 @@ import Dispatch
1515
import class Foundation.NSLock
1616
import class Foundation.ProcessInfo
1717
import struct Foundation.URL
18-
import enum TSCBasic.ProcessEnv
1918
import func TSCBasic.tsc_await
2019

2120
public enum Concurrency {
2221
public static var maxOperations: Int {
23-
ProcessEnv.block["SWIFTPM_MAX_CONCURRENT_OPERATIONS"].flatMap(Int.init) ?? ProcessInfo.processInfo
22+
Environment.current["SWIFTPM_MAX_CONCURRENT_OPERATIONS"].flatMap(Int.init) ?? ProcessInfo.processInfo
2423
.activeProcessorCount
2524
}
2625
}
Lines changed: 357 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
15+
#if canImport(Glibc)
16+
import Glibc
17+
#elseif canImport(Musl)
18+
import Musl
19+
#elseif os(Windows)
20+
import CRT
21+
import WinSDK
22+
#else
23+
import Darwin.C
24+
#endif
25+
26+
// FIXME: Use Synchronization.Mutex when available
27+
class Mutex<T>: @unchecked Sendable {
28+
var lock: NSLock
29+
var value: T
30+
31+
init(value: T) {
32+
self.lock = .init()
33+
self.value = value
34+
}
35+
36+
func withLock<U>(_ body: (inout T) -> U) -> U {
37+
self.lock.lock()
38+
defer { self.lock.unlock() }
39+
return body(&self.value)
40+
}
41+
}
42+
43+
// FIXME: This should come from Foundation
44+
// FIXME: package (public required by users)
45+
public struct Environment {
46+
var storage: [EnvironmentKey: String]
47+
}
48+
49+
// MARK: - Accessors
50+
51+
extension Environment {
52+
package init() {
53+
self.storage = .init()
54+
}
55+
56+
package subscript(_ key: EnvironmentKey) -> String? {
57+
_read { yield self.storage[key] }
58+
_modify { yield &self.storage[key] }
59+
}
60+
}
61+
62+
// MARK: - Conversions between Dictionary<String, String>
63+
64+
extension Environment {
65+
package init(_ dictionary: [String: String]) {
66+
self.storage = .init()
67+
let sorted = dictionary.sorted { $0.key < $1.key }
68+
for (key, value) in sorted {
69+
self.storage[.init(key)] = value
70+
}
71+
}
72+
}
73+
74+
extension [String: String] {
75+
package init(_ environment: Environment) {
76+
self.init()
77+
let sorted = environment.sorted { $0.key < $1.key }
78+
for (key, value) in sorted {
79+
self[key.rawValue] = value
80+
}
81+
}
82+
}
83+
84+
// MARK: - Path Modification
85+
86+
extension Environment {
87+
package mutating func prependPath(key: EnvironmentKey, value: String) {
88+
guard !value.isEmpty else { return }
89+
if let existing = self[key] {
90+
self[key] = "\(value)\(Self.pathEntryDelimiter)\(existing)"
91+
} else {
92+
self[key] = value
93+
}
94+
}
95+
96+
package mutating func appendPath(key: EnvironmentKey, value: String) {
97+
guard !value.isEmpty else { return }
98+
if let existing = self[key] {
99+
self[key] = "\(existing)\(Self.pathEntryDelimiter)\(value)"
100+
} else {
101+
self[key] = value
102+
}
103+
}
104+
105+
package static var pathEntryDelimiter: String {
106+
#if os(Windows)
107+
";"
108+
#else
109+
":"
110+
#endif
111+
}
112+
}
113+
114+
// MARK: - Global Environment
115+
116+
extension Environment {
117+
static let _cachedCurrent = Mutex<Self?>(value: nil)
118+
119+
/// Vends a copy of the current process's environment variables.
120+
///
121+
/// Mutations to the current process's global environment are not reflected
122+
/// in the returned value.
123+
public static var current: Self {
124+
Self._cachedCurrent.withLock { cachedValue in
125+
if let cachedValue = cachedValue {
126+
return cachedValue
127+
} else {
128+
let current = Self(ProcessInfo.processInfo.environment)
129+
cachedValue = current
130+
return current
131+
}
132+
}
133+
}
134+
135+
/// Temporary override environment variables
136+
///
137+
/// WARNING! This method is not thread-safe. POSIX environments are shared
138+
/// between threads. This means that when this method is called simultaneously
139+
/// from different threads, the environment will neither be setup nor restored
140+
/// correctly.
141+
package static func makeCustom<T>(
142+
_ environment: Self,
143+
body: () async throws -> T
144+
) async throws -> T {
145+
let current = Self.current
146+
let state = environment.storage.keys.map { ($0, current[$0]) }
147+
let restore = {
148+
for (key, value) in state {
149+
try Self.set(key: key, value: value)
150+
}
151+
}
152+
let returnValue: T
153+
do {
154+
for (key, value) in environment {
155+
try Self.set(key: key, value: value)
156+
}
157+
returnValue = try await body()
158+
} catch {
159+
try? restore()
160+
throw error
161+
}
162+
try restore()
163+
return returnValue
164+
}
165+
166+
/// Temporary override environment variables
167+
///
168+
/// WARNING! This method is not thread-safe. POSIX environments are shared
169+
/// between threads. This means that when this method is called simultaneously
170+
/// from different threads, the environment will neither be setup nor restored
171+
/// correctly.
172+
package static func makeCustom<T>(
173+
_ environment: Self,
174+
body: () throws -> T
175+
) throws -> T {
176+
let current = Self.current
177+
let state = environment.storage.keys.map { ($0, current[$0]) }
178+
let restore = {
179+
for (key, value) in state {
180+
try Self.set(key: key, value: value)
181+
}
182+
}
183+
let returnValue: T
184+
do {
185+
for (key, value) in environment {
186+
try Self.set(key: key, value: value)
187+
}
188+
returnValue = try body()
189+
} catch {
190+
try? restore()
191+
throw error
192+
}
193+
try restore()
194+
return returnValue
195+
}
196+
197+
struct UpdateEnvironmentError: CustomStringConvertible, Error {
198+
var function: StaticString
199+
var code: Int32
200+
var description: String { "\(self.function) returned \(self.code)" }
201+
}
202+
203+
/// Modifies the process's global environment.
204+
///
205+
/// > Important: This operation is _not_ concurrency safe.
206+
package static func set(key: EnvironmentKey, value: String?) throws {
207+
#if os(Windows)
208+
func _SetEnvironmentVariableW(_ key: String, _ value: String?) -> Bool {
209+
key.withCString(encodedAs: UTF16.self) { key in
210+
if let value {
211+
value.withCString(encodedAs: UTF16.self) { value in
212+
SetEnvironmentVariableW(key, value)
213+
}
214+
} else {
215+
SetEnvironmentVariableW(key, nil)
216+
}
217+
}
218+
}
219+
#endif
220+
221+
// Invalidate cached value after mutating the global environment.
222+
// This is potentially overly safe because we may not need to invalidate
223+
// the cache if the mutation fails. However this approach is easier to
224+
// read and reason about.
225+
defer { Self._cachedCurrent.withLock { $0 = nil } }
226+
if let value = value {
227+
#if os(Windows)
228+
guard _SetEnvironmentVariableW(key.rawValue, value) else {
229+
throw UpdateEnvironmentError(
230+
function: "SetEnvironmentVariableW",
231+
code: Int32(GetLastError())
232+
)
233+
}
234+
guard _putenv("\(key)=\(value)") == 0 else {
235+
throw UpdateEnvironmentError(
236+
function: "_putenv",
237+
code: Int32(GetLastError())
238+
)
239+
}
240+
#else
241+
guard setenv(key.rawValue, value, 1) == 0 else {
242+
throw UpdateEnvironmentError(
243+
function: "setenv",
244+
code: errno
245+
)
246+
}
247+
#endif
248+
} else {
249+
#if os(Windows)
250+
guard _SetEnvironmentVariableW(key.rawValue, nil) else {
251+
throw UpdateEnvironmentError(
252+
function: "SetEnvironmentVariableW",
253+
code: Int32(GetLastError())
254+
)
255+
}
256+
guard _putenv("\(key)=") == 0 else {
257+
throw UpdateEnvironmentError(
258+
function: "_putenv",
259+
code: Int32(GetLastError())
260+
)
261+
}
262+
#else
263+
guard unsetenv(key.rawValue) == 0 else {
264+
throw UpdateEnvironmentError(
265+
function: "unsetenv",
266+
code: errno
267+
)
268+
}
269+
#endif
270+
}
271+
}
272+
}
273+
274+
// MARK: - Cachable Keys
275+
276+
extension Environment {
277+
/// Returns a copy of `self` with known non-cacheable keys removed.
278+
///
279+
/// - Issue: rdar://107029374
280+
package var cachable: Environment {
281+
var cachable = Environment()
282+
for (key, value) in self {
283+
if !EnvironmentKey.nonCachable.contains(key) {
284+
cachable[key] = value
285+
}
286+
}
287+
return cachable
288+
}
289+
}
290+
291+
// MARK: - Protocol Conformances
292+
293+
extension Environment: Collection {
294+
public struct Index: Comparable {
295+
public static func < (lhs: Self, rhs: Self) -> Bool {
296+
lhs.underlying < rhs.underlying
297+
}
298+
299+
var underlying: Dictionary<EnvironmentKey, String>.Index
300+
}
301+
302+
public typealias Element = (key: EnvironmentKey, value: String)
303+
304+
public var startIndex: Index {
305+
Index(underlying: self.storage.startIndex)
306+
}
307+
308+
public var endIndex: Index {
309+
Index(underlying: self.storage.endIndex)
310+
}
311+
312+
public subscript(index: Index) -> Element {
313+
self.storage[index.underlying]
314+
}
315+
316+
public func index(after index: Self.Index) -> Self.Index {
317+
Index(underlying: self.storage.index(after: index.underlying))
318+
}
319+
}
320+
321+
extension Environment: CustomStringConvertible {
322+
public var description: String {
323+
let body = self
324+
.sorted { $0.key < $1.key }
325+
.map { "\"\($0.rawValue)=\($1)\"" }
326+
.joined(separator: ", ")
327+
return "[\(body)]"
328+
}
329+
}
330+
331+
extension Environment: Encodable {
332+
public func encode(to encoder: any Encoder) throws {
333+
try self.storage.encode(to: encoder)
334+
}
335+
}
336+
337+
extension Environment: Equatable {}
338+
339+
extension Environment: ExpressibleByDictionaryLiteral {
340+
public typealias Key = EnvironmentKey
341+
public typealias Value = String
342+
343+
public init(dictionaryLiteral elements: (Key, Value)...) {
344+
self.storage = .init()
345+
for (key, value) in elements {
346+
self.storage[key] = value
347+
}
348+
}
349+
}
350+
351+
extension Environment: Decodable {
352+
public init(from decoder: any Decoder) throws {
353+
self.storage = try .init(from: decoder)
354+
}
355+
}
356+
357+
extension Environment: Sendable {}

0 commit comments

Comments
 (0)