upstream-rs 1.17.1

Fetch package updates directly from the source.
Documentation
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};

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(&self, request: BuildRequest, channel: Channel) -> Result<BuildOutput> {
        let downloader = SourceDownloader::new(self.provider_manager)?;
        let source = downloader
            .fetch_source(
                &request.repo_slug,
                &request.provider,
                request.base_url.as_deref(),
                &channel,
                request.version_tag.as_deref(),
                request.branch.as_deref(),
            )
            .await?;

        let handlers = handlers();
        let profile =
            determine_profile(&source.workspace_path, request.requested_profile, &handlers)
                .map_err(|err| {
                    anyhow!("{} (workspace: '{}')", err, source.workspace_path.display())
                })?;
        let selected = handlers
            .iter()
            .find(|handler| handler.profile() == profile)
            .ok_or_else(|| anyhow!("Unsupported build profile"))?;

        let artifact = selected.run_build(
            &source.workspace_path,
            &request.name,
            request.build_output.as_deref(),
        )?;
        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 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);
        }
    }
}