Files
reyna-cli/native/ReynaCLIHost/Sources/ReynaCLIHostCore/PythonLauncher.swift
T
2026-08-16 08:24:45 -04:00

91 lines
3.8 KiB
Swift

import Foundation
public struct PythonRuntime: Equatable {
public let workingDirectory: URL
public let python: URL
}
public func parsePythonRuntimeConfig(_ content: String) -> [String: String] {
var values: [String: String] = [:]
for rawLine in content.components(separatedBy: .newlines) {
let line = rawLine.trimmingCharacters(in: .whitespacesAndNewlines)
guard !line.isEmpty, !line.hasPrefix("#") else { continue }
let parts = line.split(separator: "=", maxSplits: 1).map(String.init)
guard parts.count == 2 else { continue }
let key = parts[0].trimmingCharacters(in: .whitespacesAndNewlines)
var value = parts[1].trimmingCharacters(in: .whitespacesAndNewlines)
if value.count >= 2,
((value.hasPrefix("\"") && value.hasSuffix("\"")) || (value.hasPrefix("'") && value.hasSuffix("'"))) {
value.removeFirst()
value.removeLast()
}
guard key == "REYNA_CLI_DIR" || key == "REYNA_CLI_PYTHON" else { continue }
values[key] = value
}
return values
}
public func pythonRuntimeConfig(homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser) -> [String: String] {
let paths = [
homeDirectory.appendingPathComponent(".reyna-cli.env"),
homeDirectory.appendingPathComponent(".config/reyna-cli/env"),
]
var result: [String: String] = [:]
for path in paths {
guard let content = try? String(contentsOf: path, encoding: .utf8) else { continue }
result.merge(parsePythonRuntimeConfig(content)) { _, newest in newest }
}
return result
}
public func resolvePythonRuntime(
homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser,
environment: [String: String] = ProcessInfo.processInfo.environment,
config: [String: String]? = nil,
isExecutable: (String) -> Bool = { FileManager.default.isExecutableFile(atPath: $0) }
) -> PythonRuntime {
let loadedConfig = config ?? pythonRuntimeConfig(homeDirectory: homeDirectory)
let defaultWorkDirectory = homeDirectory.appendingPathComponent("Projects/reyna-cli").path
let workDirectoryPath = environment["REYNA_CLI_DIR"] ?? loadedConfig["REYNA_CLI_DIR"] ?? defaultWorkDirectory
let selectedPython = environment["REYNA_CLI_PYTHON"]
?? loadedConfig["REYNA_CLI_PYTHON"]
?? URL(fileURLWithPath: workDirectoryPath).appendingPathComponent(".venv/bin/python").path
let pythonPath = isExecutable(selectedPython) ? selectedPython : "/usr/bin/python3"
return PythonRuntime(
workingDirectory: URL(fileURLWithPath: workDirectoryPath),
python: URL(fileURLWithPath: pythonPath)
)
}
public func pythonCommand(runtime: PythonRuntime, forwardedArguments: [String]) -> [String] {
[runtime.python.path, "-m", "reyna_cli.cli"] + forwardedArguments
}
public func pythonLaunchEnvironment(from environment: [String: String] = ProcessInfo.processInfo.environment) -> [String: String] {
var sanitized = environment
for key in ["PYTHONHOME", "PYTHONPATH", "VIRTUAL_ENV"] {
sanitized.removeValue(forKey: key)
}
sanitized["PYTHONNOUSERSITE"] = "1"
return sanitized
}
public func runPythonCLI(forwardedArguments: [String]) -> Int32 {
let runtime = resolvePythonRuntime()
let command = pythonCommand(runtime: runtime, forwardedArguments: forwardedArguments)
let process = Process()
process.executableURL = runtime.python
process.arguments = Array(command.dropFirst())
process.currentDirectoryURL = runtime.workingDirectory
process.environment = pythonLaunchEnvironment()
do {
try process.run()
process.waitUntilExit()
return process.terminationStatus
} catch {
fputs("error: failed to run Reyna CLI Python: \(error)\n", stderr)
return 127
}
}