1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
//! The `build` subcommand and the shared build-command runner.
//!
//! Today building is delegated to an external command (`npm run build`, vite,
//! webpack, esbuild, ...), which makes boatramp framework-agnostic and lets it
//! act as the deploy step of any existing toolchain. The `bundler` cargo
//! feature is reserved for a future in-process Rust bundler.
use std::path::PathBuf;
use crate::config::ProjectConfig;
/// A failure in the `build` / `validate` subcommands.
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// No build command was configured and none was passed.
#[error("no build command configured; set build.command in project.cfg or pass --command")]
NoBuildCommand,
/// Reading the project config to validate failed.
#[error("reading {path}: {source}")]
Read {
path: String,
#[source]
source: std::io::Error,
},
/// Parsing or compile-checking the project config failed.
#[error("{path}: {source}")]
Config {
path: String,
#[source]
source: crate::config::ConfigError,
},
/// The external build command exited non-zero.
#[error("build command failed: `{command}` exited with {status}")]
CommandFailed {
command: String,
status: std::process::ExitStatus,
},
/// Spawning or running the build command failed.
#[error(transparent)]
Io(#[from] std::io::Error),
}
/// `build` module result; `Err` is [`Error`].
type Result<T> = std::result::Result<T, Error>;
/// The result of `boatramp build`: the external build command that ran.
///
/// `build` itself emits no human text — the build tool it shells out to prints its
/// own output on the inherited stdio — so [`render_human`](crate::output::Rendered::render_human)
/// writes nothing. JSON callers still get the command that was executed.
#[derive(Debug, serde::Serialize)]
pub struct BuildOutput {
/// The build command that was run (e.g. `npm run build`).
command: String,
}
impl crate::output::Rendered for BuildOutput {
fn render_human(&self, _w: &mut dyn std::io::Write) -> std::io::Result<()> {
// `build` historically printed nothing of its own; the build tool's output
// goes to the inherited stdio. Preserve that: no extra line.
Ok(())
}
}
/// The result of `boatramp validate`: a parsed-and-compile-checked `project.cfg`
/// summary (the counts of each routing surface). Rendered by [`crate::output`].
#[derive(Debug, serde::Serialize)]
pub struct ValidateOutput {
/// The config file that was validated.
path: String,
redirects: usize,
rewrites: usize,
headers: usize,
handlers: usize,
consumers: usize,
crons: usize,
streams: usize,
}
impl crate::output::Rendered for ValidateOutput {
fn render_human(&self, w: &mut dyn std::io::Write) -> std::io::Result<()> {
writeln!(
w,
"{} ok — {} redirect(s), {} rewrite(s), {} header rule(s)",
self.path, self.redirects, self.rewrites, self.headers,
)?;
if self.handlers > 0 || self.consumers > 0 || self.crons > 0 || self.streams > 0 {
writeln!(
w,
" {} handler(s), {} consumer(s), {} cron(s), {} stream(s)",
self.handlers, self.consumers, self.crons, self.streams,
)?;
}
Ok(())
}
}
/// Arguments for `boatramp build`.
#[derive(Debug, clap::Args)]
pub struct BuildArgs {
/// Override the configured build command.
#[arg(long)]
command: Option<String>,
}
/// Entry point for `boatramp build`.
pub async fn run(args: BuildArgs, config: &ProjectConfig) -> Result<BuildOutput> {
let command = resolve_command(args.command, config)?;
run_command(&command).await?;
Ok(BuildOutput { command })
}
/// Determine which build command to run, preferring an explicit override.
pub fn resolve_command(override_command: Option<String>, config: &ProjectConfig) -> Result<String> {
override_command
.or_else(|| config.build.as_ref().map(|build| build.command.clone()))
.ok_or(Error::NoBuildCommand)
}
/// Arguments for `boatramp validate`.
#[derive(Debug, clap::Args)]
pub struct ValidateArgs {
/// Path to the project config to validate.
#[arg(default_value = "project.cfg")]
path: PathBuf,
}
/// Entry point for `boatramp validate`: parse and compile-check a `project.cfg`
/// (its `routing` section).
pub fn validate(args: ValidateArgs) -> Result<ValidateOutput> {
let text = std::fs::read_to_string(&args.path).map_err(|source| Error::Read {
path: args.path.display().to_string(),
source,
})?;
let project = ProjectConfig::parse(&text).map_err(|source| Error::Config {
path: args.path.display().to_string(),
source,
})?;
let config = &project.routing;
Ok(ValidateOutput {
path: args.path.display().to_string(),
redirects: config.redirects.len(),
rewrites: config.rewrites.len(),
headers: config.headers.len(),
handlers: config.handlers.len(),
consumers: config.consumers.len(),
crons: config.crons.len(),
streams: config.streams.len(),
})
}
/// Run `command` via the system shell, failing on a non-zero exit.
pub async fn run_command(command: &str) -> Result<()> {
tracing::info!(%command, "running build command");
let status = tokio::process::Command::new("sh")
.arg("-c")
.arg(command)
.status()
.await?;
if !status.success() {
return Err(Error::CommandFailed {
command: command.to_string(),
status,
});
}
Ok(())
}