Skip to content

Commit 071939b

Browse files
committed
Start adopting the new Environment type throughout the codebase
Mostly starting at the "edges", in tests and in some central APIs. Does nothing for Unix-like platforms, but will fix some case sensitivity issues on Windows when accessing specific environment variables. Closes #296
1 parent 7f75d72 commit 071939b

31 files changed

+124
-84
lines changed

Sources/SWBBuildService/Messages.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ extension SetSessionWorkspaceContainerPathRequest: PIFProvidingRequest {
184184
try fs.createDirectory(dir, recursive: true)
185185
let pifPath = dir.join(Foundation.UUID().description + ".json")
186186
let argument = isProject ? "-project" : "-workspace"
187-
let result = try await Process.getOutput(url: URL(fileURLWithPath: "/usr/bin/xcrun"), arguments: ["xcodebuild", "-dumpPIF", pifPath.str, argument, path.str], currentDirectoryURL: URL(fileURLWithPath: containerPath.dirname.str, isDirectory: true), environment: ProcessInfo.processInfo.cleanEnvironment.merging(["DEVELOPER_DIR": session.core.developerPath.str], uniquingKeysWith: { _, new in new }))
187+
let result = try await Process.getOutput(url: URL(fileURLWithPath: "/usr/bin/xcrun"), arguments: ["xcodebuild", "-dumpPIF", pifPath.str, argument, path.str], currentDirectoryURL: URL(fileURLWithPath: containerPath.dirname.str, isDirectory: true), environment: Environment.current.addingContents(of: [.developerDir: session.core.developerPath.str]))
188188
if !result.exitStatus.isSuccess {
189189
throw StubError.error("Could not dump PIF for '\(path.str)': \(String(decoding: result.stderr, as: UTF8.self))")
190190
}

Sources/SWBBuildSystem/CleanOperation.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -214,10 +214,10 @@ package final class CleanOperation: BuildSystemOperation, TargetDependencyResolv
214214
let taskIdentifier = task.identifier
215215
let taskOutputDelegate = delegate.taskStarted(self, taskIdentifier: taskIdentifier, task: task, dependencyInfo: nil)
216216

217-
let resolvedExecutable = StackedSearchPath(environment: environment, fs: workspaceContext.fs).lookup(Path(executable)) ?? Path(executable)
217+
let resolvedExecutable = StackedSearchPath(environment: .init(environment), fs: workspaceContext.fs).lookup(Path(executable)) ?? Path(executable)
218218

219219
do {
220-
let result = try await Process.getMergedOutput(url: URL(fileURLWithPath: resolvedExecutable.str), arguments: arguments, currentDirectoryURL: URL(fileURLWithPath: workingDirectory.str), environment: environment)
220+
let result = try await Process.getMergedOutput(url: URL(fileURLWithPath: resolvedExecutable.str), arguments: arguments, currentDirectoryURL: URL(fileURLWithPath: workingDirectory.str), environment: .init(environment))
221221

222222
if !result.exitStatus.isSuccess {
223223
taskOutputDelegate.emitError("Failed to clean target '\(configuredTarget.target.name)': \(String(decoding: result.output, as: UTF8.self))")

Sources/SWBCore/SWBFeatureFlag.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,15 @@ public struct SWBFeatureFlagProperty {
3535

3636
/// Whether the feature flag is actually set at all.
3737
public var hasValue: Bool {
38-
return SWBUtil.UserDefaults.hasValue(forKey: key) || getEnvironmentVariable(key) != nil
38+
return SWBUtil.UserDefaults.hasValue(forKey: key) || getEnvironmentVariable(EnvironmentKey(key)) != nil
3939
}
4040

4141
/// Indicates whether the feature flag is currently active in the calling environment.
4242
public var value: Bool {
4343
if !hasValue {
4444
return defaultValue
4545
}
46-
return SWBUtil.UserDefaults.bool(forKey: key) || getEnvironmentVariable(key)?.boolValue == true
46+
return SWBUtil.UserDefaults.bool(forKey: key) || getEnvironmentVariable(EnvironmentKey(key))?.boolValue == true
4747
}
4848

4949
fileprivate init(_ key: String, defaultValue: Bool = false) {
@@ -59,7 +59,7 @@ public struct SWBOptionalFeatureFlagProperty {
5959
/// Returns nil if neither environment variable nor User Default are set. An implementation can then pick a default behavior.
6060
/// If both the environment variable and User Default are set, the two values are logically AND'd together; this allows the set false value of either to force the feature flag off.
6161
public var value: Bool? {
62-
let envValue = getEnvironmentVariable(key)
62+
let envValue = getEnvironmentVariable(EnvironmentKey(key))
6363
let envHasValue = envValue != nil
6464
let defHasValue = SWBUtil.UserDefaults.hasValue(forKey: key)
6565
if !envHasValue && !defHasValue {

Sources/SWBCore/Settings/StackedSearchPaths.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ public final class StackedSearchPath: Sendable {
3333
self.fs = fs
3434
}
3535

36-
public init(environment: [String: String], fs: any FSProxy) {
37-
self.paths = environment["PATH"]?.split(separator: Path.pathEnvironmentSeparator).map(Path.init) ?? []
36+
public init(environment: Environment, fs: any FSProxy) {
37+
self.paths = environment[.path]?.split(separator: Path.pathEnvironmentSeparator).map(Path.init) ?? []
3838
self.fs = fs
3939
}
4040

Sources/SWBCore/SpecImplementations/CommandLineToolSpec.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -1390,7 +1390,7 @@ open class CommandLineToolSpec : PropertyDomainSpec, SpecType, TaskTypeDescripti
13901390
public func executeExternalTool<T>(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate, commandLine: [String], workingDirectory: String?, environment: [String: String], executionDescription: String?, _ parse: @escaping (ByteString) throws -> T) async throws -> T {
13911391
let executionResult = try await delegate.executeExternalTool(commandLine: commandLine, workingDirectory: workingDirectory, environment: environment, executionDescription: executionDescription)
13921392
guard executionResult.exitStatus.isSuccess else {
1393-
throw RunProcessNonZeroExitError(args: commandLine, workingDirectory: workingDirectory, environment: environment, status: executionResult.exitStatus, stdout: ByteString(executionResult.stdout), stderr: ByteString(executionResult.stderr))
1393+
throw RunProcessNonZeroExitError(args: commandLine, workingDirectory: workingDirectory, environment: .init(environment), status: executionResult.exitStatus, stdout: ByteString(executionResult.stdout), stderr: ByteString(executionResult.stderr))
13941394
}
13951395
return try parse(ByteString(executionResult.stdout))
13961396
}

Sources/SWBCore/TaskGeneration.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -781,7 +781,7 @@ extension CoreClientDelegate {
781781
}
782782

783783
return try await externalToolExecutionQueue.withOperation {
784-
try await Process.getOutput(url: url, arguments: Array(commandLine.dropFirst()), currentDirectoryURL: workingDirectory.map(URL.init(fileURLWithPath:)), environment: ProcessInfo.processInfo.cleanEnvironment.merging(environment, uniquingKeysWith: { _, new in new }))
784+
try await Process.getOutput(url: url, arguments: Array(commandLine.dropFirst()), currentDirectoryURL: workingDirectory.map(URL.init(fileURLWithPath:)), environment: Environment.current.addingContents(of: .init(environment)))
785785
}
786786
case let .result(status, stdout, stderr):
787787
return Processes.ExecutionResult(exitStatus: status, stdout: stdout, stderr: stderr)

Sources/SWBCore/ToolchainRegistry.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,7 @@ public final class ToolchainRegistry: @unchecked Sendable {
472472
return
473473
}
474474

475-
if let swift = StackedSearchPath(environment: ProcessInfo.processInfo.cleanEnvironment, fs: fs).lookup(Path("swift")), fs.exists(swift) {
475+
if let swift = StackedSearchPath(environment: .current, fs: fs).lookup(Path("swift")), fs.exists(swift) {
476476
let hasUsrBin = swift.normalize().str.hasSuffix("/usr/bin/swift")
477477
let hasUsrLocalBin = swift.normalize().str.hasSuffix("/usr/local/bin/swift")
478478
let path: Path

Sources/SWBQNXPlatform/Plugin.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ struct QNXEnvironmentExtension: EnvironmentExtension {
4646

4747
func additionalEnvironmentVariables(context: any EnvironmentExtensionAdditionalEnvironmentVariablesContext) async throws -> [String : String] {
4848
if let latest = try await plugin.cachedQNXSDPInstallations(host: context.hostOperatingSystem).first {
49-
return latest.environment
49+
return .init(latest.environment)
5050
}
5151
return [:]
5252
}

Sources/SWBQNXPlatform/QNXSDP.swift

+6-4
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@ struct QNXSDP: Sendable {
2828
self.sysroot = sysroot
2929
self.configurationPath = configurationPath
3030

31-
let environment = [
31+
var environment: Environment = [
3232
"QNX_TARGET": sysroot.str,
33-
"QNX_HOST": hostPath?.str,
3433
"QNX_CONFIGURATION_EXCLUSIVE": configurationPath.str,
35-
].compactMapValues { $0 }
34+
]
35+
if let hostPath {
36+
environment["QNX_HOST"] = hostPath.str
37+
}
3638
self.environment = environment
3739

3840
self.version = try await {
@@ -60,7 +62,7 @@ struct QNXSDP: Sendable {
6062
/// Equivalent to `QNX_HOST`.
6163
public let hostPath: Path?
6264

63-
public let environment: [String: String]
65+
public let environment: Environment
6466

6567
private static func hostPath(host: OperatingSystem, path: Path) -> Path? {
6668
switch host {

Sources/SWBTaskExecution/TaskActions/EmbedSwiftStdLibTaskAction.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,7 @@ public final class EmbedSwiftStdLibTaskAction: TaskAction {
525525
}
526526

527527
guard !failed else {
528-
throw RunProcessNonZeroExitError(args: args, workingDirectory: task.workingDirectory.str, environment: effectiveEnvironment, status: {
528+
throw RunProcessNonZeroExitError(args: args, workingDirectory: task.workingDirectory.str, environment: .init(effectiveEnvironment), status: {
529529
if case let .exit(exitStatus, _) = processDelegate.outputDelegate.result {
530530
return exitStatus
531531
}

Sources/SWBTestSupport/BuildOperationTester.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -1918,7 +1918,7 @@ private final class BuildOperationTesterDelegate: BuildOperationDelegate {
19181918
if !self.hadErrors {
19191919
switch result {
19201920
case let .exit(exitStatus, _) where !exitStatus.isSuccess && !exitStatus.wasCanceled:
1921-
self.delegate.events.append(.buildHadDiagnostic(Diagnostic(behavior: .error, location: .unknown, data: DiagnosticData("Command \(task.ruleInfo[0]) failed. \(RunProcessNonZeroExitError(args: Array(task.commandLineAsStrings), workingDirectory: task.workingDirectory.str, environment: task.environment.bindingsDictionary, status: exitStatus, mergedOutput: output).description)"))))
1921+
self.delegate.events.append(.buildHadDiagnostic(Diagnostic(behavior: .error, location: .unknown, data: DiagnosticData("Command \(task.ruleInfo[0]) failed. \(RunProcessNonZeroExitError(args: Array(task.commandLineAsStrings), workingDirectory: task.workingDirectory.str, environment: .init(task.environment.bindingsDictionary), status: exitStatus, mergedOutput: output).description)"))))
19221922
case .failedSetup:
19231923
self.delegate.events.append(.buildHadDiagnostic(Diagnostic(behavior: .error, location: .unknown, data: DiagnosticData("Command \(task.ruleInfo[0]) failed setup."))))
19241924
case .exit, .skipped:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2025 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+
public import SWBUtil
14+
15+
extension EnvironmentKey {
16+
public static let externalToolchainsDir = Self("EXTERNAL_TOOLCHAINS_DIR")
17+
}

Sources/SWBTestSupport/Misc.swift

+5-6
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ package extension Sequence where Element: Equatable {
3939
/// - throws: ``StubError`` if the arguments list is an empty array.
4040
/// - throws: ``RunProcessNonZeroExitError`` if the process exited with a nonzero status code or uncaught signal.
4141
@discardableResult
42-
package func runProcess(_ args: [String], workingDirectory: String? = nil, environment: [String: String] = [:], interruptible: Bool = true, redirectStderr: Bool = false) async throws -> String {
42+
package func runProcess(_ args: [String], workingDirectory: String? = nil, environment: Environment = .init(), interruptible: Bool = true, redirectStderr: Bool = false) async throws -> String {
4343
guard let first = args.first else {
4444
throw StubError.error("Invalid number of arguments")
4545
}
@@ -66,19 +66,18 @@ package func runProcess(_ args: [String], workingDirectory: String? = nil, envir
6666
///
6767
/// This method will use the current value of `DEVELOPER_DIR` in the environment by default, or the value of `overrideDeveloperDirectory` if specified.
6868
package func runProcessWithDeveloperDirectory(_ args: [String], workingDirectory: String? = nil, overrideDeveloperDirectory: String? = nil, interruptible: Bool = true, redirectStderr: Bool = true) async throws -> String {
69-
let environment = ProcessInfo.processInfo.environment
69+
let environment = Environment.current
7070
.filter(keys: ["DEVELOPER_DIR", "LLVM_PROFILE_FILE"])
71-
.addingContents(of: overrideDeveloperDirectory.map { ["DEVELOPER_DIR": $0] } ?? [:])
71+
.addingContents(of: overrideDeveloperDirectory.map { Environment(["DEVELOPER_DIR": $0]) } ?? .init())
7272
return try await runProcess(args, workingDirectory: workingDirectory, environment: environment, interruptible: interruptible, redirectStderr: redirectStderr)
7373
}
7474

7575
package func runHostProcess(_ args: [String], workingDirectory: String? = nil, interruptible: Bool = true, redirectStderr: Bool = true) async throws -> String {
76-
let processInfo = ProcessInfo.processInfo
77-
switch try processInfo.hostOperatingSystem() {
76+
switch try ProcessInfo.processInfo.hostOperatingSystem() {
7877
case .macOS:
7978
return try await InstalledXcode.currentlySelected().xcrun(args, workingDirectory: workingDirectory, redirectStderr: redirectStderr)
8079
default:
81-
return try await runProcess(args, workingDirectory: workingDirectory, environment: processInfo.environment, interruptible: interruptible, redirectStderr: redirectStderr)
80+
return try await runProcess(args, workingDirectory: workingDirectory, environment: .current, interruptible: interruptible, redirectStderr: redirectStderr)
8281
}
8382
}
8483

Sources/SWBTestSupport/SkippedTestSupport.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -215,13 +215,13 @@ extension Trait where Self == Testing.ConditionTrait {
215215
}
216216
}
217217

218-
package static func skipIfEnvironment(key: String, value: String) -> Self {
218+
package static func skipIfEnvironment(key: EnvironmentKey, value: String) -> Self {
219219
disabled("environment sets '\(key)' to '\(value)'") {
220220
getEnvironmentVariable(key) == value
221221
}
222222
}
223223

224-
package static func skipIfEnvironmentVariableSet(key: String) -> Self {
224+
package static func skipIfEnvironmentVariableSet(key: EnvironmentKey) -> Self {
225225
disabled("environment sets '\(key)'") {
226226
getEnvironmentVariable(key) != nil
227227
}

Sources/SWBUtil/Environment.swift

+32-14
Original file line numberDiff line numberDiff line change
@@ -76,22 +76,9 @@ extension Environment {
7676
// MARK: - Global Environment
7777

7878
extension Environment {
79-
fileprivate static let _cachedCurrent = SWBMutex<Self?>(nil)
80-
8179
/// Vends a copy of the current process's environment variables.
82-
///
83-
/// Mutations to the current process's global environment are not reflected
84-
/// in the returned value.
8580
public static var current: Self {
86-
Self._cachedCurrent.withLock { cachedValue in
87-
if let cachedValue = cachedValue {
88-
return cachedValue
89-
} else {
90-
let current = Self(ProcessInfo.processInfo.environment)
91-
cachedValue = current
92-
return current
93-
}
94-
}
81+
Self(ProcessInfo.processInfo.cleanEnvironment)
9582
}
9683
}
9784

@@ -162,3 +149,34 @@ extension Environment: Decodable {
162149
}
163150

164151
extension Environment: Sendable {}
152+
153+
extension Environment {
154+
public func filter(_ isIncluded: @escaping (Dictionary<EnvironmentKey, String>.Element) throws -> Bool) rethrows -> Environment {
155+
try Environment(storage: storage.filter(isIncluded))
156+
}
157+
158+
public func filter<KeyCollection: Collection>(keys: KeyCollection) -> Environment where KeyCollection.Element == EnvironmentKey {
159+
return filter { key, _ in keys.contains(key) }
160+
}
161+
162+
public mutating func addContents(of other: Environment) {
163+
storage.addContents(of: other.storage)
164+
}
165+
166+
public func addingContents(of other: Environment) -> Environment {
167+
var env = self
168+
env.addContents(of: other)
169+
return env
170+
}
171+
172+
public mutating func addContents(of other: [EnvironmentKey: String]) {
173+
storage.addContents(of: other)
174+
}
175+
176+
public func addingContents(of other: [EnvironmentKey: String]) -> Environment {
177+
var env = self
178+
env.addContents(of: other)
179+
return env
180+
}
181+
182+
}

Sources/SWBUtil/EnvironmentHelpers.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,25 @@
1212

1313
import Foundation
1414

15-
@TaskLocal fileprivate var processEnvironment = ProcessInfo.processInfo.environment
15+
@TaskLocal fileprivate var processEnvironment = Environment.current
1616

1717
/// Binds the internal defaults to the specified `environment` for the duration of the synchronous `operation`.
1818
/// - parameter clean: `true` to start with a clean environment, `false` to merge the input environment over the existing process environment.
1919
/// - note: This is implemented via task-local values.
20-
@_spi(Testing) public func withEnvironment<R>(_ environment: [String: String], clean: Bool = false, operation: () throws -> R) rethrows -> R {
20+
@_spi(Testing) public func withEnvironment<R>(_ environment: Environment, clean: Bool = false, operation: () throws -> R) rethrows -> R {
2121
try $processEnvironment.withValue(clean ? environment : processEnvironment.addingContents(of: environment), operation: operation)
2222
}
2323

2424
/// Binds the internal defaults to the specified `environment` for the duration of the asynchronous `operation`.
2525
/// - parameter clean: `true` to start with a clean environment, `false` to merge the input environment over the existing process environment.
2626
/// - note: This is implemented via task-local values.
27-
@_spi(Testing) public func withEnvironment<R>(_ environment: [String: String], clean: Bool = false, operation: () async throws -> R) async rethrows -> R {
27+
@_spi(Testing) public func withEnvironment<R>(_ environment: Environment, clean: Bool = false, operation: () async throws -> R) async rethrows -> R {
2828
try await $processEnvironment.withValue(clean ? environment : processEnvironment.addingContents(of: environment), operation: operation)
2929
}
3030

3131
/// Gets the value of the named variable from the process' environment.
3232
/// - parameter name: The name of the environment variable.
3333
/// - returns: The value of the variable as a `String`, or `nil` if it is not defined in the environment.
34-
public func getEnvironmentVariable(_ name: String) -> String? {
34+
public func getEnvironmentVariable(_ name: EnvironmentKey) -> String? {
3535
processEnvironment[name]
3636
}

Sources/SWBUtil/EnvironmentKey.swift

+4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ extension EnvironmentKey {
2525
package static let path: Self = "PATH"
2626
}
2727

28+
extension EnvironmentKey {
29+
package static let developerDir: Self = "DEVELOPER_DIR"
30+
}
31+
2832
extension EnvironmentKey: CodingKeyRepresentable {}
2933

3034
extension EnvironmentKey: Comparable {

Sources/SWBUtil/PbxCp.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ fileprivate func xSecCodePathIsSigned(_ path: Path) throws -> Bool {
8080

8181
// FIXME: Move this fully to Swift Concurrency and execute the process via llbuild after PbxCp is fully converted to Swift
8282
/// Spawns a process and waits for it to finish, closing stdin and redirecting stdout and stderr to fdout. Failure to launch, non-zero exit code, or exit with a signal will throw an error.
83-
fileprivate func spawnTaskAndWait(_ launchPath: Path, _ arguments: [String]?, _ environment: [String: String]?, _ workingDirPath: String?, _ dryRun: Bool, _ stream: OutputByteStream) async throws {
83+
fileprivate func spawnTaskAndWait(_ launchPath: Path, _ arguments: [String]?, _ environment: Environment?, _ workingDirPath: String?, _ dryRun: Bool, _ stream: OutputByteStream) async throws {
8484
stream <<< launchPath.str
8585
for arg in arguments ?? [] {
8686
stream <<< " \(arg)"
@@ -97,7 +97,7 @@ fileprivate func spawnTaskAndWait(_ launchPath: Path, _ arguments: [String]?, _
9797
stream <<< "\(String(decoding: output, as: UTF8.self))"
9898

9999
if !exitStatus.isSuccess {
100-
throw RunProcessNonZeroExitError(args: [launchPath.str] + (arguments ?? []), workingDirectory: workingDirPath, environment: environment ?? [:], status: exitStatus, mergedOutput: ByteString(output))
100+
throw RunProcessNonZeroExitError(args: [launchPath.str] + (arguments ?? []), workingDirectory: workingDirPath, environment: environment ?? .init(), status: exitStatus, mergedOutput: ByteString(output))
101101
}
102102
}
103103

0 commit comments

Comments
 (0)