// Author: Simon-Pierre Boucher — contact@spboucher.ai // // Finds and validates the forge binary: a candidate is accepted only if // `forge info` runs and reports a device — the same probe surfaces the GPU // name for the header. Paths persist in UserDefaults. import Foundation struct ForgeValidation: Equatable, Sendable { var version: String // device line, e.g. "Apple M5 Max" var binaryPath: String } enum ForgeBinaryLocator { static let defaultsKey = "forge.binary.path" static let workspaceKey = "forge.workspace.path" static var savedBinaryURL: URL? { UserDefaults.standard.string(forKey: defaultsKey).map { URL(fileURLWithPath: $0) } } static var savedWorkspaceURL: URL? { UserDefaults.standard.string(forKey: workspaceKey).map { URL(fileURLWithPath: $0) } } static func save(binary: URL) { UserDefaults.standard.set(binary.path, forKey: defaultsKey) } static func save(workspace: URL) { UserDefaults.standard.set(workspace.path, forKey: workspaceKey) } /// Candidate locations, most specific first. static func candidates() -> [URL] { var urls: [URL] = [] if let saved = savedBinaryURL { urls.append(saved) } let home = FileManager.default.homeDirectoryForCurrentUser urls.append(home.appendingPathComponent("Desktop/forge/build/forge")) urls.append(URL(fileURLWithPath: "/usr/local/bin/forge")) return urls.filter { FileManager.default.isExecutableFile(atPath: $0.path) } } /// Runs `forge info` and parses the device line. static func validate(binary: URL) async -> Result { let runner = ProcessRunner() do { let (lines, exit) = try await runner.launch( executable: binary, arguments: ["info"], currentDirectory: binary.deletingLastPathComponent()) var device = "" for await (line, _) in lines where line.hasPrefix("device: ") { device = String(line.dropFirst("device: ".count)) } let status = await exit.value guard status.code == 0, !device.isEmpty else { throw NSError(domain: "ForgeStudio", code: 1, userInfo: [ NSLocalizedDescriptionKey: "forge info a échoué (code \(status.code)). Vérifiez que forge.metallib est à côté du binaire.", ]) } return .success(ForgeValidation(version: device, binaryPath: binary.path)) } catch { return .failure(error) } } }