1use std::path::{Path, PathBuf};
2use std::process::Command;
3
4use crate::cli::BuildMode;
5use crate::error::{DealerError, DealerResult, process_output_message};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub(crate) enum ToolSource {
9 PinnedToolchain,
10 DevelopmentFallback,
11}
12
13impl ToolSource {
14 pub(crate) fn as_metadata_value(self) -> &'static str {
15 match self {
16 Self::PinnedToolchain => "pinned_toolchain",
17 Self::DevelopmentFallback => "development_fallback",
18 }
19 }
20
21 fn label(self) -> &'static str {
22 match self {
23 Self::PinnedToolchain => "pinned Xtazy toolchain",
24 Self::DevelopmentFallback => "development fallback",
25 }
26 }
27}
28
29#[derive(Debug, Clone)]
30pub(crate) struct RustBackend {
31 pub(crate) cargo_path: PathBuf,
32 pub(crate) source: ToolSource,
33}
34
35impl RustBackend {
36 pub(crate) fn pinned_toolchain(path: impl Into<PathBuf>) -> Self {
37 Self {
38 cargo_path: path.into(),
39 source: ToolSource::PinnedToolchain,
40 }
41 }
42
43 pub(crate) fn development_system() -> Self {
44 Self::from_cargo_path("cargo")
45 }
46
47 pub(crate) fn from_cargo_path(path: impl Into<PathBuf>) -> Self {
48 Self {
49 cargo_path: path.into(),
50 source: ToolSource::DevelopmentFallback,
51 }
52 }
53
54 pub(crate) fn build(&self, rust_dir: &Path, mode: BuildMode) -> DealerResult<()> {
55 let mut command = Command::new(&self.cargo_path);
56 command.args(["build", "--quiet"]);
57 if mode == BuildMode::Prod {
58 command.arg("--release");
59 }
60
61 let output = command.current_dir(rust_dir).output().map_err(|error| {
62 DealerError::Backend(format!(
63 "failed to start {} Cargo backend '{}' in '{}': {error}",
64 self.source.label(),
65 self.cargo_path.display(),
66 rust_dir.display()
67 ))
68 })?;
69
70 if output.status.success() {
71 return Ok(());
72 }
73
74 Err(DealerError::Backend(process_output_message(
75 output,
76 &format!("{} Cargo backend", self.source.label()),
77 )))
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use crate::test_support::TempProject;
85 use std::fs;
86
87 #[test]
88 fn development_backend_is_explicit_system_cargo_fallback() {
89 let backend = RustBackend::development_system();
90
91 assert_eq!(backend.cargo_path, PathBuf::from("cargo"));
92 assert_eq!(backend.source, ToolSource::DevelopmentFallback);
93 }
94
95 #[test]
96 fn backend_reports_missing_cargo_executable() {
97 let temp = TempProject::new("missing-cargo-backend");
98 let backend = RustBackend::from_cargo_path(temp.path().join("missing-cargo"));
99
100 let error = backend
101 .build(temp.path(), BuildMode::Dev)
102 .expect_err("missing cargo executable should fail");
103
104 let message = error.to_string();
105 assert!(message.contains("failed to start development fallback Cargo backend"));
106 assert!(message.contains("missing-cargo"));
107 }
108
109 #[test]
110 fn backend_reports_rust_build_failure_output() {
111 let temp = TempProject::new("cargo-build-failure");
112 fs::create_dir_all(temp.path().join("src")).expect("src dir should be created");
113 fs::write(
114 temp.path().join("Cargo.toml"),
115 "[workspace]\n\n[package]\nname = \"dealer_backend_failure\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
116 )
117 .expect("Cargo.toml should be written");
118 fs::write(temp.path().join("src/main.rs"), "fn main() { let = ; }\n")
119 .expect("invalid main.rs should be written");
120
121 let error = RustBackend::development_system()
122 .build(temp.path(), BuildMode::Dev)
123 .expect_err("invalid generated Rust should fail");
124 let message = error.to_string();
125
126 assert!(
127 message.contains("dealer_backend_failure"),
128 "expected Cargo/Rust error output, got:\n{}",
129 error
130 );
131 }
132}