1use std::fmt::Display;
2use std::path::PathBuf;
3
4use anyhow::Context;
5use clap::{Parser, Subcommand};
6use pyo3::exceptions::PyException;
7use pyo3::prelude::*;
8use pyo3::types::PyList;
9
10use crate::{AnyhowError, AnyhowWrapper};
11
12#[derive(Parser, Debug)]
13#[command(name = "Hydro Deploy", author, version, about, long_about = None)]
14struct Cli {
15 #[command(subcommand)]
16 command: Commands,
17}
18
19#[derive(Subcommand, Debug)]
20enum Commands {
21 Deploy {
23 config: PathBuf,
25 #[arg(last(true))]
27 args: Vec<String>,
28 },
29}
30
31fn async_wrapper_module(py: Python) -> Result<&PyModule, PyErr> {
32 PyModule::from_code(
33 py,
34 include_str!("../hydro/async_wrapper.py"),
35 "wrapper.py",
36 "wrapper",
37 )
38}
39
40#[derive(Debug)]
41struct PyErrWithTraceback {
42 err_display: String,
43 traceback: String,
44}
45
46impl std::error::Error for PyErrWithTraceback {}
47
48impl Display for PyErrWithTraceback {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 writeln!(f, "{}", self.err_display)?;
51 write!(f, "{}", self.traceback)
52 }
53}
54
55fn deploy(config: PathBuf, args: Vec<String>) -> anyhow::Result<()> {
56 Python::with_gil(|py| -> anyhow::Result<()> {
57 let syspath: &PyList = py
58 .import("sys")
59 .and_then(|s| s.getattr("path"))
60 .and_then(|p| Ok(p.downcast::<PyList>()?))?;
61
62 syspath.insert(0, PathBuf::from(".").canonicalize().unwrap())?;
63
64 let filename = config.canonicalize().unwrap();
65 let fun: Py<PyAny> = PyModule::from_code(
66 py,
67 std::fs::read_to_string(config).unwrap().as_str(),
68 filename.to_str().unwrap(),
69 "",
70 )
71 .with_context(|| format!("failed to load deployment script: {}", filename.display()))?
72 .getattr("main")
73 .context("expected deployment script to define a `main` function, but one was not found")?
74 .into();
75
76 let wrapper = async_wrapper_module(py)?;
77 match wrapper.call_method1("run", (fun, args)) {
78 Ok(_) => Ok(()),
79 Err(err) => {
80 let traceback = err
81 .traceback(py)
82 .context("traceback was expected but none found")
83 .and_then(|tb| Ok(tb.format()?))?
84 .trim()
85 .to_string();
86
87 if err.is_instance_of::<AnyhowError>(py) {
88 let args = err
89 .value(py)
90 .getattr("args")?
91 .extract::<Vec<AnyhowWrapper>>()?;
92 let wrapper = args.first().unwrap();
93 let underlying = &wrapper.underlying;
94 let mut underlying = underlying.blocking_write();
95 Err(underlying.take().unwrap()).context(traceback)
96 } else {
97 Err(PyErrWithTraceback {
98 err_display: format!("{}", err),
99 traceback,
100 }
101 .into())
102 }
103 }
104 }
105 })?;
106
107 Ok(())
108}
109
110#[pyfunction]
111fn cli_entrypoint(args: Vec<String>) -> PyResult<()> {
112 match Cli::try_parse_from(args) {
113 Ok(args) => {
114 let res = match args.command {
115 Commands::Deploy { config, args } => deploy(config, args),
116 };
117
118 match res {
119 Ok(_) => Ok(()),
120 Err(err) => {
121 eprintln!("{:?}", err);
122 Err(PyErr::new::<PyException, _>(""))
123 }
124 }
125 }
126 Err(err) => {
127 err.print().unwrap();
128 Err(PyErr::new::<PyException, _>(""))
129 }
130 }
131}
132
133#[pymodule]
134pub fn cli(_py: Python, m: &PyModule) -> PyResult<()> {
135 m.add_function(wrap_pyfunction!(cli_entrypoint, m)?)?;
136
137 Ok(())
138}