blueprint-manager 0.4.0-alpha.3

Tangle Blueprint manager and Runner
use super::{BlueprintArgs, BlueprintEnvVars, BlueprintSourceHandler};
use crate::config::BlueprintManagerContext;
use crate::error::{Error, Result};
use crate::rt::ResourceLimits;
use crate::rt::service::Service;
use crate::sdk::utils::make_executable;
use crate::sources::types::TestFetcher;
use blueprint_core::trace;
use blueprint_runner::config::BlueprintEnvironment;
use std::path::{Path, PathBuf};
use std::process::Stdio;

pub struct TestSourceFetcher {
    pub fetcher: TestFetcher,
    pub blueprint_id: u64,
    pub blueprint_name: String,
    resolved_binary_path: Option<PathBuf>,
}

impl TestSourceFetcher {
    #[must_use]
    pub fn new(fetcher: TestFetcher, blueprint_id: u64, blueprint_name: String) -> Self {
        Self {
            fetcher,
            blueprint_id,
            blueprint_name,
            resolved_binary_path: None,
        }
    }

    async fn get_binary(&mut self, _cache_dir: &Path) -> Result<PathBuf> {
        let cargo_bin = self.fetcher.cargo_bin.clone();
        let base_path_str = self.fetcher.base_path.clone();
        let git_repo_root = get_git_repo_root_path_in(&base_path_str).await?;

        let profile = if cfg!(debug_assertions) {
            "debug"
        } else {
            "release"
        };
        let base_path = std::path::absolute(&git_repo_root)?;

        let target_dir = match std::env::var("CARGO_TARGET_DIR") {
            Ok(target) => PathBuf::from(target),
            Err(_) => git_repo_root.join("target"),
        };

        let binary_path = target_dir.join(profile).join(&cargo_bin);
        let binary_path = std::path::absolute(&binary_path)?;

        trace!("Base Path: {}", base_path.display());
        trace!("Binary Path: {}", binary_path.display());

        // Check if the binary already exists and is built (only when we are not in debug mode)
        if binary_path.exists() && !cfg!(debug_assertions) {
            trace!(
                "Binary already built, using existing binary at {}",
                binary_path.display()
            );
            trace!(
                binary_path = %binary_path.display(),
                "if you want to rebuild the binary, run `cargo clean` in the repository root or remove the built binary manually"
            );
            return Ok(binary_path);
        }

        // Run cargo build on the cargo_bin and ensure it build to the binary_path
        let mut command = tokio::process::Command::new("cargo");
        command
            .arg("build")
            .arg(format!("--target-dir={}", target_dir.display()))
            .arg("--bin")
            .arg(&cargo_bin);

        if !cfg!(debug_assertions) {
            command.arg("--release");
        }

        let output = match command
            .current_dir(&base_path)
            .stdin(Stdio::null())
            .kill_on_drop(true)
            .output()
            .await
        {
            Ok(output) => output,
            Err(err) => {
                blueprint_core::warn!(
                    "Failed to run build command using cargo: {err}. Ensure that cargo is installed and available in your PATH."
                );
                return Err(Error::from(err));
            }
        };
        trace!("Build command run, this may take a while...");
        if !output.status.success() {
            blueprint_core::warn!("Failed to build binary");
            return Err(Error::BuildBinary(output));
        }

        trace!("Successfully built binary");

        Ok(binary_path)
    }
}

async fn get_git_repo_root_path_in<P: AsRef<Path>>(cwd: P) -> Result<PathBuf> {
    // Run a process to determine the root directory for this repo
    let output = tokio::process::Command::new("git")
        .arg("rev-parse")
        .arg("--show-toplevel")
        .current_dir(cwd)
        .output()
        .await?;

    if !output.status.success() {
        return Err(Error::FetchGitRoot(output));
    }

    Ok(PathBuf::from(String::from_utf8(output.stdout)?.trim()))
}

impl BlueprintSourceHandler for TestSourceFetcher {
    async fn fetch(&mut self, cache_dir: &Path) -> Result<PathBuf> {
        if let Some(binary_path) = &self.resolved_binary_path {
            if binary_path.exists() {
                return Ok(binary_path.clone());
            }

            // Re-resolve
            self.resolved_binary_path = None;
        }

        let mut binary_path = self.get_binary(cache_dir).await?;

        // Ensure the binary is executable
        binary_path = make_executable(&binary_path)?;
        self.resolved_binary_path = Some(binary_path.clone());
        Ok(binary_path)
    }

    async fn spawn(
        &mut self,
        ctx: &BlueprintManagerContext,
        limits: ResourceLimits,
        blueprint_config: &BlueprintEnvironment,
        id: u32,
        env: BlueprintEnvVars,
        args: BlueprintArgs,
        _confidentiality_policy: blueprint_client_tangle::ConfidentialityPolicy,
        sub_service_str: &str,
        cache_dir: &Path,
        runtime_dir: &Path,
    ) -> Result<Service> {
        let resolved_binary_path = self.fetch(cache_dir).await?;
        Service::from_binary(
            ctx,
            limits,
            blueprint_config,
            id,
            env,
            args,
            &resolved_binary_path,
            sub_service_str,
            cache_dir,
            runtime_dir,
        )
        .await
    }

    fn blueprint_id(&self) -> u64 {
        self.blueprint_id
    }

    fn name(&self) -> String {
        self.blueprint_name.clone()
    }
}