Skip to main content

npmgen_core/compile/
mod.rs

1//! Phase 1: cross-build the binary for each target.
2//!
3//! [`Compiler`] drives an injected [`BuildDriver`] (the default [`CargoDriver`]
4//! shells out to cargo) and verifies each artifact lands where the assembly
5//! phase looks for it. Swapping the driver makes the build phase mockable and
6//! lets a non-cargo backend slot in without bundling a cross-compiler.
7
8mod cargo;
9mod driver;
10
11pub use cargo::CargoDriver;
12pub use driver::BuildDriver;
13
14use std::path::PathBuf;
15use std::process::ExitStatus;
16
17use crate::project::Project;
18use crate::target::Target;
19
20/// Builds target binaries through an injected [`BuildDriver`].
21#[derive(Debug)]
22pub struct Compiler<'a> {
23    driver: &'a dyn BuildDriver,
24}
25
26impl<'a> Compiler<'a> {
27    pub fn new(driver: &'a dyn BuildDriver) -> Self {
28        Self { driver }
29    }
30
31    /// Build every target, verifying each artifact exists where the assembly
32    /// phase will look for it.
33    pub fn compile_all(&self, project: &Project, targets: &[Target]) -> Result<(), CompileError> {
34        for target in targets {
35            self.driver.build(project, target)?;
36            let path = target.binary_path(&project.target_directory, &project.bin);
37            if !path.is_file() {
38                return Err(CompileError::BinaryMissing {
39                    triple: target.triple.clone(),
40                    path,
41                });
42            }
43        }
44        Ok(())
45    }
46}
47
48/// Failures while building target binaries.
49#[derive(Debug, thiserror::Error)]
50pub enum CompileError {
51    #[error("spawning build driver {driver:?}")]
52    Spawn {
53        driver: String,
54        #[source]
55        source: std::io::Error,
56    },
57
58    #[error("build for {triple} failed: {status}")]
59    BuildFailed { triple: String, status: ExitStatus },
60
61    #[error("binary for {triple} not found after a successful build: {}", path.display())]
62    BinaryMissing { triple: String, path: PathBuf },
63
64    #[error("build driver {driver:?} must be a bare command name on PATH, not a path")]
65    InvalidDriver { driver: String },
66
67    #[error("workspace root does not exist: {}", path.display())]
68    MissingWorkspaceRoot { path: PathBuf },
69}
70
71#[cfg(test)]
72mod tests {
73    use super::{BuildDriver, CompileError, Compiler};
74    use crate::project::{Project, sample_project};
75    use crate::target::Target;
76    use std::fs;
77    use std::path::PathBuf;
78
79    fn scratch(tag: &str) -> PathBuf {
80        let dir = std::env::temp_dir().join(format!("npmgen-compile-{}-{tag}", std::process::id()));
81        let _ = fs::remove_dir_all(&dir);
82        fs::create_dir_all(&dir).unwrap();
83        dir
84    }
85
86    /// Reports success without producing any artifact.
87    #[derive(Debug)]
88    struct NoopDriver;
89    impl BuildDriver for NoopDriver {
90        fn build(&self, _project: &Project, _target: &Target) -> Result<(), CompileError> {
91            Ok(())
92        }
93    }
94
95    /// Writes the artifact where the assembly phase expects it.
96    #[derive(Debug)]
97    struct PlacingDriver;
98    impl BuildDriver for PlacingDriver {
99        fn build(&self, project: &Project, target: &Target) -> Result<(), CompileError> {
100            let path = target.binary_path(&project.target_directory, &project.bin);
101            fs::create_dir_all(path.parent().unwrap()).unwrap();
102            fs::write(&path, b"binary").unwrap();
103            Ok(())
104        }
105    }
106
107    #[test]
108    fn missing_binary_after_a_successful_build_is_an_error() {
109        let mut project = sample_project();
110        project.target_directory = scratch("missing");
111        project.bin = "tool".to_owned();
112        let target = Target::from_triple("x86_64-unknown-linux-gnu").unwrap();
113
114        let error = Compiler::new(&NoopDriver)
115            .compile_all(&project, std::slice::from_ref(&target))
116            .unwrap_err();
117        assert!(matches!(error, CompileError::BinaryMissing { .. }));
118        let _ = fs::remove_dir_all(&project.target_directory);
119    }
120
121    #[test]
122    fn verifies_a_placed_binary() {
123        let mut project = sample_project();
124        project.target_directory = scratch("placed");
125        project.bin = "tool".to_owned();
126        let target = Target::from_triple("x86_64-unknown-linux-gnu").unwrap();
127
128        Compiler::new(&PlacingDriver)
129            .compile_all(&project, std::slice::from_ref(&target))
130            .unwrap();
131        let _ = fs::remove_dir_all(&project.target_directory);
132    }
133
134    #[test]
135    fn a_missing_workspace_root_is_an_error() {
136        use super::CargoDriver;
137        let mut project = sample_project();
138        project.workspace_root = PathBuf::from("npmgen-nonexistent-workspace-root-xyz");
139        let target = Target::from_triple("x86_64-unknown-linux-gnu").unwrap();
140        let error = CargoDriver::new("cargo")
141            .build(&project, &target)
142            .unwrap_err();
143        assert!(matches!(error, CompileError::MissingWorkspaceRoot { .. }));
144    }
145}