Skip to main content

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", "--locked"]);
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                    if let Some(rustflags) = params.rustflags.as_ref() {
158                        command.env("RUSTFLAGS", rustflags);
159                    }
160
161                    for (k, v) in params.build_env {
162                        command.env(k, v);
163                    }
164
165                    let mut spawned = command
166                        .current_dir(&params.src)
167                        .stdout(Stdio::piped())
168                        .stderr(Stdio::piped())
169                        .stdin(Stdio::null())
170                        .spawn()
171                        .unwrap();
172
173                    let reader = std::io::BufReader::new(spawned.stdout.take().unwrap());
174                    let stderr_reader = std::io::BufReader::new(spawned.stderr.take().unwrap());
175
176                    let stderr_worker = std::thread::spawn(move || {
177                        let mut stderr_lines = Vec::new();
178                        for line in stderr_reader.lines() {
179                            let Ok(line) = line else {
180                                break;
181                            };
182                            set_msg(line.clone());
183                            stderr_lines.push(line);
184                        }
185                        stderr_lines
186                    });
187
188                    let mut diagnostics = Vec::new();
189                    let mut text_lines = Vec::new();
190                    for message in cargo_metadata::Message::parse_stream(reader) {
191                        match message.unwrap() {
192                            cargo_metadata::Message::CompilerArtifact(artifact) => {
193                                let is_output = if params.example.is_some() {
194                                    artifact.target.kind.iter().any(|k| "example" == k)
195                                } else {
196                                    artifact.target.kind.iter().any(|k| "bin" == k)
197                                };
198
199                                if is_output {
200                                    let path = artifact.executable.unwrap();
201                                    let path_buf: PathBuf = path.clone().into();
202                                    let path = path.into_string();
203                                    let data = std::fs::read(path).unwrap();
204                                    assert!(spawned.wait().unwrap().success());
205                                    return Ok(BuildOutput {
206                                        bin_data: data,
207                                        bin_path: path_buf,
208                                        shared_library_path: if params.is_dylib {
209                                            Some(
210                                                params
211                                                    .target_dir
212                                                    .as_ref()
213                                                    .unwrap_or(&params.src.join("target"))
214                                                    .join("debug")
215                                                    .join("deps"),
216                                            )
217                                        } else {
218                                            None
219                                        },
220                                    });
221                                }
222                            }
223                            cargo_metadata::Message::CompilerMessage(mut msg) => {
224                                // Update the path displayed to enable clicking in IDE.
225                                // TODO(mingwei): deduplicate code with hydro_lang sim/graph.rs
226                                if let Some(rendered) = msg.message.rendered.as_mut() {
227                                    let file_names = msg
228                                        .message
229                                        .spans
230                                        .iter()
231                                        .map(|s| &s.file_name)
232                                        .collect::<std::collections::BTreeSet<_>>();
233                                    for file_name in file_names {
234                                        *rendered = rendered.replace(
235                                            file_name,
236                                            &format!(
237                                                "(full path) {}/{file_name}",
238                                                params.src.display(),
239                                            ),
240                                        )
241                                    }
242                                }
243                                ProgressTracker::println(msg.message.to_string());
244                                diagnostics.push(msg.message);
245                            }
246                            cargo_metadata::Message::TextLine(line) => {
247                                ProgressTracker::println(&line);
248                                text_lines.push(line);
249                            }
250                            cargo_metadata::Message::BuildFinished(_) => {}
251                            cargo_metadata::Message::BuildScriptExecuted(_) => {}
252                            msg => panic!("Unexpected message type: {:?}", msg),
253                        }
254                    }
255
256                    let exit_status = spawned.wait().unwrap();
257                    if exit_status.success() {
258                        Err(BuildError::NoBinaryEmitted)
259                    } else {
260                        let stderr_lines = stderr_worker
261                            .join()
262                            .expect("Stderr worker unexpectedly panicked.");
263                        Err(BuildError::FailedToBuildCrate {
264                            exit_status,
265                            diagnostics,
266                            text_lines,
267                            stderr_lines,
268                        })
269                    }
270                })
271                .await
272                .map_err(|_| BuildError::TokioJoinError)?
273            })
274        })
275        .await
276}
277
278#[derive(Clone, Debug)]
279pub enum BuildError {
280    FailedToBuildCrate {
281        exit_status: ExitStatus,
282        diagnostics: Vec<Diagnostic>,
283        text_lines: Vec<String>,
284        stderr_lines: Vec<String>,
285    },
286    TokioJoinError,
287    NoBinaryEmitted,
288}
289
290impl Display for BuildError {
291    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292        match self {
293            Self::FailedToBuildCrate {
294                exit_status,
295                diagnostics,
296                text_lines,
297                stderr_lines,
298            } => {
299                writeln!(f, "Failed to build crate ({})", exit_status)?;
300                writeln!(f, "Diagnostics ({}):", diagnostics.len())?;
301                for diagnostic in diagnostics {
302                    write!(f, "{}", diagnostic)?;
303                }
304                writeln!(f, "Text output ({} lines):", text_lines.len())?;
305                for line in text_lines {
306                    writeln!(f, "{}", line)?;
307                }
308                writeln!(f, "Stderr output ({} lines):", stderr_lines.len())?;
309                for line in stderr_lines {
310                    writeln!(f, "{}", line)?;
311                }
312            }
313            Self::TokioJoinError => {
314                write!(f, "Failed to spawn tokio blocking task.")?;
315            }
316            Self::NoBinaryEmitted => {
317                write!(f, "`cargo build` succeeded but no binary was emitted.")?;
318            }
319        }
320        Ok(())
321    }
322}
323
324impl Error for BuildError {}