Skip to content

Commit bbd9ecb

Browse files
committed
Make Configuration introspectable
1 parent 745d4a7 commit bbd9ecb

4 files changed

Lines changed: 337 additions & 15 deletions

File tree

Sources/Subprocess/Configuration.swift

Lines changed: 137 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,32 @@ extension Executable: CustomStringConvertible, CustomDebugStringConvertible {
330330
}
331331
}
332332

333+
// MARK: - Executable Introspection
334+
335+
extension Executable {
336+
/// The public representation of an executable's contents.
337+
///
338+
/// Use this to introspect how an ``Executable`` was constructed (for
339+
/// example, to verify in tests that a configuration builder produced the
340+
/// expected executable reference without spawning a subprocess).
341+
public enum Representation: Sendable, Hashable {
342+
/// The executable is referenced by name and resolved against `PATH`.
343+
case name(String)
344+
/// The executable is referenced by an absolute or relative file path.
345+
case path(FilePath)
346+
}
347+
348+
/// The contents of this executable.
349+
public var representation: Representation {
350+
switch self.storage {
351+
case .executable(let name):
352+
return .name(name)
353+
case .path(let path):
354+
return .path(path)
355+
}
356+
}
357+
}
358+
333359
// MARK: - Arguments
334360

335361
/// A collection of arguments to pass to the subprocess.
@@ -338,17 +364,17 @@ public struct Arguments: Sendable, ExpressibleByArrayLiteral, Hashable {
338364
public typealias ArrayLiteralElement = String
339365

340366
internal let storage: [StringOrRawBytes]
341-
internal let executablePathOverride: StringOrRawBytes?
367+
internal let _executablePathOverride: StringOrRawBytes?
342368

343369
/// Creates an arguments value from the given literal values.
344370
public init(arrayLiteral elements: String...) {
345371
self.storage = elements.map { .string($0) }
346-
self.executablePathOverride = nil
372+
self._executablePathOverride = nil
347373
}
348374
/// Creates an arguments value from the given array.
349375
public init(_ array: [String]) {
350376
self.storage = array.map { .string($0) }
351-
self.executablePathOverride = nil
377+
self._executablePathOverride = nil
352378
}
353379

354380
/// Creates an ``Arguments`` value using the given values, but
@@ -362,9 +388,9 @@ public struct Arguments: Sendable, ExpressibleByArrayLiteral, Hashable {
362388
public init(executablePathOverride: String?, remainingValues: [String]) {
363389
self.storage = remainingValues.map { .string($0) }
364390
if let executablePathOverride = executablePathOverride {
365-
self.executablePathOverride = .string(executablePathOverride)
391+
self._executablePathOverride = .string(executablePathOverride)
366392
} else {
367-
self.executablePathOverride = nil
393+
self._executablePathOverride = nil
368394
}
369395
}
370396
#if !os(Windows) // Windows does not support non-unicode arguments
@@ -379,15 +405,15 @@ public struct Arguments: Sendable, ExpressibleByArrayLiteral, Hashable {
379405
public init(executablePathOverride: [UInt8]?, remainingValues: [[UInt8]]) {
380406
self.storage = remainingValues.map { .rawBytes($0) }
381407
if let override = executablePathOverride {
382-
self.executablePathOverride = .rawBytes(override)
408+
self._executablePathOverride = .rawBytes(override)
383409
} else {
384-
self.executablePathOverride = nil
410+
self._executablePathOverride = nil
385411
}
386412
}
387413
/// Creates an arguments value from the array you provide.
388414
public init(_ array: [[UInt8]]) {
389415
self.storage = array.map { .rawBytes($0) }
390-
self.executablePathOverride = nil
416+
self._executablePathOverride = nil
391417
}
392418
#endif
393419
}
@@ -397,7 +423,7 @@ extension Arguments: CustomStringConvertible, CustomDebugStringConvertible {
397423
public var description: String {
398424
var result: [String] = self.storage.map(\.description)
399425

400-
if let override = self.executablePathOverride {
426+
if let override = self._executablePathOverride {
401427
result.insert("override\(override.description)", at: 0)
402428
}
403429
return result.description
@@ -407,6 +433,63 @@ extension Arguments: CustomStringConvertible, CustomDebugStringConvertible {
407433
public var debugDescription: String { return self.description }
408434
}
409435

436+
// MARK: - Arguments Introspection
437+
438+
extension Arguments {
439+
/// A single argument value, preserving the form in which it was supplied.
440+
///
441+
/// On POSIX platforms, arguments may be constructed from non-Unicode
442+
/// raw bytes; on Windows, only `String` values are representable.
443+
public enum Value: Sendable, Hashable {
444+
/// A string argument.
445+
case string(String)
446+
#if !os(Windows)
447+
/// A raw-bytes argument.
448+
///
449+
/// - Note: This case is only available on POSIX platforms.
450+
case rawBytes([UInt8])
451+
#endif
452+
}
453+
454+
/// The argument that overrides the executable path as `argv[0]`, or `nil`
455+
/// if the executable path is used unchanged.
456+
///
457+
/// This corresponds to the `executablePathOverride` parameter passed to
458+
/// the initializer, and is useful for verifying configuration in tests
459+
/// without spawning a subprocess.
460+
public var executablePathOverride: Value? {
461+
self._executablePathOverride.map(Value.init)
462+
}
463+
}
464+
465+
extension Arguments: RandomAccessCollection {
466+
public typealias Element = Value
467+
public typealias Index = Int
468+
469+
public var startIndex: Int { self.storage.startIndex }
470+
public var endIndex: Int { self.storage.endIndex }
471+
472+
public subscript(position: Int) -> Value {
473+
Value(self.storage[position])
474+
}
475+
}
476+
477+
extension Arguments.Value {
478+
internal init(_ storage: StringOrRawBytes) {
479+
switch storage {
480+
case .string(let s):
481+
self = .string(s)
482+
case .rawBytes(let b):
483+
#if os(Windows)
484+
// Unreachable: The Windows public API cannot construct rawBytes arguments.
485+
fatalError("Internal inconsistency: rawBytes argument on Windows")
486+
#else
487+
self = .rawBytes(b)
488+
#endif
489+
}
490+
}
491+
}
492+
410493
// MARK: - Environment
411494

412495
/// A set of environment variables to use when running the subprocess.
@@ -545,7 +628,8 @@ extension Environment: CustomStringConvertible, CustomDebugStringConvertible {
545628
}
546629

547630
extension Environment.Key {
548-
package static let path: Self = "PATH"
631+
/// The well-known key for the `PATH` environment variable.
632+
public static let path: Self = "PATH"
549633
}
550634

551635
extension Environment.Key: CodingKeyRepresentable {}
@@ -607,6 +691,49 @@ extension Environment.Key: RawRepresentable {
607691

608692
extension Environment.Key: Sendable {}
609693

694+
// MARK: - Environment Introspection
695+
696+
extension Environment {
697+
/// The public representation of an environment's contents.
698+
///
699+
/// Use this to introspect how an ``Environment`` was constructed (for
700+
/// example, to verify in tests that a configuration builder produced
701+
/// the expected inherited overrides or custom values without spawning a
702+
/// subprocess).
703+
public enum Representation: Sendable, Hashable {
704+
/// The environment inherits from the current process, with the given
705+
/// updates applied.
706+
///
707+
/// A `nil` value for a key indicates that the key is unset relative to
708+
/// the inherited environment, rather than being set to an empty value.
709+
case inherited(updates: [Key: String?])
710+
/// The environment uses the given custom values, with no inheritance
711+
/// from the current process.
712+
case custom([Key: String])
713+
#if !os(Windows)
714+
/// The environment uses the given raw bytes, with no inheritance from
715+
/// the current process.
716+
///
717+
/// - Note: This case is only available on POSIX platforms.
718+
case rawBytes([[UInt8]])
719+
#endif
720+
}
721+
722+
/// The contents of this environment.
723+
public var representation: Representation {
724+
switch self.config {
725+
case .inherit(let updates):
726+
return .inherited(updates: updates)
727+
case .custom(let values):
728+
return .custom(values)
729+
#if !os(Windows)
730+
case .rawBytes(let bytes):
731+
return .rawBytes(bytes)
732+
#endif
733+
}
734+
}
735+
}
736+
610737
// MARK: - TerminationStatus
611738

612739
/// The exit status of a subprocess.

Sources/Subprocess/Platforms/Subprocess+Unix.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ extension Arguments {
258258
internal func createArgs(withExecutablePath executablePath: String) -> [UnsafeMutablePointer<CChar>?] {
259259
var argv: [UnsafeMutablePointer<CChar>?] = self.storage.map { $0.createRawBytes() }
260260
// argv[0] = executable path
261-
if let override = self.executablePathOverride {
261+
if let override = self._executablePathOverride {
262262
argv.insert(override.createRawBytes(), at: 0)
263263
} else {
264264
argv.insert(strdup(executablePath), at: 0)

Sources/Subprocess/Platforms/Subprocess+Windows.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ extension Configuration {
7272
// user wants to override executable path in arguments, we have to use `lpApplicationName`
7373
// to specify the executable path. In this case, manually loop over all possible paths.
7474
let possibleExecutablePaths: _OrderedSet<String>
75-
if _fastPath(self.arguments.executablePathOverride == nil) {
75+
if _fastPath(self.arguments._executablePathOverride == nil) {
7676
// Fast path: we can rely on `CreateProcessW`'s built in Path searching
7777
switch self.executable.storage {
7878
case .executable(let executable):
@@ -266,7 +266,7 @@ extension Configuration {
266266
// user wants to override executable path in arguments, we have to use `lpApplicationName`
267267
// to specify the executable path. In this case, manually loop over all possible paths.
268268
let possibleExecutablePaths: _OrderedSet<String>
269-
if _fastPath(self.arguments.executablePathOverride == nil) {
269+
if _fastPath(self.arguments._executablePathOverride == nil) {
270270
// Fast path: we can rely on `CreateProcessW`'s built in Path searching
271271
switch self.executable.storage {
272272
case .executable(let executable):
@@ -1044,7 +1044,7 @@ extension Configuration {
10441044
// Omit applicationName (and therefore rely on commandAndArgs
10451045
// for executable path) if we don't need to override arg0
10461046
return (
1047-
applicationName: self.arguments.executablePathOverride == nil ? nil : applicationName,
1047+
applicationName: self.arguments._executablePathOverride == nil ? nil : applicationName,
10481048
commandAndArgs: commandAndArgs,
10491049
environment: environmentString,
10501050
intendedWorkingDir: self.workingDirectory?.string
@@ -1216,7 +1216,7 @@ extension Configuration {
12161216
return stringValue
12171217
}
12181218

1219-
if case .string(let overrideName) = self.arguments.executablePathOverride {
1219+
if case .string(let overrideName) = self.arguments._executablePathOverride {
12201220
// Use the override as argument0 and set applicationName
12211221
args.insert(overrideName, at: 0)
12221222
} else {

0 commit comments

Comments
 (0)