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
14 changes: 12 additions & 2 deletions Docs/generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ header directory when a run starts and again in the completion summary.

macOS generation works from the host system. iOS and watchOS generation require
Xcode, `xcrun`, `simctl`, and the selected Simulator runtime. PrivateHeaderKit
selects and boots a compatible simulator device for the run. It does not use a
connected iPhone or Apple Watch as a generation source.
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.

## Automation

Expand Down Expand Up @@ -181,6 +184,13 @@ whether to continue or restart.
The interactive wizard presents the same Continue or Restart choice when it
finds compatible unfinished work.

Resume compatibility is bound to the PrivateHeaderKit producer version emitted
by the raw helper, the selected source and Simulator runtime, generation
options, and the loaded shared-cache cohort when used. A simulator device UDID
is only a temporary execution address and does not affect compatibility. After
upgrading from state created before producer-version tracking, select Restart or
use `--fresh` once; existing published headers remain available until replaced.

## Legacy Output

PrivateHeaderKit does not silently adopt either legacy form:
Expand Down
19 changes: 18 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ let package = Package(
targets: [
.target(
name: "PrivateHeaderKitHelperProtocol",
dependencies: []
dependencies: [],
plugins: [
.plugin(name: "PrivateHeaderKitBuildInfoPlugin"),
]
),
.target(
name: "PrivateHeaderKitExecutableResolution",
Expand Down Expand Up @@ -151,6 +154,14 @@ let package = Package(
"PrivateHeaderKitRawDumpCore",
]
),
.executableTarget(
name: "PrivateHeaderKitBuildInfoTool"
),
.plugin(
name: "PrivateHeaderKitBuildInfoPlugin",
capability: .buildTool(),
dependencies: ["PrivateHeaderKitBuildInfoTool"]
),
.executableTarget(
name: "PrivateHeaderKitToolingTestHelper",
dependencies: [
Expand All @@ -165,6 +176,12 @@ let package = Package(
],
path: "Tests/PrivateHeaderKitTestSupport"
),
.testTarget(
name: "PrivateHeaderKitBuildInfoToolTests",
dependencies: [
"PrivateHeaderKitBuildInfoTool",
]
),
.testTarget(
name: "PrivateHeaderKitHelperProtocolTests",
dependencies: [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import Foundation
import PackagePlugin

@main
struct PrivateHeaderKitBuildInfoPlugin: BuildToolPlugin {
private static let environmentKey = "PRIVATEHEADERKIT_BUILD_VERSION"

func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
guard target is SourceModuleTarget else { return [] }

let outputFile = context.pluginWorkDirectoryURL.appending(
path: "PrivateHeaderKitBuildInfo.generated.swift"
)
let tool = try context.tool(named: "PrivateHeaderKitBuildInfoTool")
var arguments = [
"--output", outputFile.path,
"--package-directory", context.package.directoryURL.path,
]
if let environmentVersion = ProcessInfo.processInfo.environment[Self.environmentKey] {
arguments.append(contentsOf: ["--environment-version", environmentVersion])
}

return [
.buildCommand(
displayName: "Generate PrivateHeaderKit build info",
executable: tool.url,
arguments: arguments,
inputFiles: Self.identityInputFiles(
in: context.package.directoryURL
),
outputFiles: [outputFile]
)
]
}

private static func identityInputFiles(in packageDirectory: URL) -> [URL] {
var inputs = [
packageDirectory.appending(path: "Package.swift"),
]
let resolved = packageDirectory.appending(path: "Package.resolved")
if FileManager.default.fileExists(atPath: resolved.path) {
inputs.append(resolved)
}
for relativeDirectory in ["Plugins", "Sources"] {
let directory = packageDirectory.appending(path: relativeDirectory)
guard let enumerator = FileManager.default.enumerator(
at: directory,
includingPropertiesForKeys: [.isRegularFileKey, .isSymbolicLinkKey],
options: [.skipsHiddenFiles]
) else { continue }
for case let url as URL in enumerator {
guard let values = try? url.resourceValues(forKeys: [
.isRegularFileKey,
.isSymbolicLinkKey,
]),
values.isRegularFile == true || values.isSymbolicLink == true
else { continue }
inputs.append(url)
}
}
inputs.append(contentsOf: gitReferenceInputs(in: packageDirectory))
return Dictionary(grouping: inputs, by: \.standardizedFileURL.path)
.compactMap { $0.value.first }
.sorted { $0.path < $1.path }
}

private static func gitReferenceInputs(in packageDirectory: URL) -> [URL] {
var inputs: [URL] = []
if let headPath = gitOutput(
["rev-parse", "--git-path", "HEAD"],
in: packageDirectory
) {
inputs.append(gitURL(path: headPath, packageDirectory: packageDirectory))
}
let headLogURL = gitOutput(
["rev-parse", "--git-path", "logs/HEAD"],
in: packageDirectory
).map { gitURL(path: $0, packageDirectory: packageDirectory) }
if let headLogURL,
FileManager.default.fileExists(atPath: headLogURL.path)
{
inputs.append(headLogURL)
} else if let reference = gitOutput(
["symbolic-ref", "-q", "HEAD"],
in: packageDirectory
),
let referencePath = gitOutput(
["rev-parse", "--git-path", reference],
in: packageDirectory
)
{
inputs.append(
gitURL(path: referencePath, packageDirectory: packageDirectory)
)
}
return inputs.filter { FileManager.default.fileExists(atPath: $0.path) }
}

private static func gitURL(path: String, packageDirectory: URL) -> URL {
if path.hasPrefix("/") {
return URL(fileURLWithPath: path)
}
return packageDirectory.appending(path: path)
}

private static func gitOutput(_ arguments: [String], in packageDirectory: URL) -> String? {
let process = Process()
let outputPipe = Pipe()
process.executableURL = URL(fileURLWithPath: "/usr/bin/git")
process.arguments = ["-C", packageDirectory.path] + arguments
process.standardOutput = outputPipe
process.standardError = FileHandle.nullDevice
do {
try process.run()
} catch {
return nil
}
let data = outputPipe.fileHandleForReading.readDataToEndOfFile()
process.waitUntilExit()
guard process.terminationStatus == 0 else { return nil }
let value = String(decoding: data, as: UTF8.self)
.trimmingCharacters(in: .whitespacesAndNewlines)
return value.isEmpty ? nil : value
}
}
Loading