hydro_deploy/rust_crate/
build.rs

1use std::error::Error;
2use std::fmt::Display;
3use std::io::BufRead;
4use std::path::{Path, PathBuf};
5use std::process::{Command, ExitStatus, Stdio};
6use std::sync::OnceLock;
7
8use cargo_metadata::diagnostic::Diagnostic;
9use memo_map::MemoMap;
10use tokio::sync::OnceCell;
11
12use crate::HostTargetType;
13use crate::progress::ProgressTracker;
14
15/// Build parameters for [`build_crate_memoized`].
16#[derive(PartialEq, Eq, Hash, Clone)]
17pub struct BuildParams {
18    /// The working directory for the build, where the `cargo build` command will be run. Crate root.
19    /// [`Self::new`] canonicalizes this path.
20    src: PathBuf,
21    /// `--bin` binary name parameter.
22    bin: Option<String>,
23    /// `--example` parameter.
24    example: Option<String>,
25    /// `--profile` parameter.
26    profile: Option<String>,
27    rustflags: Option<String>,
28    target_dir: Option<PathBuf>,
29    // Environment variables available during build
30    build_env: Vec<(String, String)>,
31    no_default_features: bool,
32    /// `--target <linux>` if cross-compiling for linux ([`HostTargetType::Linux`]).
33    target_type: HostTargetType,
34    /// True is the build should use dynamic linking.
35    is_dylib: bool,
36    /// `--features` flags, will be comma-delimited.
37    features: Option<Vec<String>>,
38    /// `--config` flag
39    config: Vec<String>,
40}
41impl BuildParams {
42    /// Creates a new `BuildParams` and canonicalizes the `src` path.
43    #[expect(clippy::too_many_arguments, reason = "internal code")]
44    pub fn new(
45        src: impl AsRef<Path>,
46        bin: Option<String>,
47        example: Option<String>,
48        profile: Option<String>,
49        rustflags: Option<String>,
50        target_dir: Option<PathBuf>,
51        build_env: Vec<(String, String)>,
52        no_default_features: bool,
53        target_type: HostTargetType,
54        is_dylib: bool,
55        features: Option<Vec<String>>,
56        config: Vec<String>,
57    ) -> Self {
58        // `fs::canonicalize` prepends windows paths with the `r"\\?\"`
59        // https://stackoverflow.com/questions/21194530/what-does-mean-when-prepended-to-a-file-path
60        // However, this breaks the `include!(concat!(env!("OUT_DIR"), "/my/forward/slash/path.rs"))`
61        // Rust codegen pattern on windows. To help mitigate this happening in third party crates, we
62        // instead use `dunce::canonicalize` which is the same as `fs::canonicalize` but avoids the
63        // `\\?\` prefix when possible.
64        let src = dunce::canonicalize(src.as_ref()).unwrap_or_else(|e| {
65            panic!(
66                "Failed to canonicalize path `{}` for build: {e}.",
67                src.as_ref().display(),
68            )
69        });
70
71        BuildParams {
72            src,
73            bin,
74            example,
75            profile,
76            rustflags,
77            target_dir,
78            build_env,
79            no_default_features,
80            target_type,
81            is_dylib,
82            features,
83            config,
84        }
85    }
86}
87
88/// Information about a built crate. See [`build_crate_memoized`].
89pub struct BuildOutput {
90    /// The binary contents as a byte array.
91    pub bin_data: Vec<u8>,
92    /// The path to the binary file. [`Self::bin_data`] has a copy of the content.
93    pub bin_path: PathBuf,
94    /// Shared library path, containing any necessary dylibs.
95    pub shared_library_path: Option<PathBuf>,
96}
97impl BuildOutput {
98    /// A unique ID for the binary, based its contents.
99    pub fn unique_id(&self) -> impl use<> + Display {
100        blake3::hash(&self.bin_data).to_hex()
101    }
102}
103
104/// Build memoization cache.
105static BUILDS: OnceLock<MemoMap<BuildParams, OnceCell<BuildOutput>>> = OnceLock::new();
106
107pub async fn build_crate_memoized(params: BuildParams) -> Result<&'static BuildOutput, BuildError> {
108    BUILDS
109        .get_or_init(MemoMap::new)
110        .get_or_insert(&params, Default::default)
111        .get_or_try_init(move || {
112            ProgressTracker::rich_leaf("build", move |set_msg| async move {
113                tokio::task::spawn_blocking(move || {
114                    let mut command = Command::new("cargo");
115                    command.args(["build", "--frozen"]);
116
117                    if let Some(profile) = params.profile.as_ref() {
118                        command.args(["--profile", profile]);
119                    }
120
121                    if let Some(bin) = params.bin.as_ref() {
122                        command.args(["--bin", bin]);
123                    }
124
125                    if let Some(example) = params.example.as_ref() {
126                        command.args(["--example", example]);
127                    }
128
129                    match params.target_type {
130                        HostTargetType::Local => {}
131                        HostTargetType::Linux(crate::LinuxCompileType::Glibc) => {
132                            command.args(["--target", "x86_64-unknown-linux-gnu"]);
133                        }
134                        HostTargetType::Linux(crate::LinuxCompileType::Musl) => {
135                            command.args(["--target", "x86_64-unknown-linux-musl"]);
136                        }
137                    }
138
139                    if params.no_default_features {
140                        command.arg("--no-default-features");
141                    }
142
143                    if let Some(features) = params.features {
144                        command.args(["--features", &features.join(",")]);
145                    }
146
147                    for config in &params.config {
148                        command.args(["--config", config]);
149                    }
150
151                    command.arg("--message-format=json-diagnostic-rendered-ansi");
152
153                    if let Some(target_dir) = params.target_dir.as_ref() {
154                        command.args(["--target-dir", target_dir.to_str().unwrap()]);
155                    }
156
157                    for (k, v) in params.build_env {
158                        command.env(k, v);
159                    }
160
161                    let mut spawned = command
162                        .current_dir(&params.src)
163                        .stdout(Stdio::piped())
164                        .stderr(Stdio::piped())
165                        .stdin(Stdio::null())
166                        .spawn()
167                        .unwrap();
168
169                    let reader = std::io::BufReader::new(spawned.stdout.take().unwrap());
170                    let stderr_reader = std::io::BufReader::new(spawned.stderr.take().unwrap());
171
172                    let stderr_worker = std::thread::spawn(move || {
173                        let mut stderr_lines = Vec::new();
174                        for line in stderr_reader.lines() {
175                            let Ok(line) = line else {
176                                break;
177                            };
178                            set_msg(line.clone());
179                            stderr_lines.push(line);
180                        }
181                        stderr_lines
182                    });
183
184                    let mut diagnostics = Vec::new();
185                    let mut text_lines = Vec::new();
186                    for message in cargo_metadata::Message::parse_stream(reader) {
187                        match message.unwrap() {
188                            cargo_metadata::Message::CompilerArtifact(artifact) => {
189                                let is_output = if params.example.is_some() {
190                                    artifact.target.kind.contains(&"example".to_string())
191                                } else {
192                                    artifact.target.kind.contains(&"bin".to_string())
193                                };
194
195                                if is_output {
196                                    let path = artifact.executable.unwrap();
197                                    let path_buf: PathBuf = path.clone().into();
198                                    let path = path.into_string();
199                                    let data = std::fs::read(path).unwrap();
200                                    assert!(spawned.wait().unwrap().success());
201                                    return Ok(BuildOutput {
202                                        bin_data: data,
203                                        bin_path: path_buf,
204                                        shared_library_path: if params.is_dylib {
205                                            Some(
206                                                params
207                                                    .target_dir
208                                                    .as_ref()
209                                                    .unwrap_or(&params.src.join("target"))
210                                                    .join("debug")
211                                                    .join("deps"),
212                                            )
213                                        } else {
214                                            None
215                                        },
216                                    });
217                                }
218                            }
219                            cargo_metadata::Message::CompilerMessage(mut msg) => {
220                                // Update the path displayed to enable clicking in IDE.
221                                // TODO(mingwei): deduplicate code with hydro_lang sim/graph.rs
222                                if let Some(rendered) = msg.message.rendered.as_mut() {
223                                    let file_names = msg
224                                        .message
225                                        .spans
226                                        .iter()
227                                        .map(|s| &s.file_name)
228                                        .collect::<std::collections::BTreeSet<_>>();
229                                    for file_name in file_names {
230                                        *rendered = rendered.replace(
231                                            file_name,
232                                            &format!(
233                                                "(full path) {}/{file_name}",
234                                                params.src.display(),
235                                            ),
236                                        )
237                                    }
238                                }
239                                ProgressTracker::println(msg.message.to_string());
240                                diagnostics.push(msg.message);
241                            }
242                            cargo_metadata::Message::TextLine(line) => {
243                                ProgressTracker::println(&line);
244                                text_lines.push(line);
245                            }
246                            cargo_metadata::Message::BuildFinished(_) => {}
247                            cargo_metadata::Message::BuildScriptExecuted(_) => {}
248                            msg => panic!("Unexpected message type: {:?}", msg),
249                        }
250                    }
251
252                    let exit_status = spawned.wait().unwrap();
253                    if exit_status.success() {
254                        Err(BuildError::NoBinaryEmitted)
255                    } else {
256                        let stderr_lines = stderr_worker
257                            .join()
258                            .expect("Stderr worker unexpectedly panicked.");
259                        Err(BuildError::FailedToBuildCrate {
260                            exit_status,
261                            diagnostics,
262                            text_lines,
263                            stderr_lines,
264                        })
265                    }
266                })
267                .await
268                .map_err(|_| BuildError::TokioJoinError)?
269            })
270        })
271        .await
272}
273
274#[derive(Clone, Debug)]
275pub enum BuildError {
276    FailedToBuildCrate {
277        exit_status: ExitStatus,
278        diagnostics: Vec<Diagnostic>,
279        text_lines: Vec<String>,
280        stderr_lines: Vec<String>,
281    },
282    TokioJoinError,
283    NoBinaryEmitted,
284}
285
286impl Display for BuildError {
287    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288        match self {
289            Self::FailedToBuildCrate {
290                exit_status,
291                diagnostics,
292                text_lines,
293                stderr_lines,
294            } => {
295                writeln!(f, "Failed to build crate ({})", exit_status)?;
296                writeln!(f, "Diagnostics ({}):", diagnostics.len())?;
297                for diagnostic in diagnostics {
298                    write!(f, "{}", diagnostic)?;
299                }
300                writeln!(f, "Text output ({} lines):", text_lines.len())?;
301                for line in text_lines {
302                    writeln!(f, "{}", line)?;
303                }
304                writeln!(f, "Stderr output ({} lines):", stderr_lines.len())?;
305                for line in stderr_lines {
306                    writeln!(f, "{}", line)?;
307                }
308            }
309            Self::TokioJoinError => {
310                write!(f, "Failed to spawn tokio blocking task.")?;
311            }
312            Self::NoBinaryEmitted => {
313                write!(f, "`cargo build` succeeded but no binary was emitted.")?;
314            }
315        }
316        Ok(())
317    }
318}
319
320impl Error for BuildError {}