Skip to main content

smbcloud_deploy/
build.rs

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
9use crate::{error::DeployError, report::Reporter};
10use anyhow::anyhow;
11use std::{
12    path::{Path, PathBuf},
13    process::Command,
14};
15
16/// What a build produced and where to ship it from.
17pub 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`].
23pub 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`).
29pub struct ViteSpaBuild {
30    pub project_path: String,
31    pub output_dir: String,
32    pub package_manager: String,
33}
34
35impl 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}