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)]
171pub enum BuildError {
172    /// Error starting, ending the build command.
173    Io(Arc<io::Error>),
174    /// Build command error.
175    Command {
176        /// Command exit status.
177        status: std::process::ExitStatus,
178        /// Display error.
179        err: Txt,
180    },
181    /// Build command did not rebuild the dylib.
182    ManifestPathDidNotBuild {
183        /// Cargo.toml file that was expected to rebuild.
184        path: PathBuf,
185    },
186    /// Cargo `--message-format json` did not output in an expected format.
187    UnknownMessageFormat {
188        /// Pattern that was not found in the message line.
189        pat: Txt,
190    },
191    /// Error loading built library.
192    Load(Arc<libloading::Error>),
193    /// Build cancelled.
194    Cancelled,
195}
196impl PartialEq for BuildError {
197    fn eq(&self, other: &Self) -> bool {
198        match (self, other) {
199            (Self::Io(l0), Self::Io(r0)) => Arc::ptr_eq(l0, r0),
200            (
201                Self::Command {
202                    status: l_exit_status,
203                    err: l_stderr,
204                },
205                Self::Command {
206                    status: r_exit_status,
207                    err: r_stderr,
208                },
209            ) => l_exit_status == r_exit_status && l_stderr == r_stderr,
210            _ => false,
211        }
212    }
213}
214impl From<io::Error> for BuildError {
215    fn from(err: io::Error) -> Self {
216        Self::Io(Arc::new(err))
217    }
218}
219impl From<libloading::Error> for BuildError {
220    fn from(err: libloading::Error) -> Self {
221        Self::Load(Arc::new(err))
222    }
223}
224impl fmt::Display for BuildError {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        match self {
227            BuildError::Io(e) => fmt::Display::fmt(e, f),
228            BuildError::Command { status, err } => {
229                write!(f, "build command failed")?;
230                let mut sep = "\n";
231                #[allow(unused_assignments)] // depends on cfg
232                if let Some(c) = status.code() {
233                    write!(f, "{sep}exit code: {c:#x}")?;
234                    sep = ", ";
235                }
236                #[cfg(unix)]
237                {
238                    use std::os::unix::process::ExitStatusExt;
239                    if let Some(s) = status.signal() {
240                        write!(f, "{sep}signal: {s}")?;
241                    }
242                }
243                write!(f, "\n{err}")?;
244
245                Ok(())
246            }
247            BuildError::ManifestPathDidNotBuild { path } => write!(f, "build command did not build `{}`", path.display()),
248            BuildError::UnknownMessageFormat { pat: field } => write!(f, "could not find expected `{field}` in cargo JSON message"),
249            BuildError::Load(e) => fmt::Display::fmt(e, f),
250            BuildError::Cancelled => write!(f, "build cancelled"),
251        }
252    }
253}
254impl std::error::Error for BuildError {
255    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
256        match self {
257            BuildError::Io(e) => Some(&**e),
258            BuildError::Load(e) => Some(&**e),
259            _ => None,
260        }
261    }
262}