spb/zyquo-mlx Public MIT
The local MLX foundry for your Mac — run, fine-tune, quantize, and ship models. Nothing leaves your machine.
Swift 93.4%
Python 3.8%
Makefile 2.2%
Shell 0.5%
1//2// PythonEnvironment.swift3// Zyquo MLX4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//89import Foundation1011/// Manages the isolated, pinned Python environment in12/// `~/Library/Application Support/ZyquoMLX/py/venv` (docs/BUILD.md §3.4).13/// Provisioned with `uv` on first use; never touches system Python; lives14/// outside the signed bundle (notarization posture, docs/BUILD.md §4).15actor PythonEnvironment {1617 static let shared = PythonEnvironment()1819 /// Exact pins per docs/MLX-RESEARCH.md §6 (core set; mlx-vlm/whisper/audio20 /// install on demand when those capabilities are first used).21 static let pythonVersion = "3.12"22 static let corePins = ["mlx-lm==0.31.3", "pyyaml==6.0.3"]2324 enum EnvironmentError: LocalizedError {25 case uvNotFound26 case provisioningFailed(String)2728 var errorDescription: String? {29 switch self {30 case .uvNotFound:31 "The 'uv' tool is required to set up the Python environment. Install it with: brew install uv"32 case .provisioningFailed(let detail):33 "Python environment setup failed: \(detail)"34 }35 }36 }3738 var venvURL: URL {39 PersistenceService.pythonDirectory.appendingPathComponent("venv", isDirectory: true)40 }4142 var pythonURL: URL {43 venvURL.appendingPathComponent("bin/python")44 }4546 private var markerURL: URL {47 PersistenceService.pythonDirectory.appendingPathComponent("provisioned.json")48 }4950 var isProvisioned: Bool {51 FileManager.default.fileExists(atPath: pythonURL.path)52 && FileManager.default.fileExists(atPath: markerURL.path)53 }5455 /// Locate `uv` in the usual installation paths (the app has no shell PATH).56 static func findUV() -> URL? {57 let candidates = [58 "\(NSHomeDirectory())/.local/bin/uv",59 "/opt/homebrew/bin/uv",60 "/usr/local/bin/uv",61 ]62 return candidates.first { FileManager.default.fileExists(atPath: $0) }63 .map { URL(fileURLWithPath: $0) }64 }6566 /// Create the venv and install core pins. Idempotent: a pre-existing67 /// healthy venv (verified by importing the pinned packages) is adopted;68 /// a broken one is recreated in place (`uv venv --clear`).69 func provision(progress: @Sendable (String) -> Void = { _ in }) async throws {70 if isProvisioned { return }71 guard let uv = Self.findUV() else { throw EnvironmentError.uvNotFound }7273 try FileManager.default.createDirectory(74 at: PersistenceService.pythonDirectory, withIntermediateDirectories: true)7576 if FileManager.default.fileExists(atPath: pythonURL.path), verifyImports() {77 progress("Adopting existing Python environment.")78 try writeMarker()79 return80 }8182 progress("Installing Python \(Self.pythonVersion)…")83 try runUV(uv, ["python", "install", Self.pythonVersion])8485 progress("Creating environment…")86 try runUV(uv, ["venv", venvURL.path, "--clear", "--python", Self.pythonVersion])8788 progress("Installing MLX packages…")89 try runUV(uv, ["pip", "install", "--python", pythonURL.path] + Self.corePins)9091 try writeMarker()92 progress("Python environment ready.")93 }9495 /// True when the pinned core packages import cleanly.96 private func verifyImports() -> Bool {97 let process = Process()98 process.executableURL = pythonURL99 process.arguments = ["-c", "import mlx_lm, yaml"]100 process.standardOutput = Pipe()101 process.standardError = Pipe()102 do {103 try process.run()104 process.waitUntilExit()105 return process.terminationStatus == 0106 } catch {107 return false108 }109 }110111 private func writeMarker() throws {112 let marker: [String: String] = [113 "python": Self.pythonVersion,114 "pins": Self.corePins.joined(separator: " "),115 "provisionedAt": ISO8601DateFormatter().string(from: .now),116 ]117 try PersistenceService.saveJSON(marker, to: markerURL)118 }119120 /// Install additional pinned packages (e.g. mlx-vlm when VLM Python121 /// pipelines are first needed).122 func install(pins: [String]) throws {123 guard let uv = Self.findUV() else { throw EnvironmentError.uvNotFound }124 try runUV(uv, ["pip", "install", "--python", pythonURL.path] + pins)125 }126127 /// Delete and re-provision (Settings › Python Environment › Repair).128 func repair(progress: @Sendable (String) -> Void = { _ in }) async throws {129 try? FileManager.default.removeItem(at: venvURL)130 try? FileManager.default.removeItem(at: markerURL)131 try await provision(progress: progress)132 }133134 private func runUV(_ uv: URL, _ arguments: [String]) throws {135 let process = Process()136 process.executableURL = uv137 process.arguments = arguments138 let errPipe = Pipe()139 process.standardError = errPipe140 process.standardOutput = Pipe()141 try process.run()142 process.waitUntilExit()143 if process.terminationStatus != 0 {144 let data = errPipe.fileHandleForReading.readDataToEndOfFile()145 throw EnvironmentError.provisioningFailed(146 String(data: data, encoding: .utf8) ?? "exit \(process.terminationStatus)")147 }148 }149}150