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
14pub 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
35pub 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 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#[derive(Debug, Clone)]
171pub enum BuildError {
172 Io(Arc<io::Error>),
174 Command {
176 status: std::process::ExitStatus,
178 err: Txt,
180 },
181 ManifestPathDidNotBuild {
183 path: PathBuf,
185 },
186 UnknownMessageFormat {
188 pat: Txt,
190 },
191 Load(Arc<libloading::Error>),
193 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)] 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}