1 //! Local build strategies.
2 //!
3 //! A [`BuildStrategy`] runs a project's build locally and reports where the
4 //! shippable output is, as a [`BuildArtifact`]. Transport (see
5 //! [`crate::transport`]) takes it from there. Keeping "build" and "ship"
6 //! separate is what lets the same engine drive a local build, a CI build, or a
7 //! server-side build without the strategies knowing which.
8
9 use crate::{error::DeployError, report::Reporter};
10 use anyhow::anyhow;
11 use std::{
12 path::{Path, PathBuf},
13 process::Command,
14 };
15
16 /// What a build produced and where to ship it from.
17 pub struct BuildArtifact {
18 /// Local directory whose contents should be shipped.
19 pub source_dir: PathBuf,
20 }
21
22 /// Builds a project locally into a [`BuildArtifact`].
23 pub trait BuildStrategy {
24 fn build(&self, reporter: &dyn Reporter) -> Result<BuildArtifact, DeployError>;
25 }
26
27 /// A Vite / SPA build: run `<package_manager> build` in the project directory
28 /// and ship its output directory (e.g. `dist`).
29 pub struct ViteSpaBuild {
30 pub project_path: String,
31 pub output_dir: String,
32 pub package_manager: String,
33 }
34
35 impl BuildStrategy for ViteSpaBuild {
36 fn build(&self, reporter: &dyn Reporter) -> Result<BuildArtifact, DeployError> {
37 reporter.step_start(&format!(
38 "Building {} with {}…",
39 self.project_path, self.package_manager
40 ));
41
42 // Validate up front: `current_dir` on a missing path only surfaces as a
43 // confusing spawn error at status() time.
44 let project_dir = Path::new(&self.project_path);
45 if !project_dir.exists() {
46 reporter.step_fail(&format!(
47 "Project path '{}' does not exist.",
48 self.project_path
49 ));
50 return Err(DeployError::Other(anyhow!(
51 "Project path '{}' does not exist. Check the 'source' field in .smb/config.toml.",
52 self.project_path
53 )));
54 }
55
56 let status = Command::new(&self.package_manager)
57 .arg("build")
58 .current_dir(&self.project_path)
59 .status()
60 .map_err(|e| {
61 reporter.step_fail(&format!("Failed to spawn '{}': {e}", self.package_manager));
62 DeployError::Other(anyhow!("Failed to spawn '{}': {e}", self.package_manager))
63 })?;
64
65 if !status.success() {
66 reporter.step_fail("Build failed. See output above.");
67 return Err(DeployError::Other(anyhow!(
68 "'{} build' exited with status {status}",
69 self.package_manager
70 )));
71 }
72
73 reporter.step_done("Build complete.");
74 Ok(BuildArtifact {
75 source_dir: project_dir.join(&self.output_dir),
76 })
77 }
78 }