use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{Context, Result, anyhow};
use crate::models::common::{enums::Channel, version::Version};
use crate::providers::provider_manager::ProviderManager;
use crate::services::builder::determine::determine_profile;
use crate::services::builder::downloader::SourceDownloader;
use crate::services::builder::profiles::handlers;
use crate::services::builder::{BuildOutput, BuildRequest, scripts};
pub struct BuildWorker<'a> {
provider_manager: &'a ProviderManager,
}
impl<'a> BuildWorker<'a> {
pub fn new(provider_manager: &'a ProviderManager) -> Self {
Self { provider_manager }
}
pub async fn build<H>(
&self,
request: BuildRequest,
channel: Channel,
line_callback: &mut Option<H>,
) -> Result<BuildOutput>
where
H: FnMut(&str),
{
Self::emit_status(line_callback, "Preparing source download ...");
let downloader = SourceDownloader::new(self.provider_manager)?;
let source = {
let mut status_callback = line_callback
.as_mut()
.map(|callback| callback as &mut dyn FnMut(&str));
downloader
.fetch_source(
&request.repo_slug,
&request.provider,
request.base_url.as_deref(),
&channel,
request.version_tag.as_deref(),
request.branch.as_deref(),
&mut status_callback,
)
.await?
};
Self::emit_status(line_callback, "Detecting build profile ...");
let profile_handlers = handlers();
let profile = determine_profile(
&source.workspace_path,
request.requested_profile,
&profile_handlers,
)
.map_err(|err| anyhow!("{} (workspace: '{}')", err, source.workspace_path.display()))?;
Self::emit_status(
line_callback,
format!("Building with {profile:?} profile ..."),
);
let (build_tx, mut build_rx) = tokio::sync::mpsc::unbounded_channel();
let workspace_path = source.workspace_path.clone();
let package_name = request.name.clone();
let output_override = request.build_output.clone();
let mut build_handle = tokio::task::spawn_blocking(move || {
let handlers = handlers();
let selected = handlers
.iter()
.find(|handler| handler.profile() == profile)
.ok_or_else(|| anyhow!("Unsupported build profile"))?;
let mut sender_callback = |line: &str| {
let _ = build_tx.send(line.to_string());
};
let mut build_line_callback: Option<&mut dyn FnMut(&str)> = Some(&mut sender_callback);
selected.run_build(
&workspace_path,
&package_name,
output_override.as_deref(),
&mut build_line_callback,
)
});
let artifact = loop {
tokio::select! {
Some(line) = build_rx.recv() => {
Self::emit_status(line_callback, line);
}
result = &mut build_handle => {
while let Ok(line) = build_rx.try_recv() {
Self::emit_status(line_callback, line);
}
break result.context("Build task failed")??;
}
}
};
if scripts::script_for(request.script_action, &source.workspace_path).is_some() {
Self::emit_status(line_callback, "Running build scripts ...");
let build_script_callback = line_callback
.as_mut()
.map(|callback| callback as &mut dyn FnMut(&str));
scripts::run_build_script(
request.script_action,
&source.workspace_path,
build_script_callback,
)?;
}
Self::emit_status(line_callback, "Staging built artifact ...");
let persisted_artifact = Self::persist_artifact(&artifact)?;
let version = if source.release.version == Version::new(0, 0, 0, false) {
Version::from_tag(&source.release.tag).unwrap_or_else(|_| Version::new(0, 0, 0, false))
} else {
source.release.version.clone()
};
Ok(BuildOutput {
artifact_path: persisted_artifact,
profile,
release: source.release,
version,
branch: source.branch,
commit: source.commit,
})
}
fn emit_status<H>(line_callback: &mut Option<H>, status: impl AsRef<str>)
where
H: FnMut(&str),
{
if let Some(callback) = line_callback.as_mut() {
callback(status.as_ref());
}
}
fn persist_artifact(artifact_path: &Path) -> Result<PathBuf> {
let file_name = artifact_path
.file_name()
.ok_or_else(|| anyhow!("Built artifact path has no filename"))?;
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let persist_dir = std::env::temp_dir().join(format!("upstream-artifact-{nonce}"));
fs::create_dir_all(&persist_dir).context(format!(
"Failed to create artifact staging directory '{}'",
persist_dir.display()
))?;
let persisted_path = persist_dir.join(file_name);
fs::copy(artifact_path, &persisted_path).context(format!(
"Failed to stage built artifact from '{}' to '{}'",
artifact_path.display(),
persisted_path.display()
))?;
let perms = fs::metadata(artifact_path)
.context(format!(
"Failed to read built artifact metadata '{}'",
artifact_path.display()
))?
.permissions();
fs::set_permissions(&persisted_path, perms).context(format!(
"Failed to preserve artifact permissions on '{}'",
persisted_path.display()
))?;
Ok(persisted_path)
}
}
#[cfg(test)]
mod tests {
use super::BuildWorker;
use std::io::Write;
use std::time::{SystemTime, UNIX_EPOCH};
use std::{fs, path::PathBuf};
fn temp_root(name: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
std::env::temp_dir().join(format!("upstream-worker-test-{name}-{nanos}"))
}
#[test]
fn persist_artifact_copies_file_to_stable_temp_path() {
let root = temp_root("persist-artifact");
fs::create_dir_all(&root).expect("create temp root");
let src = root.join("tool");
let mut f = fs::File::create(&src).expect("create source artifact");
f.write_all(b"binary-data").expect("write source artifact");
let persisted = BuildWorker::persist_artifact(&src).expect("persist artifact");
assert!(persisted.exists());
assert_eq!(
fs::read(&persisted).expect("read persisted"),
b"binary-data"
);
assert_ne!(persisted, src);
let _ = fs::remove_dir_all(&root);
if let Some(parent) = persisted.parent() {
let _ = fs::remove_dir_all(parent);
}
}
}