1use crate::project::ProjectRoot;
2use crate::toolchain::ToolchainSelection;
3use std::fs;
4use std::io;
5use std::path::{Path, PathBuf};
6use std::process::Command;
7
8use crate::cli::BuildMode;
9use crate::compiler::CompilerBackend;
10use crate::error::{DealerError, DealerResult};
11use crate::names::sanitize_package_name;
12
13struct BuildLayout {
14 dealer_dir: PathBuf,
15 rust_dir: PathBuf,
16 product_dir: PathBuf,
17 metadata_dir: PathBuf,
18 logs_dir: PathBuf,
19}
20
21impl BuildLayout {
22 fn for_project(project: &ProjectRoot) -> Self {
23 let dealer_dir = project.root_dir.join(".dealer");
24 Self {
25 rust_dir: dealer_dir.join("rust"),
26 product_dir: project.root_dir.join("product"),
27 metadata_dir: dealer_dir.join("metadata"),
28 logs_dir: dealer_dir.join("logs"),
29 dealer_dir,
30 }
31 }
32}
33
34pub(crate) fn run_check_or_exit(project: &ProjectRoot, toolchain: &ToolchainSelection) {
35 let compiler = toolchain.compiler_backend();
36 exit_on_error(run_check(project, &compiler));
37 println!("{}", crate::messages::check_passed());
38}
39
40pub(crate) fn run_build_or_exit(
41 project: &ProjectRoot,
42 toolchain: &ToolchainSelection,
43 mode: BuildMode,
44) {
45 let compiler = toolchain.compiler_backend();
46 match run_build(project, toolchain, mode, &compiler) {
47 Ok(product_path) => println!("Product written to {}", product_path.display()),
48 Err(error) => {
49 eprintln!("dealer: {error}");
50 std::process::exit(1);
51 }
52 }
53}
54
55pub(crate) fn run_project_or_exit(
56 project: &ProjectRoot,
57 toolchain: &ToolchainSelection,
58 mode: BuildMode,
59) {
60 let compiler = toolchain.compiler_backend();
61 match run_project(project, toolchain, mode, &compiler) {
62 Ok(status_code) => std::process::exit(status_code),
63 Err(error) => {
64 eprintln!("dealer: {error}");
65 std::process::exit(1);
66 }
67 }
68}
69
70pub(crate) fn run_clean_or_exit(project: &ProjectRoot) {
71 exit_on_error(run_clean(project));
72 println!(
73 "Removed dealer build state for {}",
74 project.root_dir.display()
75 );
76}
77
78fn exit_on_error(result: DealerResult<()>) {
79 if let Err(error) = result {
80 eprintln!("dealer: {error}");
81 std::process::exit(1);
82 }
83}
84
85fn run_check(project: &ProjectRoot, compiler: &dyn CompilerBackend) -> DealerResult<()> {
86 let deps = crate::project::resolve_dependencies(project, compiler).map_err(|error| {
87 DealerError::PackageResolution(format!("failed to resolve dependencies: {error}"))
88 })?;
89 compiler.check(&project.root_file, &deps)
90}
91
92fn run_build(
93 project: &ProjectRoot,
94 toolchain: &ToolchainSelection,
95 mode: BuildMode,
96 compiler: &dyn CompilerBackend,
97) -> DealerResult<PathBuf> {
98 let layout = BuildLayout::for_project(project);
99 prepare_build_folders(&layout).map_err(|error| DealerError::io(&layout.dealer_dir, error))?;
100
101 let deps = crate::project::resolve_dependencies(project, compiler).map_err(|error| {
102 DealerError::PackageResolution(format!("failed to resolve dependencies: {error}"))
103 })?;
104
105 compiler.build(
106 &project.root_file,
107 &deps,
108 &layout.rust_dir,
109 &project.package_name,
110 &toolchain.rusttime_path,
111 )?;
112
113 toolchain.backend.build(&layout.rust_dir, mode)?;
114
115 let is_executable = is_app_root(project);
116 let product_path = copy_product_artifact(
117 project,
118 &layout.rust_dir,
119 &layout.product_dir,
120 is_executable,
121 mode,
122 )?;
123
124 write_last_build_metadata(project, toolchain, &layout, &product_path)
125 .map_err(|error| DealerError::io(layout.metadata_dir.join("last-build.txt"), error))?;
126 Ok(product_path)
127}
128
129fn run_project(
130 project: &ProjectRoot,
131 toolchain: &ToolchainSelection,
132 mode: BuildMode,
133 compiler: &dyn CompilerBackend,
134) -> DealerResult<i32> {
135 if !is_app_root(project) {
136 return Err(DealerError::Backend(format!(
137 "dealer run requires app.x; '{}' is a package root",
138 project.root_file.display()
139 )));
140 }
141
142 let product_path = run_build(project, toolchain, mode, compiler)?;
143 let status = Command::new(&product_path)
144 .status()
145 .map_err(|error| DealerError::io(&product_path, error))?;
146 Ok(status.code().unwrap_or(1))
147}
148
149fn run_clean(project: &ProjectRoot) -> DealerResult<()> {
150 let layout = BuildLayout::for_project(project);
151 remove_dir_if_exists(&layout.dealer_dir)
152 .map_err(|error| DealerError::io(&layout.dealer_dir, error))?;
153 remove_dir_if_exists(&layout.product_dir)
154 .map_err(|error| DealerError::io(&layout.product_dir, error))?;
155 Ok(())
156}
157
158fn remove_dir_if_exists(path: &Path) -> io::Result<()> {
159 if path.exists() {
160 fs::remove_dir_all(path)?;
161 }
162 Ok(())
163}
164
165fn is_app_root(project: &ProjectRoot) -> bool {
166 project.root_file.file_name().and_then(|n| n.to_str()) == Some("app.x")
167}
168
169fn prepare_build_folders(layout: &BuildLayout) -> io::Result<()> {
170 fs::create_dir_all(&layout.dealer_dir)?;
171 fs::create_dir_all(&layout.rust_dir)?;
172 fs::create_dir_all(&layout.product_dir)?;
173 fs::create_dir_all(&layout.metadata_dir)?;
174 fs::create_dir_all(&layout.logs_dir)?;
175 Ok(())
176}
177
178fn copy_product_artifact(
179 project: &ProjectRoot,
180 rust_dir: &Path,
181 product_dir: &Path,
182 is_executable: bool,
183 mode: BuildMode,
184) -> DealerResult<PathBuf> {
185 let package_name = sanitize_package_name(&project.package_name);
186 let profile = match mode {
187 BuildMode::Dev => "debug",
188 BuildMode::Prod => "release",
189 };
190 let artifact = if is_executable {
191 rust_dir
192 .join("target")
193 .join(profile)
194 .join(format!("{package_name}{}", std::env::consts::EXE_SUFFIX))
195 } else {
196 rust_dir
197 .join("target")
198 .join(profile)
199 .join(format!("lib{package_name}.rlib"))
200 };
201
202 if !artifact.is_file() {
203 return Err(DealerError::Backend(format!(
204 "dealer: expected backend artifact '{}' was not produced",
205 artifact.display()
206 )));
207 }
208
209 let product_name = artifact.file_name().unwrap_or_default();
210 let product_path = product_dir.join(product_name);
211 copy_file(&artifact, &product_path).map_err(|error| {
212 DealerError::Backend(format!(
213 "dealer: failed to copy product artifact to '{}': {error}",
214 product_path.display()
215 ))
216 })?;
217 Ok(product_path)
218}
219
220fn copy_file(from: &Path, to: &Path) -> io::Result<()> {
221 if to.exists() {
222 fs::remove_file(to)?;
223 }
224 fs::copy(from, to)?;
225 Ok(())
226}
227
228fn write_last_build_metadata(
229 project: &ProjectRoot,
230 toolchain: &ToolchainSelection,
231 layout: &BuildLayout,
232 product_path: &Path,
233) -> io::Result<()> {
234 let metadata = format!(
235 "project_root={}\nroot_file={}\nrust_dir={}\nproduct_path={}\ndealer_home={}\ntoolchain_version={}\ntoolchain_dir={}\ncompiler_source={}\npiko_path={}\nrust_backend_id={}\nrust_backend_dir={}\nbackend_source={}\ncargo_path={}\nrusttime_source={}\nrusttime_path={}\n",
236 project.root_dir.display(),
237 project.root_file.display(),
238 layout.rust_dir.display(),
239 product_path.display(),
240 toolchain.dealer_home.display(),
241 toolchain.version,
242 toolchain.toolchain_dir.display(),
243 toolchain.piko_source.as_metadata_value(),
244 toolchain.piko_path.display(),
245 toolchain.rust_backend_id,
246 toolchain.rust_backend_dir.display(),
247 toolchain.backend_source().as_metadata_value(),
248 toolchain.backend.cargo_path.display(),
249 toolchain.rusttime_source.as_metadata_value(),
250 toolchain.rusttime_path.display(),
251 );
252 fs::write(layout.metadata_dir.join("last-build.txt"), metadata)
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use crate::compiler::{ProjectMetadata, StaticCompilerBackend};
259 use crate::project::validate_project_root;
260 use crate::test_support::TempProject;
261 use crate::toolchain::{ToolchainEnv, ToolchainSelection};
262 use std::collections::HashMap;
263 use std::process::Command;
264
265 #[test]
266 fn build_writes_generated_rust_under_dealer_and_final_artifact_under_product() {
267 let temp = TempProject::new("build-layout");
268 fs::write(
269 temp.path().join("app.x"),
270 "app DealerSmoke\n\tterminal.log\n\t\tmessage: \"dealer ok\"\n",
271 )
272 .expect("app root should be written");
273 let project = validate_project_root(temp.path()).expect("app.x root should be valid");
274 let workspace_root = crate::workspace_root();
275 let toolchain = ToolchainSelection::discover(
276 &workspace_root,
277 &ToolchainEnv::for_test(Some(temp.path().join("dealer-home")), None),
278 );
279
280 let compiler = StaticCompilerBackend {
281 metadata: HashMap::from([(
282 project.root_file.clone(),
283 ProjectMetadata {
284 project_type: "app".to_string(),
285 name: "DealerSmoke".to_string(),
286 dependencies: Vec::new(),
287 },
288 )]),
289 };
290
291 let product_path =
292 run_build(&project, &toolchain, BuildMode::Dev, &compiler).expect("build should pass");
293
294 assert!(temp.path().join(".dealer/rust/Cargo.toml").is_file());
295 assert!(temp.path().join(".dealer/rust/src/main.rs").is_file());
296 assert!(
297 temp.path()
298 .join(".dealer/metadata/last-build.txt")
299 .is_file()
300 );
301 assert!(temp.path().join(".dealer/logs").is_dir());
302 assert!(
303 !temp.path().join("Cargo.toml").exists(),
304 "dealer must not write generated Cargo files next to app.x"
305 );
306
307 let metadata = fs::read_to_string(temp.path().join(".dealer/metadata/last-build.txt"))
308 .expect("build metadata should be readable");
309 assert!(metadata.contains("backend_source=development_fallback"));
310 assert!(metadata.contains("rusttime_source=development_fallback"));
311 assert!(metadata.contains("toolchain_version=0.1.0"));
312
313 assert!(product_path.is_file());
314
315 let output = Command::new(product_path)
316 .output()
317 .expect("product artifact should run");
318 assert!(
319 output.status.success(),
320 "expected product artifact to run successfully"
321 );
322 assert_eq!(String::from_utf8_lossy(&output.stdout), "");
323 }
324
325 #[test]
326 fn clean_removes_dealer_and_product_dirs() {
327 let temp = TempProject::new("clean-layout");
328 fs::write(temp.path().join("app.x"), "app CleanSmoke\n")
329 .expect("app root should be written");
330 fs::create_dir_all(temp.path().join(".dealer/rust")).expect("dealer dir should be made");
331 fs::create_dir_all(temp.path().join("product")).expect("product dir should be made");
332 let project = validate_project_root(temp.path()).expect("app.x root should be valid");
333
334 run_clean(&project).expect("clean should pass");
335
336 assert!(!temp.path().join(".dealer").exists());
337 assert!(!temp.path().join("product").exists());
338 assert!(temp.path().join("app.x").is_file());
339 }
340
341 #[test]
342 fn run_project_builds_and_executes_app_artifact() {
343 let temp = TempProject::new("run-app");
344 fs::write(temp.path().join("app.x"), "app RunSmoke\n").expect("app root should be written");
345 let project = validate_project_root(temp.path()).expect("app.x root should be valid");
346 let workspace_root = crate::workspace_root();
347 let toolchain = ToolchainSelection::discover(
348 &workspace_root,
349 &ToolchainEnv::for_test(Some(temp.path().join("dealer-home")), None),
350 );
351 let compiler = StaticCompilerBackend {
352 metadata: HashMap::from([(
353 project.root_file.clone(),
354 ProjectMetadata {
355 project_type: "app".to_string(),
356 name: "RunSmoke".to_string(),
357 dependencies: Vec::new(),
358 },
359 )]),
360 };
361
362 let status =
363 run_project(&project, &toolchain, BuildMode::Dev, &compiler).expect("run should pass");
364
365 assert_eq!(status, 0);
366 }
367
368 #[test]
369 fn run_project_rejects_package_root() {
370 let temp = TempProject::new("run-package");
371 fs::write(temp.path().join("package.x"), "package Lib\n")
372 .expect("package root should be written");
373 let project = validate_project_root(temp.path()).expect("package.x root should be valid");
374 let workspace_root = crate::workspace_root();
375 let toolchain = ToolchainSelection::discover(
376 &workspace_root,
377 &ToolchainEnv::for_test(Some(temp.path().join("dealer-home")), None),
378 );
379 let compiler = StaticCompilerBackend {
380 metadata: HashMap::new(),
381 };
382
383 let error = run_project(&project, &toolchain, BuildMode::Dev, &compiler)
384 .expect_err("package run should fail");
385
386 assert!(error.to_string().contains("requires app.x"));
387 }
388}