feat: install release builds and report missing FFmpeg

Add make release and make install PREFIX=/Users/valdemar/.local. Keep optimized no-debug release artifacts separate; install adjacent CLI/app under lib/terebi and link bin/terebi. Preflight FFmpeg/pkg-config before compiling or installing, including cached builds.

Record the player's exact linked FFmpeg ABI paths. AppKit-only CLI/bootstrap validate native loadability and transitive dependencies before entering the FFmpeg-linked terebi-player. CLI fails with status 69 and remediation; Finder bootstrap shows a native alert and execs the player without losing initial events or process identity.

Verified staged installation with spaces, reinstall preservation, absent build dependencies, missing/corrupt/transitively broken dylibs, real modal error surface, installed CLI and Finder startup across exec, local/YouTube resume and window reuse, and full make check.
This commit is contained in:
2026-10-11 01:17:14 +02:00
parent 230949bb95
commit c6d2958744
6 changed files with 245 additions and 73 deletions
+34
View File
@@ -0,0 +1,34 @@
import AppKit
import Darwin
@main
@MainActor
enum TerebiBootstrap {
static func main() {
let app = Bundle.main.bundleURL
if let error = FFmpegRuntime.error(for: app) {
fail(error)
}
let payload = app.appendingPathComponent("Contents/MacOS/terebi-player")
let errorCode = payload.path.withCString { path in
_ = execv(path, CommandLine.unsafeArgv)
return errno
}
let detail = String(cString: strerror(errorCode))
fail("Could not start the player: \(detail)\n\nPlayer executable: \(payload.path)\nRebuild and reinstall terebi.")
}
private static func fail(_ message: String) -> Never {
FileHandle.standardError.write(Data("terebi: \(message)\n".utf8))
let application = NSApplication.shared
application.setActivationPolicy(.regular)
application.activate(ignoringOtherApps: true)
let alert = NSAlert()
alert.alertStyle = .critical
alert.messageText = "terebi could not start"
alert.informativeText = message
alert.addButton(withTitle: "Quit")
alert.runModal()
exit(69)
}
}
+3
View File
@@ -20,6 +20,9 @@ enum TerebiCLI {
}
let app = executable.resolvingSymlinksInPath().deletingLastPathComponent()
.appendingPathComponent("terebi.app", isDirectory: true)
if let error = FFmpegRuntime.error(for: app) {
fail(error, code: 69)
}
let configuration = NSWorkspace.OpenConfiguration()
do {
// Launch without media first so SwiftUI creates its singleton window.
+39
View File
@@ -0,0 +1,39 @@
import Foundation
import Darwin
enum FFmpegRuntime {
static func error(for app: URL) -> String? {
let manifest = app.appendingPathComponent("Contents/Resources/ffmpeg-libraries.txt")
let metadataError = "This app's FFmpeg library metadata is missing or invalid. Rebuild and reinstall terebi."
guard let contents = try? String(contentsOf: manifest, encoding: .utf8) else {
return metadataError
}
var paths = contents.components(separatedBy: "\n")
if paths.last == "" { paths.removeLast() }
let libraries: Set<String> = ["libavformat", "libavcodec", "libavutil", "libswresample", "libswscale"]
guard paths.count == libraries.count else { return metadataError }
var found = Set<String>()
for path in paths {
guard path.hasPrefix("/"), !path.contains("\0"), !path.contains("\r") else {
return metadataError
}
let parts = URL(fileURLWithPath: path).lastPathComponent.split(separator: ".", omittingEmptySubsequences: false)
guard parts.count >= 2, parts.last == "dylib" else { return metadataError }
let library = String(parts[0])
guard libraries.contains(library),
parts.dropFirst().dropLast().allSatisfy({ !$0.isEmpty && $0.utf8.allSatisfy { (48...57).contains($0) } }),
found.insert(library).inserted else {
return metadataError
}
}
for path in paths {
_ = dlerror()
guard dlopen(path, RTLD_NOW | RTLD_LOCAL) != nil else {
let detail = dlerror().map { String(cString: $0) } ?? "The dynamic loader could not load this library."
return "The FFmpeg libraries required by this build are unavailable or broken.\n\nRequired library: \(path)\n\(detail)\n\nInstall FFmpeg with `brew install ffmpeg`. If FFmpeg was upgraded or its library versions changed, rebuild and reinstall terebi."
}
// Deliberately never dlclose: retain successful loads until exit or bootstrap exec.
}
return nil
}
}