blue-build 0.8.1

A CLI tool built for creating Containerfile templates based on the Ublue Community Project
Documentation
use std::{
    env, fs,
    path::{Path, PathBuf},
    process,
};

use anyhow::{anyhow, Result};
use askama::Template;
use clap::Args;
use log::{debug, error, info, trace};
use typed_builder::TypedBuilder;
use uuid::Uuid;

use crate::{constants::*, module_recipe::Recipe};

use super::BlueBuildCommand;

#[derive(Debug, Clone, Template, TypedBuilder)]
#[template(path = "Containerfile.j2", escape = "none")]
pub struct ContainerFileTemplate<'a> {
    recipe: &'a Recipe<'a>,

    #[builder(setter(into))]
    recipe_path: &'a Path,

    #[builder(setter(into))]
    build_id: Uuid,

    #[builder(default)]
    export_script: ExportsTemplate,
}

#[derive(Debug, Clone, Default, Template)]
#[template(path = "export.sh", escape = "none")]
pub struct ExportsTemplate;

impl ExportsTemplate {
    fn print_script(&self) -> String {
        trace!("print_script({self})");

        format!(
            "\"{}\"",
            self.render()
                .unwrap_or_else(|e| {
                    error!("Failed to render export.sh script: {e}");
                    process::exit(1);
                })
                .replace('\n', "\\n")
                .replace('\"', "\\\"")
                .replace('$', "\\$")
        )
    }
}

#[derive(Debug, Clone, Args, TypedBuilder)]
pub struct TemplateCommand {
    /// The recipe file to create a template from
    #[arg()]
    #[builder(default, setter(into, strip_option))]
    recipe: Option<PathBuf>,

    /// File to output to instead of STDOUT
    #[arg(short, long)]
    #[builder(default, setter(into, strip_option))]
    output: Option<PathBuf>,

    #[clap(skip)]
    #[builder(default, setter(into, strip_option))]
    build_id: Option<Uuid>,
}

impl BlueBuildCommand for TemplateCommand {
    fn try_run(&mut self) -> Result<()> {
        info!(
            "Templating for recipe at {}",
            self.recipe
                .clone()
                .unwrap_or_else(|| PathBuf::from(RECIPE_PATH))
                .display()
        );

        self.build_id.get_or_insert(Uuid::new_v4());

        self.template_file()
    }
}

impl TemplateCommand {
    fn template_file(&self) -> Result<()> {
        trace!("TemplateCommand::template_file()");

        let recipe_path = self
            .recipe
            .clone()
            .unwrap_or_else(|| PathBuf::from(RECIPE_PATH));

        debug!("Deserializing recipe");
        let recipe_de = Recipe::parse(&recipe_path)?;
        trace!("recipe_de: {recipe_de:#?}");

        let build_id = self
            .build_id
            .ok_or_else(|| anyhow!("Build ID should have been generated by now"))?;

        let template = ContainerFileTemplate::builder()
            .build_id(build_id)
            .recipe(&recipe_de)
            .recipe_path(recipe_path.as_path())
            .build();

        let output_str = template.render()?;
        if let Some(output) = self.output.as_ref() {
            debug!("Templating to file {}", output.display());
            trace!("Containerfile:\n{output_str}");

            std::fs::write(output, output_str)?;
        } else {
            debug!("Templating to stdout");
            println!("{output_str}");
        }

        info!("Finished templating Containerfile");
        Ok(())
    }
}

// ======================================================== //
// ========================= Helpers ====================== //
// ======================================================== //

fn has_cosign_file() -> bool {
    trace!("has_cosign_file()");
    std::env::current_dir()
        .map(|p| p.join(COSIGN_PATH).exists())
        .unwrap_or(false)
}

#[must_use]
fn print_containerfile(containerfile: &str) -> String {
    trace!("print_containerfile({containerfile})");
    debug!("Loading containerfile contents for {containerfile}");

    let path = format!("config/containerfiles/{containerfile}/Containerfile");

    let file = fs::read_to_string(&path).unwrap_or_else(|e| {
        error!("Failed to read file {path}: {e}");
        process::exit(1);
    });

    debug!("Containerfile contents {path}:\n{file}");

    file
}

fn get_github_repo_owner() -> Option<String> {
    Some(env::var(GITHUB_REPOSITORY_OWNER).ok()?.to_lowercase())
}

fn get_gitlab_registry_path() -> Option<String> {
    Some(
        format!(
            "{}/{}/{}",
            env::var(CI_REGISTRY).ok()?,
            env::var(CI_PROJECT_NAMESPACE).ok()?,
            env::var(CI_PROJECT_NAME).ok()?,
        )
        .to_lowercase(),
    )
}

fn modules_exists() -> bool {
    let mod_path = Path::new("modules");
    mod_path.exists() && mod_path.is_dir()
}