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
10 changes: 9 additions & 1 deletion Docs/generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ creates and boots one dedicated simulator device for the run, then deletes that
exact device after generation, failure, or interruption. It does not use a
connected iPhone or Apple Watch as a generation source. An explicit `--device`
selects an existing borrowed simulator instead; PrivateHeaderKit never deletes
that device.
that device. When generation has produced a typed terminal outcome, its final
`Finished` block is rendered after successful cleanup of a dedicated device. If
cleanup fails, the command instead reports the exact device name and UDID and
exits with an error.

## Automation

Expand Down Expand Up @@ -92,6 +95,11 @@ Consumers should use only the concrete directory printed as `Headers`:
<output-base>/generated-headers/<platform>/<release-directory>/
```

The completion summary keeps this consumer path under `Output`. The internal
state database and the full, unabridged run identifier are listed separately
under `Diagnostics`; they are troubleshooting references, not generated-header
locations.

Platform directories use the displayed Apple platform name: `iOS`, `watchOS`,
or `macOS`. Release directories include the exact build when it is available:

Expand Down
95 changes: 75 additions & 20 deletions Sources/PrivateHeaderKitCLI/PrivateHeaderKitCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ typealias PrivateHeaderKitReleaseMetadataResolver = @Sendable (
) throws -> Bool
typealias PrivateHeaderKitOutputLogger = @Sendable (String) -> Void

struct PrivateHeaderKitCommandOutcome: Equatable, Sendable {
let exitCode: Int32
let runStatus: PrivateHeaderGeneration.RunStatus?
}

func resolvePrivateHeaderKitReleaseMetadata(
systemRoot: URL,
layout: RuntimeRootLayout
Expand Down Expand Up @@ -303,7 +308,7 @@ func runPrivateHeaderKitGenerateCommand(
outputLogger: @escaping PrivateHeaderKitOutputLogger,
errorLogger: @escaping PrivateHeaderKitOutputLogger
) async throws -> Int32 {
try await withPrivateHeaderKitSimulatorSession(
let outcome = try await withPrivateHeaderKitSimulatorSession(
command,
resolver: simulatorResolver,
cleaner: simulatorCleaner,
Expand All @@ -330,25 +335,37 @@ func runPrivateHeaderKitGenerateCommand(
errorLogger: errorLogger
)
} catch let error as PrivateHeaderGeneration.GenerationError {
if Task.isCancelled {
throw CancellationError()
let cancellationRequested = Task.isCancelled
if cancellationRequested {
guard case .runInterrupted = error else {
throw CancellationError()
}
}
renderPrivateHeaderKitGenerationError(
let runStatus = renderPrivateHeaderKitGenerationError(
error,
sourceDisplayName: request.source.label.displayName,
targetQuery: command.targetQuery,
screenClearer: resultScreenClearer,
outputLogger: errorLogger
)
return 2
return PrivateHeaderKitCommandOutcome(
exitCode: cancellationRequested ? 130 : 2,
runStatus: runStatus
)
}
} catch is CancellationError {
throw CancellationError()
} catch {
errorLogger("error: \(error)")
return 2
return PrivateHeaderKitCommandOutcome(exitCode: 2, runStatus: nil)
}
}
renderPrivateHeaderKitCommandOutcome(
outcome,
outputLogger: outputLogger,
errorLogger: errorLogger
)
return outcome.exitCode
}

func preparePrivateHeaderKitGenerationRequest(
Expand Down Expand Up @@ -391,7 +408,7 @@ func runPrivateHeaderKitPreparedGeneration(
resultScreenClearer: PrivateHeaderKitInteractiveScreenClearer?,
outputLogger: @escaping PrivateHeaderKitOutputLogger,
errorLogger: @escaping PrivateHeaderKitOutputLogger
) async throws -> Int32 {
) async throws -> PrivateHeaderKitCommandOutcome {
do {
let result = try await preparedGeneration.run(
resumeBehavior,
Expand All @@ -409,24 +426,30 @@ func runPrivateHeaderKitPreparedGeneration(
title: "Generation completed",
outputLogger: outputLogger
)
return 0
return PrivateHeaderKitCommandOutcome(exitCode: 0, runStatus: result.summary.status)
} catch let error as PrivateHeaderGeneration.GenerationError {
if Task.isCancelled {
throw CancellationError()
let cancellationRequested = Task.isCancelled
if cancellationRequested {
guard case .runInterrupted = error else {
throw CancellationError()
}
}
renderPrivateHeaderKitGenerationError(
let runStatus = renderPrivateHeaderKitGenerationError(
error,
sourceDisplayName: request.source.label.displayName,
targetQuery: targetQuery,
screenClearer: resultScreenClearer,
outputLogger: errorLogger
)
return 2
return PrivateHeaderKitCommandOutcome(
exitCode: cancellationRequested ? 130 : 2,
runStatus: runStatus
)
} catch is CancellationError {
throw CancellationError()
} catch {
errorLogger("error: \(error)")
return 2
return PrivateHeaderKitCommandOutcome(exitCode: 2, runStatus: nil)
}
}

Expand Down Expand Up @@ -1085,8 +1108,13 @@ func withPrivateHeaderKitSimulatorSession<Result>(
if command.platform.simulatorPlatform == nil {
resolution = nil
} else {
renderPrivateHeaderKitSimulatorPreparation(command, outputLogger: outputLogger)
let resolved = try await resolver(command)
outputLogger("selected simulator: \(resolved.deviceName) (\(resolved.deviceUDID))")
renderPrivateHeaderKitSimulatorReady(
resolved,
command: command,
outputLogger: outputLogger
)
resolution = resolved
}

Expand All @@ -1097,9 +1125,15 @@ func withPrivateHeaderKitSimulatorSession<Result>(
let operationError = error
if let resolution {
do {
try await finishPrivateHeaderKitSimulatorSession(resolution, cleaner: cleaner)
try await finishPrivateHeaderKitSimulatorSession(
resolution,
cleaner: cleaner,
outputLogger: outputLogger
)
} catch {
throw PrivateHeaderKitSimulatorSessionCleanupError(
deviceName: resolution.deviceName,
deviceUDID: resolution.deviceUDID,
operationError: String(describing: operationError),
cleanupError: String(describing: error)
)
Expand All @@ -1108,28 +1142,49 @@ func withPrivateHeaderKitSimulatorSession<Result>(
throw operationError
}
if let resolution {
try await finishPrivateHeaderKitSimulatorSession(resolution, cleaner: cleaner)
do {
try await finishPrivateHeaderKitSimulatorSession(
resolution,
cleaner: cleaner,
outputLogger: outputLogger
)
} catch {
throw PrivateHeaderKitSimulatorSessionCleanupError(
deviceName: resolution.deviceName,
deviceUDID: resolution.deviceUDID,
operationError: nil,
cleanupError: String(describing: error)
)
}
}
return result
}

private struct PrivateHeaderKitSimulatorSessionCleanupError: Error, CustomStringConvertible {
let operationError: String
struct PrivateHeaderKitSimulatorSessionCleanupError: Error, CustomStringConvertible {
let deviceName: String
let deviceUDID: String
let operationError: String?
let cleanupError: String

var description: String {
"simulator cleanup failed after \(operationError): \(cleanupError)"
let device = "\(deviceName) (UDID: \(deviceUDID))"
guard let operationError else {
return "simulator cleanup failed for \(device): \(cleanupError)"
}
return "simulator cleanup failed for \(device) after \(operationError): \(cleanupError)"
}
}

private func finishPrivateHeaderKitSimulatorSession(
_ resolution: PrivateHeaderKitSimulatorResolution,
cleaner: @escaping PrivateHeaderKitSimulatorCleaner
cleaner: @escaping PrivateHeaderKitSimulatorCleaner,
outputLogger: @escaping PrivateHeaderKitOutputLogger
) async throws {
guard resolution.deviceOwnership == .runOwned else { return }
try await Task.detached {
try await cleaner(resolution)
}.value
renderPrivateHeaderKitSimulatorCleanup(outputLogger: outputLogger)
}

func cleanupPrivateHeaderKitSimulator(
Expand Down
11 changes: 10 additions & 1 deletion Sources/PrivateHeaderKitCLI/PrivateHeaderKitInteractive.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ func runPrivateHeaderKitInteractiveGenerate(
simulatorHelperPath: nil
)
do {
return try await withPrivateHeaderKitSimulatorSession(
let outcome = try await withPrivateHeaderKitSimulatorSession(
command,
resolver: simulatorResolver,
cleaner: simulatorCleaner,
Expand Down Expand Up @@ -197,13 +197,22 @@ func runPrivateHeaderKitInteractiveGenerate(
errorLogger: errorLogger
)
}
renderPrivateHeaderKitCommandOutcome(
outcome,
outputLogger: outputLogger,
errorLogger: errorLogger
)
return outcome.exitCode
} catch PrivateHeaderKitInteractiveNavigation.back {
continue targetSelection
}
}
}
} catch is CancellationError {
throw CancellationError()
} catch let error as PrivateHeaderKitSimulatorSessionCleanupError {
errorLogger("error: \(error)")
return 2
} catch let error as PrivateHeaderKitCLIError {
errorLogger("error: \(error.description)")
return 1
Expand Down
68 changes: 57 additions & 11 deletions Sources/PrivateHeaderKitCLI/PrivateHeaderKitRendering.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Dispatch
import Foundation
import PrivateHeaderKitCore
import PrivateHeaderKitTooling

#if canImport(Darwin)
import Darwin
Expand Down Expand Up @@ -72,7 +73,7 @@ final class PrivateHeaderKitProgressOutputLogger: @unchecked Sendable {
switch event {
case .runStarted(let runID, let totalTargetCount):
outputLogger(
"Generation \(shortenedRunID(runID.rawValue)): \(totalTargetCount) targets → "
"Generation \(runID.rawValue): \(totalTargetCount) targets → "
+ artifactDirectory.path
)
case .targetStarted(let index, let total, let displayName):
Expand Down Expand Up @@ -315,13 +316,14 @@ func concisePrivateHeaderKitDiagnostic(_ message: String) -> String {
return String(sanitized.prefix(maximumLength - 1)) + "…"
}

@discardableResult
func renderPrivateHeaderKitGenerationError(
_ error: PrivateHeaderGeneration.GenerationError,
sourceDisplayName: String,
targetQuery: String,
screenClearer: PrivateHeaderKitInteractiveScreenClearer?,
outputLogger: PrivateHeaderKitOutputLogger
) {
) -> PrivateHeaderGeneration.RunStatus? {
switch error {
case .runFailed(let failure):
screenClearer?()
Expand All @@ -333,6 +335,7 @@ func renderPrivateHeaderKitGenerationError(
failedTargetIDs: failure.failedTargetIDs,
outputLogger: outputLogger
)
return failure.summary.status
case .runInterrupted(let interruption):
renderPrivateHeaderKitRunSummary(
interruption.summary,
Expand All @@ -341,6 +344,7 @@ func renderPrivateHeaderKitGenerationError(
title: "Generation interrupted",
outputLogger: outputLogger
)
return interruption.summary.status
case .infrastructureFailed(let failure):
screenClearer?()
renderPrivateHeaderKitRunSummary(
Expand All @@ -351,11 +355,14 @@ func renderPrivateHeaderKitGenerationError(
infrastructureMessage: failure.message,
outputLogger: outputLogger
)
return failure.summary.status
case .resumeRequired:
outputLogger("error: \(error.description)")
outputLogger("rerun with `--resume` to continue or `--fresh` to restart")
return nil
default:
outputLogger("error: \(error.description)")
return nil
}
}

Expand Down Expand Up @@ -430,8 +437,55 @@ func renderPrivateHeaderKitRunSummary(
outputLogger("")
outputLogger("Output")
outputLogger(formatResultField("Headers", summary.artifactDirectory.path))

outputLogger("")
outputLogger("Diagnostics")
outputLogger(formatResultField("State", summary.stateDatabaseURL.path))
outputLogger(formatResultField("Run", shortenedRunID(summary.runID.rawValue)))
outputLogger(formatResultField("Run", summary.runID.rawValue))
}

func renderPrivateHeaderKitSimulatorPreparation(
_ command: PrivateHeaderKitGenerateCommand,
outputLogger: PrivateHeaderKitOutputLogger
) {
outputLogger("Preparing simulator for \(command.platform.rawValue) \(command.version)...")
}

func renderPrivateHeaderKitSimulatorReady(
_ resolution: PrivateHeaderKitSimulatorResolution,
command: PrivateHeaderKitGenerateCommand,
outputLogger: PrivateHeaderKitOutputLogger
) {
let description: String
switch resolution.deviceOwnership {
case .borrowed:
description = "\(resolution.deviceName) (UDID: \(resolution.deviceUDID))"
case .runOwned:
description =
"temporary \(command.platform.rawValue) \(resolution.runtimeVersion) device "
+ "(UDID: \(resolution.deviceUDID))"
}
outputLogger("Simulator ready: \(description)")
}

func renderPrivateHeaderKitSimulatorCleanup(
outputLogger: PrivateHeaderKitOutputLogger
) {
outputLogger("")
outputLogger("Cleanup")
outputLogger(formatResultField("Simulator", "Temporary device deleted"))
}

func renderPrivateHeaderKitCommandOutcome(
_ outcome: PrivateHeaderKitCommandOutcome,
outputLogger: PrivateHeaderKitOutputLogger,
errorLogger: PrivateHeaderKitOutputLogger
) {
guard let runStatus = outcome.runStatus else { return }
let logger = outcome.exitCode == 0 ? outputLogger : errorLogger
logger("")
logger("Finished")
logger(formatResultField("Status", runStatus.rawValue))
}

private func formatResultMetric(_ label: String, _ value: Int) -> String {
Expand All @@ -457,14 +511,6 @@ private func formattedTargetQuery(_ query: String) -> String {
.joined(separator: ", ")
}

private func shortenedRunID(_ runID: String) -> String {
let maximumLength = 36
guard runID.count > maximumLength else {
return runID
}
return String(runID.prefix(maximumLength - 1)) + "…"
}

func logCLIError(_ message: String) {
FileHandle.standardError.write(Data((message + "\n").utf8))
}
Expand Down
Loading