PadXcode-Daemon/Daemon/PreBuildRunner.swift
2026-04-12 00:42:51 -07:00

60 lines
3 KiB
Swift

import Foundation
import Vapor
final class PreBuildRunner {
static func run(hooks: [PreBuildHook], session: BuildSession, logger: Logger) async -> Bool {
for hook in hooks {
session.broadcast(BuildLogMessage(type: .stdout, text: "\n⚙️ Hook: \(hook.name)\n", exitCode: nil))
let ok = await runHook(hook, session: session)
if !ok {
session.broadcast(BuildLogMessage(type: .stderr, text: "❌ Hook '\(hook.name)' failed.", exitCode: nil))
if !hook.continueOnFailure { return false }
} else {
session.broadcast(BuildLogMessage(type: .stdout, text: "\(hook.name) done.\n", exitCode: nil))
}
}
return true
}
private static func runHook(_ hook: PreBuildHook, session: BuildSession) async -> Bool {
await withCheckedContinuation { cont in
let p = Process()
p.executableURL = URL(fileURLWithPath: "/bin/zsh")
p.arguments = ["-l", "-c", hook.command]
p.currentDirectoryURL = URL(fileURLWithPath: hook.workingDirectory)
var env = ProcessInfo.processInfo.environment
env["PATH"] = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:" + (env["PATH"] ?? "")
p.environment = env
let out = Pipe(); let err = Pipe()
p.standardOutput = out; p.standardError = err
var timedOut = false
let wd = DispatchWorkItem { timedOut = true; p.terminate()
session.broadcast(BuildLogMessage(type: .stderr, text: "⏱ Hook '\(hook.name)' timed out.", exitCode: nil))
}
DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(hook.timeoutSeconds), execute: wd)
let g = DispatchGroup()
g.enter()
out.fileHandleForReading.readabilityHandler = { h in
let d = h.availableData
if d.isEmpty { out.fileHandleForReading.readabilityHandler = nil; g.leave(); return }
if let t = String(data: d, encoding: .utf8) { session.broadcast(BuildLogMessage(type: .stdout, text: t, exitCode: nil)) }
}
g.enter()
err.fileHandleForReading.readabilityHandler = { h in
let d = h.availableData
if d.isEmpty { err.fileHandleForReading.readabilityHandler = nil; g.leave(); return }
if let t = String(data: d, encoding: .utf8) { session.broadcast(BuildLogMessage(type: .stderr, text: t, exitCode: nil)) }
}
p.terminationHandler = { proc in
wd.cancel()
g.notify(queue: .global()) { cont.resume(returning: proc.terminationStatus == 0 && !timedOut) }
}
do { try p.run() }
catch {
wd.cancel()
session.broadcast(BuildLogMessage(type: .stderr, text: "Launch failed: \(error)", exitCode: nil))
cont.resume(returning: false)
}
}
}
}