use std::path::{Path, PathBuf};
use std::pin::pin;
use std::process::Stdio;
use std::sync::Arc;
use anyhow::{bail, Context, Result};
use cargo_metadata::Metadata;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
use tokio::sync::OnceCell;
use tokio::{fs, spawn};
use crate::env_file::EnvFile;
use crate::manifest::Manifest;
use crate::paths::Paths;
use crate::profile::Profile;
use crate::utils::random_str;
use crate::Stctl;
#[derive(Debug)]
pub(crate) struct Builder {
build_id: String,
paths: Arc<Paths>,
profile: Profile,
env_file: EnvFile,
manifest: Arc<Manifest>,
is_watch_build: bool,
frontend_build_dir: OnceCell<PathBuf>,
backend_build_dir: OnceCell<PathBuf>,
backend_target: Option<String>,
}
impl Builder {
pub async fn new(stctl: &Stctl) -> Result<Self> {
Ok(Builder {
build_id: random_str()?,
paths: stctl.paths.clone(),
profile: stctl.profile.clone(),
env_file: stctl.env_file.clone(),
manifest: stctl.manifest.clone(),
is_watch_build: false,
frontend_build_dir: OnceCell::new(),
backend_build_dir: OnceCell::new(),
backend_target: None,
})
}
pub fn watch_build(mut self, is_watch_build: bool) -> Self {
self.is_watch_build = is_watch_build;
self
}
pub fn backend_target(mut self, backend_target: Option<String>) -> Self {
self.backend_target = backend_target;
self
}
pub async fn frontend_build_dir(&self) -> Result<&Path> {
self.frontend_build_dir
.get_or_try_init(|| async {
let frontend_build_dir =
self.paths.frontend_builds_dir().await?.join(&self.build_id);
fs::create_dir_all(&frontend_build_dir)
.await
.context("failed to create build directory for frontend build.")?;
Ok(frontend_build_dir)
})
.await
.map(|m| m.as_ref())
}
pub async fn backend_build_dir(&self) -> Result<&Path> {
self.backend_build_dir
.get_or_try_init(|| async {
let backend_build_dir = self.paths.backend_builds_dir().await?.join(&self.build_id);
fs::create_dir_all(&backend_build_dir)
.await
.context("failed to create build directory for backend build.")?;
Ok(backend_build_dir)
})
.await
.map(|m| m.as_ref())
}
async fn transfer_to_file<R, P>(source: R, target: P) -> Result<()>
where
R: 'static + AsyncRead + Send,
P: Into<PathBuf>,
{
let target_path = target.into();
let mut target = fs::File::create(&target_path)
.await
.with_context(|| format!("failed to create {}", target_path.display()))?;
let inner = async move {
let mut source = pin!(source);
loop {
let mut buf = [0_u8; 8192];
let buf_len = source.read(&mut buf[..]).await?;
if buf_len == 0 {
break;
}
target.write_all(&buf[..buf_len]).await?;
}
Ok::<(), anyhow::Error>(())
};
spawn(async move {
if let Err(e) = inner
.await
.with_context(|| format!("failed to transfer logs to: {}", target_path.display()))
{
tracing::error!("{:#?}", e);
}
});
Ok(())
}
pub async fn build_frontend(&self) -> Result<&Path> {
use tokio::process::Command;
let frontend_logs_dir = self.paths.frontend_logs_dir().await?;
let frontend_build_dir = self.frontend_build_dir().await?;
let workspace_dir = self.paths.workspace_dir().await?;
let create_proc = || {
let mut proc = Command::new("trunk");
proc.arg("build")
.arg("--dist")
.arg(frontend_build_dir)
.arg(workspace_dir.join("index.html"))
.current_dir(workspace_dir)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if let Some(m) = self.profile.to_profile_argument() {
proc.arg(m);
}
let envs = self.env_file.load(workspace_dir);
proc.envs(envs);
if !self.is_watch_build {
proc.stdout(Stdio::inherit()).stderr(Stdio::inherit());
}
proc
};
let mut child = create_proc().spawn()?;
if let Some(m) = child.stdout.take() {
Self::transfer_to_file(
m,
frontend_logs_dir.join(format!("log-stdout-{}", self.build_id)),
)
.await?;
}
if let Some(m) = child.stderr.take() {
Self::transfer_to_file(
m,
frontend_logs_dir.join(format!("log-stderr-{}", self.build_id)),
)
.await?;
}
let status = child.wait().await?;
if !status.success() {
if !self.is_watch_build {
bail!("trunk failed with status {}", status);
}
let mut proc = create_proc();
proc.stdout(Stdio::inherit()).stderr(Stdio::inherit());
let mut child = proc.spawn()?;
let status = child.wait().await?;
if !status.success() {
bail!("trunk failed with status {}", status);
}
}
Ok(frontend_build_dir)
}
pub async fn build_backend(&self) -> Result<PathBuf> {
use tokio::process::Command;
let frontend_build_dir = self.frontend_build_dir().await?;
let backend_logs_dir = self.paths.backend_logs_dir().await?;
let workspace_dir = self.paths.workspace_dir().await?;
let backend_build_dir = self.backend_build_dir().await?;
let create_proc = || {
let mut proc = Command::new("cargo");
proc.arg("build")
.arg("--bin")
.arg(&self.manifest.dev_server.bin_name)
.current_dir(workspace_dir)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true);
if let Some(m) = self.profile.to_profile_argument() {
proc.arg(m);
}
if let Some(ref m) = self.backend_target {
proc.arg(format!("--target={}", m));
}
let envs = self.env_file.load(workspace_dir);
proc.envs(envs);
if !self.is_watch_build {
proc.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.env("RUSTFLAGS", "--cfg stellation_embedded_frontend");
}
proc.env("STELLATION_FRONTEND_BUILD_DIR", frontend_build_dir);
proc
};
let mut child = create_proc().spawn()?;
if let Some(m) = child.stdout.take() {
Self::transfer_to_file(
m,
backend_logs_dir.join(format!("log-stdout-{}", self.build_id)),
)
.await?;
}
if let Some(m) = child.stderr.take() {
Self::transfer_to_file(
m,
backend_logs_dir.join(format!("log-stderr-{}", self.build_id)),
)
.await?;
}
let status = child.wait().await?;
if !status.success() {
if !self.is_watch_build {
bail!("trunk failed with status {}", status);
}
let mut proc = create_proc();
proc.stdout(Stdio::inherit()).stderr(Stdio::inherit());
let mut child = proc.spawn()?;
let status = child.wait().await?;
if !status.success() {
bail!("trunk failed with status {}", status);
}
}
let pkg_meta_output = Command::new("cargo")
.arg("metadata")
.arg("--format-version=1")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.current_dir(workspace_dir)
.spawn()?
.wait_with_output()
.await
.context("failed to read package metadata")?;
if !pkg_meta_output.status.success() {
bail!(
"cargo metadata failed with status {}",
pkg_meta_output.status
);
}
let meta: Metadata = serde_json::from_slice(&pkg_meta_output.stdout)
.context("failed to parse package metadata")?;
let mut bin_path = meta.target_directory.into_std_path_buf();
if let Some(ref m) = self.backend_target {
bin_path = bin_path.join(m);
}
bin_path = bin_path
.join(self.profile.name())
.join(&self.manifest.dev_server.bin_name);
let backend_bin_path = backend_build_dir.join(&self.manifest.dev_server.bin_name);
fs::copy(bin_path, &backend_bin_path)
.await
.context("failed to copy binary")?;
Ok(backend_bin_path)
}
}