Skip to content

Commit 231d282

Browse files
committed
Make Configuration introspectable
1 parent 570c92f commit 231d282

4 files changed

Lines changed: 340 additions & 15 deletions

File tree

Sources/Subprocess/Configuration.swift

Lines changed: 140 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,33 @@ extension Executable: CustomStringConvertible, CustomDebugStringConvertible {
371371
}
372372
}
373373

374+
// MARK: - Executable Introspection
375+
376+
extension Executable {
377+
/// The public representation of an executable's contents.
378+
///
379+
/// Use this to introspect how an ``Executable`` was constructed (for
380+
/// example, to verify in tests that a configuration builder produced the
381+
/// expected executable reference without spawning a subprocess).
382+
@frozen
383+
public enum Representation: Sendable, Hashable {
384+
/// The executable is referenced by name and resolved against `PATH`.
385+
case name(String)
386+
/// The executable is referenced by an absolute or relative file path.
387+
case path(FilePath)
388+
}
389+
390+
/// The contents of this executable.
391+
public var representation: Representation {
392+
switch self.storage {
393+
case .executable(let name):
394+
return .name(name)
395+
case .path(let path):
396+
return .path(path)
397+
}
398+
}
399+
}
400+
374401
// MARK: - Arguments
375402

376403
/// A collection of arguments to pass to the subprocess.
@@ -379,17 +406,17 @@ public struct Arguments: Sendable, ExpressibleByArrayLiteral, Hashable {
379406
public typealias ArrayLiteralElement = String
380407

381408
internal let storage: [StringOrRawBytes]
382-
internal let executablePathOverride: StringOrRawBytes?
409+
internal let _executablePathOverride: StringOrRawBytes?
383410

384411
/// Creates an arguments value from the given literal values.
385412
public init(arrayLiteral elements: String...) {
386413
self.storage = elements.map { .string($0) }
387-
self.executablePathOverride = nil
414+
self._executablePathOverride = nil
388415
}
389416
/// Creates an arguments value from the given array.
390417
public init(_ array: [String]) {
391418
self.storage = array.map { .string($0) }
392-
self.executablePathOverride = nil
419+
self._executablePathOverride = nil
393420
}
394421

395422
/// Creates an ``Arguments`` value using the given values, but
@@ -403,9 +430,9 @@ public struct Arguments: Sendable, ExpressibleByArrayLiteral, Hashable {
403430
public init(executablePathOverride: String?, remainingValues: [String]) {
404431
self.storage = remainingValues.map { .string($0) }
405432
if let executablePathOverride = executablePathOverride {
406-
self.executablePathOverride = .string(executablePathOverride)
433+
self._executablePathOverride = .string(executablePathOverride)
407434
} else {
408-
self.executablePathOverride = nil
435+
self._executablePathOverride = nil
409436
}
410437
}
411438
#if !os(Windows) // Windows does not support non-unicode arguments
@@ -420,15 +447,15 @@ public struct Arguments: Sendable, ExpressibleByArrayLiteral, Hashable {
420447
public init(executablePathOverride: [UInt8]?, remainingValues: [[UInt8]]) {
421448
self.storage = remainingValues.map { .rawBytes($0) }
422449
if let override = executablePathOverride {
423-
self.executablePathOverride = .rawBytes(override)
450+
self._executablePathOverride = .rawBytes(override)
424451
} else {
425-
self.executablePathOverride = nil
452+
self._executablePathOverride = nil
426453
}
427454
}
428455
/// Creates an arguments value from the array you provide.
429456
public init(_ array: [[UInt8]]) {
430457
self.storage = array.map { .rawBytes($0) }
431-
self.executablePathOverride = nil
458+
self._executablePathOverride = nil
432459
}
433460
#endif
434461
}
@@ -438,7 +465,7 @@ extension Arguments: CustomStringConvertible, CustomDebugStringConvertible {
438465
public var description: String {
439466
var result: [String] = self.storage.map(\.description)
440467

441-
if let override = self.executablePathOverride {
468+
if let override = self._executablePathOverride {
442469
result.insert("override\(override.description)", at: 0)
443470
}
444471
return result.description
@@ -448,6 +475,64 @@ extension Arguments: CustomStringConvertible, CustomDebugStringConvertible {
448475
public var debugDescription: String { return self.description }
449476
}
450477

478+
// MARK: - Arguments Introspection
479+
480+
extension Arguments {
481+
/// A single argument value, preserving the form in which it was supplied.
482+
///
483+
/// On POSIX platforms, arguments may be constructed from non-Unicode
484+
/// raw bytes; on Windows, only `String` values are representable.
485+
@frozen
486+
public enum Value: Sendable, Hashable {
487+
/// A string argument.
488+
case string(String)
489+
#if !os(Windows)
490+
/// A raw-bytes argument.
491+
///
492+
/// - Note: This case is only available on POSIX platforms.
493+
case rawBytes([UInt8])
494+
#endif
495+
}
496+
497+
/// The argument that overrides the executable path as `argv[0]`, or `nil`
498+
/// if the executable path is used unchanged.
499+
///
500+
/// This corresponds to the `executablePathOverride` parameter passed to
501+
/// the initializer, and is useful for verifying configuration in tests
502+
/// without spawning a subprocess.
503+
public var executablePathOverride: Value? {
504+
self._executablePathOverride.map(Value.init)
505+
}
506+
}
507+
508+
extension Arguments: RandomAccessCollection {
509+
public typealias Element = Value
510+
public typealias Index = Int
511+
512+
public var startIndex: Int { self.storage.startIndex }
513+
public var endIndex: Int { self.storage.endIndex }
514+
515+
public subscript(position: Int) -> Value {
516+
Value(self.storage[position])
517+
}
518+
}
519+
520+
extension Arguments.Value {
521+
internal init(_ storage: StringOrRawBytes) {
522+
switch storage {
523+
case .string(let s):
524+
self = .string(s)
525+
case .rawBytes(let b):
526+
#if os(Windows)
527+
// Unreachable: The Windows public API cannot construct rawBytes arguments.
528+
fatalError("Internal inconsistency: rawBytes argument on Windows")
529+
#else
530+
self = .rawBytes(b)
531+
#endif
532+
}
533+
}
534+
}
535+
451536
// MARK: - Environment
452537

453538
/// A set of environment variables to use when running the subprocess.
@@ -586,7 +671,8 @@ extension Environment: CustomStringConvertible, CustomDebugStringConvertible {
586671
}
587672

588673
extension Environment.Key {
589-
package static let path: Self = "PATH"
674+
/// The well-known key for the `PATH` environment variable.
675+
public static let path: Self = "PATH"
590676
}
591677

592678
extension Environment.Key: CodingKeyRepresentable {}
@@ -648,6 +734,50 @@ extension Environment.Key: RawRepresentable {
648734

649735
extension Environment.Key: Sendable {}
650736

737+
// MARK: - Environment Introspection
738+
739+
extension Environment {
740+
/// The public representation of an environment's contents.
741+
///
742+
/// Use this to introspect how an ``Environment`` was constructed (for
743+
/// example, to verify in tests that a configuration builder produced
744+
/// the expected inherited overrides or custom values without spawning a
745+
/// subprocess).
746+
@nonexhaustive
747+
public enum Representation: Sendable, Hashable {
748+
/// The environment inherits from the current process, with the given
749+
/// updates applied.
750+
///
751+
/// A `nil` value for a key indicates that the key is unset relative to
752+
/// the inherited environment, rather than being set to an empty value.
753+
case inherited(updates: [Key: String?])
754+
/// The environment uses the given custom values, with no inheritance
755+
/// from the current process.
756+
case custom([Key: String])
757+
#if !os(Windows)
758+
/// The environment uses the given raw bytes, with no inheritance from
759+
/// the current process.
760+
///
761+
/// - Note: This case is only available on POSIX platforms.
762+
case rawBytes([[UInt8]])
763+
#endif
764+
}
765+
766+
/// The contents of this environment.
767+
public var representation: Representation {
768+
switch self.config {
769+
case .inherit(let updates):
770+
return .inherited(updates: updates)
771+
case .custom(let values):
772+
return .custom(values)
773+
#if !os(Windows)
774+
case .rawBytes(let bytes):
775+
return .rawBytes(bytes)
776+
#endif
777+
}
778+
}
779+
}
780+
651781
// MARK: - TerminationStatus
652782

653783
/// 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
@@ -83,7 +83,7 @@ extension Configuration {
8383
// user wants to override executable path in arguments, we have to use `lpApplicationName`
8484
// to specify the executable path. In this case, manually loop over all possible paths.
8585
let possibleExecutablePaths: _OrderedSet<String>
86-
if _fastPath(self.arguments.executablePathOverride == nil) {
86+
if _fastPath(self.arguments._executablePathOverride == nil) {
8787
// Fast path: we can rely on `CreateProcessW`'s built in Path searching
8888
switch self.executable.storage {
8989
case .executable(let executable):
@@ -335,7 +335,7 @@ extension Configuration {
335335
// user wants to override executable path in arguments, we have to use `lpApplicationName`
336336
// to specify the executable path. In this case, manually loop over all possible paths.
337337
let possibleExecutablePaths: _OrderedSet<String>
338-
if _fastPath(self.arguments.executablePathOverride == nil) {
338+
if _fastPath(self.arguments._executablePathOverride == nil) {
339339
// Fast path: we can rely on `CreateProcessW`'s built in Path searching
340340
switch self.executable.storage {
341341
case .executable(let executable):
@@ -1215,7 +1215,7 @@ extension Configuration {
12151215
// Omit applicationName (and therefore rely on commandAndArgs
12161216
// for executable path) if we don't need to override arg0
12171217
return (
1218-
applicationName: self.arguments.executablePathOverride == nil ? nil : applicationName,
1218+
applicationName: self.arguments._executablePathOverride == nil ? nil : applicationName,
12191219
commandAndArgs: commandAndArgs,
12201220
environment: environmentString,
12211221
intendedWorkingDir: self.workingDirectory?.string
@@ -1460,7 +1460,7 @@ extension Configuration {
14601460
return stringValue
14611461
}
14621462

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

0 commit comments

Comments
 (0)