zng_ext_hot_reload/
cargo.rs

1use std::{
2    fmt,
3    io::{self, BufRead as _, Read},
4    path::PathBuf,
5    process::{ChildStdout, Command, Stdio},
6    sync::Arc,
7};
8
9use zng_app::handler::clmv;
10use zng_task::SignalOnce;
11use zng_txt::{ToTxt, Txt};
12use zng_var::ResponseVar;
13
14/// Build and return the dylib path.
15pub fn build(
16    manifest_dir: &str,
17    package_option: &str,
18    package: &str,
19    bin_option: &str,
20    bin: &str,
21    cancel: SignalOnce,
22) -> ResponseVar<Result<PathBuf, BuildError>> {
23    let mut build = Command::new("cargo");
24    build.arg("build").arg("--message-format").arg("json");
25    if !package.is_empty() {
26        build.arg(package_option).arg(package);
27    }
28    if !bin.is_empty() {
29        build.arg(bin_option).arg(bin);
30    }
31
32    build_custom(manifest_dir, build, cancel)
33}
34
35/// Build and return the dylib path.
36pub fn build_custom(manifest_dir: &str, mut build: Command, cancel: SignalOnce) -> ResponseVar<Result<PathBuf, BuildError>> {
37    let manifest_path = PathBuf::from(manifest_dir).join("Cargo.toml");
38
39    zng_task::respond(async move {
40        let mut child = zng_task::wait(move || build.stdin(Stdio::null()).stderr(Stdio::piped()).stdout(Stdio::piped()).spawn()).await?;
41
42        let stdout = child.stdout.take().unwrap();
43        let stderr = child.stderr.take().unwrap();
44        let run = zng_task::wait(clmv!(manifest_path, || run_build(manifest_path, stdout)));
45
46        let cancel = async move {
47            cancel.await;
48            Err(BuildError::Cancelled)
49        };
50
51        match zng_task::any!(run, cancel).await {
52            Ok(p) => {
53                zng_task::spawn_wait(move || {
54                    if let Err(e) = child.kill() {
55                        tracing::error!("failed to kill build after hot dylib successfully built, {e}");
56                    } else {
57                        let _ = child.wait();
58                    }
59                });
60                Ok(p)
61            }
62            Err(e) => {
63                if matches!(e, BuildError::Cancelled) {
64                    zng_task::spawn_wait(move || {
65                        if let Err(e) = child.kill() {
66                            tracing::error!("failed to kill build after cancel, {e}");
67                        } else {
68                            let _ = child.wait();
69                        }
70                    });
71
72                    Err(e)
73                } else if matches!(&e, BuildError::Io(e) if e.kind() == io::ErrorKind::UnexpectedEof) {
74                    // run_build read to EOF without finding manifest_path
75                    let status = zng_task::wait(move || {
76                        child.kill()?;
77                        child.wait()
78                    });
79                    match status.await {
80                        Ok(status) => {
81                            if status.success() {
82                                Err(BuildError::ManifestPathDidNotBuild { path: manifest_path })
83                            } else {
84                                let mut err = String::new();
85                                let mut stderr = stderr;
86                                stderr.read_to_string(&mut err)?;
87                                Err(BuildError::Command {
88                                    status,
89                                    err: err.lines().next_back().unwrap_or("").to_txt(),
90                                })
91                            }
92                        }
93                        Err(wait_e) => Err(wait_e.into()),
94                    }
95                } else {
96                    Err(e)
97                }
98            }
99        }
100    })
101}
102
103fn run_build(manifest_path: PathBuf, stdout: ChildStdout) -> Result<PathBuf, BuildError> {
104    for line in io::BufReader::new(stdout).lines() {
105        let line = line?;
106
107        const COMP_ARTIFACT: &str = r#"{"reason":"compiler-artifact","#;
108        const MANIFEST_FIELD: &str = r#""manifest_path":""#;
109        const FILENAMES_FIELD: &str = r#""filenames":["#;
110
111        if line.starts_with(COMP_ARTIFACT) {
112            let i = match line.find(MANIFEST_FIELD) {
113                Some(i) => i,
114                None => {
115                    return Err(BuildError::UnknownMessageFormat {
116                        pat: MANIFEST_FIELD.into(),
117                    });
118                }
119            };
120            let line = &line[i + MANIFEST_FIELD.len()..];
121            let i = match line.find('"') {
122                Some(i) => i,
123                None => {
124                    return Err(BuildError::UnknownMessageFormat {
125                        pat: MANIFEST_FIELD.into(),
126                    });
127                }
128            };
129            let line_manifest = PathBuf::from(&line[..i]);
130
131            if line_manifest != manifest_path {
132                continue;
133            }
134
135            let line = &line[i..];
136            let i = match line.find(FILENAMES_FIELD) {
137                Some(i) => i,
138                None => {
139                    return Err(BuildError::UnknownMessageFormat {
140                        pat: FILENAMES_FIELD.into(),
141                    });
142                }
143            };
144            let line = &line[i + FILENAMES_FIELD.len()..];
145            let i = match line.find(']') {
146                Some(i) => i,
147                None => {
148                    return Err(BuildError::UnknownMessageFormat {
149                        pat: FILENAMES_FIELD.into(),
150                    });
151                }
152            };
153
154            for file in line[..i].split(',') {
155                let file = PathBuf::from(file.trim().trim_matches('"'));
156                if file.extension().map(|e| e != "rlib").unwrap_or(true) {
157                    return Ok(file);
158                }
159            }
160            return Err(BuildError::UnknownMessageFormat {
161                pat: FILENAMES_FIELD.into(),
162            });
163        }
164    }
165
166    Err(BuildError::Io(Arc::new(io::Error::new(io::ErrorKind::UnexpectedEof, ""))))
167}
168
169/// Rebuild error.
170#[derive(Debug, Clone)]
171#[non_exhaustive]
172pub enum BuildError {
173    /// Error starting, ending the build command.
174    Io(Arc<io::Error>),
175    /// Build command error.
176    Command {
177        /// Command exit status.
178        status: std::process::ExitStatus,
179        /// Display error.
180        err: Txt,
181    },
182    /// Build command did not rebuild the dylib.
183    ManifestPathDidNotBuild {
184        /// Cargo.toml file that was expected to rebuild.
185        path: PathBuf,
186    },
187    /// Cargo `--message-format json` did not output in an expected format.
188    UnknownMessageFormat {
189        /// Pattern that was not found in the message line.
190        pat: Txt,
191    },
192    /// Error loading built library.
193    Load(Arc<libloading::Error>),
194    /// Build cancelled.
195    Cancelled,
196}
197impl PartialEq for BuildError {
198    fn eq(&self, other: &Self) -> bool {
199        match (self, other) {
200            (Self::Io(l0), Self::Io(r0)) => Arc::ptr_eq(l0, r0),
201            (
202                Self::Command {
203                    status: l_exit_status,
204                    err: l_stderr,
205                },
206                Self::Command {
207                    status: r_exit_status,
208                    err: r_stderr,
209                },
210            ) => l_exit_status == r_exit_status && l_stderr == r_stderr,
211            _ => false,
212        }
213    }
214}
215impl From<io::Error> for BuildError {
216    fn from(err: io::Error) -> Self {
217        Self::Io(Arc::new(err))
218    }
219}
220impl From<libloading::Error> for BuildError {
221    fn from(err: libloading::Error) -> Self {
222        Self::Load(Arc::new(err))
223    }
224}
225impl fmt::Display for BuildError {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        match self {
228            BuildError::Io(e) => fmt::Display::fmt(e, f),
229            BuildError::Command { status, err } => {
230                write!(f, "build command failed")?;
231                let mut sep = "\n";
232                #[allow(unused_assignments)] // depends on cfg
233                if let Some(c) = status.code() {
234                    write!(f, "{sep}exit code: {c:#x}")?;
235                    sep = ", ";
236                }
237                #[cfg(unix)]
238                {
239                    use std::os::unix::process::ExitStatusExt;
240                    if let Some(s) = status.signal() {
241                        write!(f, "{sep}signal: {s}")?;
242                    }
243                }
244                write!(f, "\n{err}")?;
245
246                Ok(())
247            }
248            BuildError::ManifestPathDidNotBuild { path } => write!(f, "build command did not build `{}`", path.display()),
249            BuildError::UnknownMessageFormat { pat: field } => write!(f, "could not find expected `{field}` in cargo JSON message"),
250            BuildError::Load(e) => fmt::Display::fmt(e, f),
251            BuildError::Cancelled => write!(f, "build cancelled"),
252        }
253    }
254}
255impl std::error::Error for BuildError {
256    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
257        match self {
258            BuildError::Io(e) => Some(&**e),
259            BuildError::Load(e) => Some(&**e),
260            _ => None,
261        }
262    }
263}