Skip to main content

lux_lib/build/
rust_binary.rs

1use crate::build::backend::{BuildBackend, BuildInfo, RunBuildArgs};
2use crate::build::{cargo, command, install};
3use crate::config::{build, Config};
4use crate::fs;
5use crate::lua_rockspec::RustBinaryBuildSpec;
6use crate::tree::InstallTree;
7use miette::Diagnostic;
8use path_slash::PathBufExt;
9use std::io;
10use std::path::{Path, PathBuf};
11use std::process::ExitStatus;
12use thiserror::Error;
13
14use tracing::Instrument;
15
16#[derive(Error, Debug, Diagnostic)]
17#[non_exhaustive]
18pub enum Error {
19    #[error("`cargo install` failed.\nstatus: {status}\nstdout: {stdout}\nstderr: {stderr}")]
20    CargoInstall {
21        status: ExitStatus,
22        stdout: String,
23        stderr: String,
24    },
25    #[error("failed to run `cargo`")]
26    #[diagnostic(help("ensure cargo is installed"))]
27    RustBuild { source: io::Error },
28    #[error("`cargo metadata` failed.\nstatus: {status}\nstdout: {stdout}\nstderr: {stderr}")]
29    CargoMetadata {
30        status: ExitStatus,
31        stdout: String,
32        stderr: String,
33    },
34    #[error("failed to parse `cargo metadata` output")]
35    CargoMetadataParse(#[from] serde_json::Error),
36    #[error("could not locate cargo package '{package}' in the source")]
37    CargoPackageNotFound { package: String },
38    #[error("failed to install binary '{file_name}'")]
39    InstallBinary {
40        file_name: String,
41        #[diagnostic_source]
42        source: install::Error,
43    },
44    #[error(transparent)]
45    #[diagnostic(transparent)]
46    Fs(#[from] fs::Error),
47}
48
49impl BuildBackend for RustBinaryBuildSpec {
50    type Err = Error;
51
52    #[tracing::instrument(name = "rust_binary::run", skip_all, level = "debug")]
53    async fn run<T>(self, args: RunBuildArgs<'_, T>) -> Result<BuildInfo, Self::Err>
54    where
55        T: InstallTree,
56    {
57        let config = args.config;
58        let build_dir = args.build_dir;
59
60        let mut install_args = vec!["install", "--root", ".", "--bins"];
61        if config.build_profile() == build::Profile::Dev {
62            install_args.push("--debug");
63        }
64        let features = self.features.join(",");
65        if !features.is_empty() {
66            install_args.push("--features");
67            install_args.push(&features);
68        }
69        let cargo_vendor_dir = config
70            .vendor_dir()
71            .map(|vendor_dir| vendor_dir.join("cargo"))
72            .filter(|dir| dir.is_dir());
73
74        let crate_dir = if let Some(package) = &self.package {
75            find_cargo_package_dir(config, build_dir, package)
76                .await?
77                .to_slash_lossy()
78                .to_string()
79        } else {
80            format!("{}", build_dir.display())
81        };
82
83        install_args.push("--path");
84        install_args.push(&crate_dir);
85
86        if let Some(cargo_vendor_dir) = &cargo_vendor_dir {
87            install_args.push("--offline");
88            cargo::prepare_vendor_config(config, build_dir, cargo_vendor_dir).await?;
89        }
90
91        match config
92            .wrapped_command("cargo", install_args)
93            .current_dir(build_dir)
94            .output()
95            .instrument(tracing::info_span!(
96                "Compiling rust binary",
97                profile = config.build_profile().to_string()
98            ))
99            .await
100        {
101            Ok(output) if output.status.success() => command::trace_output(&output),
102            Ok(output) => {
103                return Err(Error::CargoInstall {
104                    status: output.status,
105                    stdout: String::from_utf8_lossy(&output.stdout).into(),
106                    stderr: String::from_utf8_lossy(&output.stderr).into(),
107                });
108            }
109            Err(source) => return Err(Error::RustBuild { source }),
110        }
111
112        let mut binaries = Vec::new();
113        for bin_script in install::detect_binaries(build_dir) {
114            if let Some(target) = bin_script.file_name() {
115                let file_name = target.to_string_lossy().to_string();
116                let installed_bin_script = install::binary(
117                    &bin_script,
118                    &file_name,
119                    args.tree,
120                    args.lua,
121                    args.deploy,
122                    config,
123                )
124                .await
125                .map_err(|err| Error::InstallBinary {
126                    file_name: file_name.to_string(),
127                    source: err,
128                })?;
129                if let Some(bin_script_file_name) = installed_bin_script.file_name() {
130                    binaries.push(bin_script_file_name.into());
131                }
132            }
133        }
134
135        Ok(BuildInfo { binaries })
136    }
137}
138
139async fn find_cargo_package_dir(
140    config: &Config,
141    source_root: &Path,
142    package: &str,
143) -> Result<PathBuf, Error> {
144    let output = config
145        .wrapped_command("cargo", ["metadata", "--no-deps", "--format-version", "1"])
146        .current_dir(source_root)
147        .output()
148        .await
149        .map_err(|source| Error::RustBuild { source })?;
150    if !output.status.success() {
151        return Err(Error::CargoMetadata {
152            status: output.status,
153            stdout: String::from_utf8_lossy(&output.stdout).into(),
154            stderr: String::from_utf8_lossy(&output.stderr).into(),
155        });
156    }
157    let metadata: serde_json::Value = serde_json::from_slice(&output.stdout)?;
158    metadata
159        .get("packages")
160        .and_then(|packages| packages.as_array())
161        .and_then(|packages| {
162            packages
163                .iter()
164                .find(|pkg| pkg.get("name").and_then(|name| name.as_str()) == Some(package))
165        })
166        .and_then(|pkg| pkg.get("manifest_path").and_then(|path| path.as_str()))
167        .and_then(|path| Path::new(path).parent())
168        .map(Path::to_path_buf)
169        .ok_or_else(|| Error::CargoPackageNotFound {
170            package: package.to_string(),
171        })
172}