Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 81 additions & 7 deletions Sources/Subprocess/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -401,8 +401,8 @@ extension Arguments: CustomStringConvertible, CustomDebugStringConvertible {
/// A set of environment variables to use when executing the subprocess.
public struct Environment: Sendable, Hashable {
internal enum Configuration: Sendable, Hashable {
case inherit([String: String])
case custom([String: String])
case inherit([Key: String])
case custom([Key: String])
#if !os(Windows)
case rawBytes([[UInt8]])
#endif
Expand All @@ -419,11 +419,11 @@ public struct Environment: Sendable, Hashable {
return .init(config: .inherit([:]))
}
/// Override the provided `newValue` in the existing `Environment`
public func updating(_ newValue: [String: String]) -> Self {
public func updating(_ newValue: [Key: String]) -> Self {
return .init(config: .inherit(newValue))
}
/// Use custom environment variables
public static func custom(_ newValue: [String: String]) -> Self {
public static func custom(_ newValue: [Key: String]) -> Self {
return .init(config: .custom(newValue))
}

Expand All @@ -436,6 +436,17 @@ public struct Environment: Sendable, Hashable {
}

extension Environment: CustomStringConvertible, CustomDebugStringConvertible {
/// A key used to access values in an ``Environment``.
///
/// This type respects the compiled platform's case sensitivity requirements.
public struct Key {
public var rawValue: String

package init(_ rawValue: String) {
self.rawValue = rawValue
}
}

/// A textual representation of the environment.
public var description: String {
switch self.config {
Expand Down Expand Up @@ -464,9 +475,9 @@ extension Environment: CustomStringConvertible, CustomDebugStringConvertible {
return self.description
}

internal static func currentEnvironmentValues() -> [String: String] {
internal static func currentEnvironmentValues() -> [Key: String] {
return self.withCopiedEnv { environments in
var results: [String: String] = [:]
var results: [Key: String] = [:]
for env in environments {
let environmentString = String(cString: env)

Expand All @@ -488,13 +499,76 @@ extension Environment: CustomStringConvertible, CustomDebugStringConvertible {
let value = String(
environmentString[environmentString.index(after: delimiter)..<environmentString.endIndex]
)
results[key] = value
results[Key(key)] = value
}
return results
}
}
}

extension Environment.Key {
package static let path: Self = "PATH"
}

extension Environment.Key: CodingKeyRepresentable {}

extension Environment.Key: Comparable {
public static func < (lhs: Self, rhs: Self) -> Bool {
// Even on windows use a stable sort order.
lhs.rawValue < rhs.rawValue
}
}

extension Environment.Key: CustomStringConvertible {
public var description: String { self.rawValue }
}

extension Environment.Key: Encodable {
public func encode(to encoder: any Swift.Encoder) throws {
try self.rawValue.encode(to: encoder)
}
}

extension Environment.Key: Equatable {
public static func == (_ lhs: Self, _ rhs: Self) -> Bool {
#if os(Windows)
lhs.rawValue.lowercased() == rhs.rawValue.lowercased()
#else
lhs.rawValue == rhs.rawValue
#endif
}
}

extension Environment.Key: ExpressibleByStringLiteral {
public init(stringLiteral rawValue: String) {
self.init(rawValue)
}
}

extension Environment.Key: Decodable {
public init(from decoder: any Swift.Decoder) throws {
self.rawValue = try String(from: decoder)
}
}

extension Environment.Key: Hashable {
public func hash(into hasher: inout Hasher) {
#if os(Windows)
self.rawValue.lowercased().hash(into: &hasher)
#else
self.rawValue.hash(into: &hasher)
#endif
}
}

extension Environment.Key: RawRepresentable {
public init?(rawValue: String) {
self.rawValue = rawValue
}
}

extension Environment.Key: Sendable {}

// MARK: - TerminationStatus

/// An exit status of a subprocess.
Expand Down
10 changes: 4 additions & 6 deletions Sources/Subprocess/Platforms/Subprocess+Unix.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,24 +145,22 @@ extension Execution {

// MARK: - Environment Resolution
extension Environment {
internal static let pathVariableName = "PATH"

internal func pathValue() -> String? {
switch self.config {
case .inherit(let overrides):
// If PATH value exists in overrides, use it
if let value = overrides[Self.pathVariableName] {
if let value = overrides[.path] {
return value
}
// Fall back to current process
return Self.currentEnvironmentValues()[Self.pathVariableName]
return Self.currentEnvironmentValues()[.path]
case .custom(let fullEnvironment):
if let value = fullEnvironment[Self.pathVariableName] {
if let value = fullEnvironment[.path] {
return value
}
return nil
case .rawBytes(let rawBytesArray):
let needle: [UInt8] = Array("\(Self.pathVariableName)=".utf8)
let needle: [UInt8] = Array("\(Key.path.rawValue)=".utf8)
for row in rawBytesArray {
guard row.starts(with: needle) else {
continue
Expand Down
23 changes: 8 additions & 15 deletions Sources/Subprocess/Platforms/Subprocess+Windows.swift
Original file line number Diff line number Diff line change
Expand Up @@ -926,13 +926,13 @@ extension Environment {
switch self.config {
case .inherit(let overrides):
// If PATH value exists in overrides, use it
if let value = overrides.pathValue() {
if let value = overrides[.path] {
return value
}
// Fall back to current process
return Self.currentEnvironmentValues().pathValue()
return Self.currentEnvironmentValues()[.path]
case .custom(let fullEnvironment):
if let value = fullEnvironment.pathValue() {
if let value = fullEnvironment[.path] {
return value
}
return nil
Expand Down Expand Up @@ -1006,7 +1006,7 @@ extension Configuration {
intendedWorkingDir: String?
) {
// Prepare environment
var env: [String: String] = [:]
var env: [Environment.Key: String] = [:]
switch self.environment.config {
case .custom(let customValues):
// Use the custom values directly
Expand All @@ -1020,17 +1020,17 @@ extension Configuration {
}
// On Windows, the PATH is required in order to locate dlls needed by
// the process so we should also pass that to the child
if env.pathValue() == nil,
let parentPath = Environment.currentEnvironmentValues().pathValue()
if env[.path] == nil,
let parentPath = Environment.currentEnvironmentValues()[.path]
{
env["Path"] = parentPath
env[.path] = parentPath
}
// The environment string must be terminated by a double
// null-terminator. Otherwise, CreateProcess will fail with
// INVALID_PARMETER.
let environmentString =
env.map {
$0.key + "=" + $0.value
$0.key.rawValue + "=" + $0.value
}.joined(separator: "\0") + "\0\0"

// Prepare arguments
Expand Down Expand Up @@ -1509,11 +1509,4 @@ internal func fillNullTerminatedWideStringBuffer(
throw SubprocessError.UnderlyingError(rawValue: DWORD(ERROR_INSUFFICIENT_BUFFER))
}

// Windows environment key is case insensitive
extension Dictionary where Key == String, Value == String {
internal func pathValue() -> String? {
return self["Path"] ?? self["PATH"] ?? self["path"]
}
}

#endif // canImport(WinSDK)