91 lines
3.8 KiB
Swift
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
|
|
}
|
|
}
|