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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
//! Build system & asset pipelines.
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{Context, Result};
use futures_util::stream::StreamExt;
use tokio::fs;
use tokio::sync::mpsc;
use tokio_stream::wrappers::ReadDirStream;
use crate::common::{remove_dir_all, BUILDING, ERROR, SUCCESS};
use crate::config::{rt::RtcBuild, types::WsProtocol, STAGE_DIR};
use crate::pipelines::HtmlPipeline;
pub type BuildResult = Result<()>;
/// A system used for building a Rust WASM app & bundling its assets.
///
/// This unit of data should be used throughout the system for driving build processes and
/// bundling tasks. Different CLI commands which need to trigger builds in some way should
/// be able to gather the needed data to create an instance of this struct, and then the various
/// build routines can be cleanly abstracted away form any specific CLI endpoints.
pub struct BuildSystem {
/// Runtime config.
cfg: Arc<RtcBuild>,
/// HTML build pipeline.
html_pipeline: Arc<HtmlPipeline>,
}
impl BuildSystem {
/// Create a new instance from the raw components.
///
/// Reducing the number of assumptions here should help us to stay flexible when adding new
/// commands, refactoring and the like.
pub async fn new(
cfg: Arc<RtcBuild>,
ignore_chan: Option<mpsc::Sender<PathBuf>>,
ws_protocol: Option<WsProtocol>,
) -> Result<Self> {
let html_pipeline = Arc::new(HtmlPipeline::new(cfg.clone(), ignore_chan, ws_protocol)?);
Ok(Self { cfg, html_pipeline })
}
/// Build the application described in the given build data.
#[tracing::instrument(level = "trace", skip(self))]
pub async fn build(&mut self) -> Result<()> {
tracing::info!("{}starting build", BUILDING);
let res = self.do_build().await;
match res {
Ok(_) => {
tracing::info!("{}success", SUCCESS);
Ok(())
}
Err(err) => {
tracing::error!("{}error\n{:?}", ERROR, err);
Err(err)
}
}
}
/// Internal business logic of `build`.
async fn do_build(&mut self) -> Result<()> {
// Ensure the output dist directories are in place.
fs::create_dir_all(self.cfg.final_dist.as_path())
.await
.with_context(|| {
format!(
"error creating build environment directory: {}",
self.cfg.final_dist.display()
)
})?;
self.prepare_staging_dist()
.await
.context("error preparing build environment")?;
// Spawn the source HTML pipeline. This will spawn all other pipelines derived from
// the source HTML, and will ultimately generate and write the final HTML.
self.html_pipeline
.clone()
.spawn()
.await
.context("error joining HTML pipeline")?
// we name if "build" pipeline here, was that's what it has become, and
// what makes more sense to the user
.context("error from build pipeline")?;
// Move distribution from staging dist to final dist
self.finalize_dist()
.await
.context("error applying built distribution")?;
Ok(())
}
/// Creates a "staging area" (dist/.stage) for storing intermediate build results.
async fn prepare_staging_dist(&self) -> Result<()> {
// Prepare staging area in which we will assemble the latest build
let staging_dist = self.cfg.staging_dist.as_path();
// Clean staging area, if applicable
remove_dir_all(staging_dist.into()).await.with_context(|| {
format!(
"error cleaning staging dist dir: {}",
staging_dist.display()
)
})?;
fs::create_dir_all(staging_dist).await.with_context(|| {
format!(
"error creating build environment directory: {}",
staging_dist.display()
)
})?;
Ok(())
}
/// Moves the contents of dist/.stage into dist, signifying the application
/// of a successful build. Also removes dist/.stage afterwards.
#[tracing::instrument(level = "trace", skip(self))]
async fn finalize_dist(&self) -> Result<()> {
let staging_dist = self.cfg.staging_dist.clone();
tracing::info!("applying new distribution");
// Build succeeded, so delete everything in `dist`, move everything
// from `dist/.stage` to `dist`, and then delete `dist/.stage`.
self.clean_final().await?;
self.move_stage_to_final().await?;
fs::remove_dir(staging_dist)
.await
.context("error deleting staging dist dir")?;
Ok(())
}
/// Move contents of stage dir to final dist dir.
async fn move_stage_to_final(&self) -> Result<()> {
let final_dist = self.cfg.final_dist.clone();
let staging_dist = self.cfg.staging_dist.clone();
let mut entries = fs::read_dir(&staging_dist)
.await
.map(ReadDirStream::new)
.context("error reading staging dist dir")?;
while let Some(entry) = entries.next().await {
let entry = entry.context("error reading contents of staging dist dir")?;
let target_path = final_dist.join(entry.file_name());
fs::rename(entry.path(), &target_path)
.await
.with_context(|| {
format!("error moving {:?} to {:?}", &entry.path(), &target_path)
})?;
}
Ok(())
}
/// Clean the contents of the final dist dir.
async fn clean_final(&self) -> Result<()> {
let final_dist = self.cfg.final_dist.clone();
let mut entries = fs::read_dir(&final_dist)
.await
.map(ReadDirStream::new)
.context("error reading final dist dir")?;
while let Some(entry) = entries.next().await {
let entry = entry.context("error reading contents of final dist dir")?;
if entry.file_name() == STAGE_DIR {
continue;
}
let file_type = entry
.file_type()
.await
.context("error reading metadata of file in final dist dir")?;
if file_type.is_dir() {
remove_dir_all(entry.path())
.await
.context("error cleaning final dist")?;
} else if file_type.is_symlink() || file_type.is_file() {
fs::remove_file(entry.path())
.await
.context("error cleaning final dist")?;
}
}
Ok(())
}
}