lux_lib/build/
rust_binary.rs1use 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}