Skip to main content

blue_build_template/
lib.rs

1use std::{borrow::Cow, collections::BTreeMap, fs, path::Path, process};
2
3use blue_build_recipe::{MaybeVersion, Recipe};
4use blue_build_utils::{
5    constants::{CONFIG_PATH, CONTAINER_FILE, CONTAINERFILES_PATH, COSIGN_PUB_PATH, FILES_PATH},
6    container::Tag,
7    secret::SecretMounts,
8};
9use bon::Builder;
10use colored::control::ShouldColorize;
11use log::{debug, error, trace, warn};
12use uuid::Uuid;
13
14pub use askama::Template;
15
16#[derive(Debug, Clone, Copy)]
17pub enum BuildEngine {
18    Oci,
19    Docker,
20}
21
22#[derive(Debug, Clone, Template, Builder)]
23#[template(
24    path = "Containerfile.j2",
25    escape = "none",
26    whitespace = "minimize",
27    print = "code"
28)]
29pub struct ContainerFileTemplate<'a> {
30    #[builder(into)]
31    recipe: &'a Recipe,
32    recipe_path: &'a Path,
33
34    #[builder(into)]
35    build_id: Uuid,
36    os_version: u64,
37    registry: &'a str,
38    build_scripts_dir: &'a Path,
39    base_digest: &'a str,
40    nushell_version: Option<&'a MaybeVersion>,
41
42    #[builder(default)]
43    build_features: &'a [String],
44    build_engine: BuildEngine,
45
46    labels: &'a BTreeMap<String, String>,
47}
48
49impl ContainerFileTemplate<'_> {
50    const fn should_install_nu(&self) -> bool {
51        match self.nushell_version {
52            None | Some(MaybeVersion::VersionOrBranch(_)) => true,
53            Some(MaybeVersion::None) => false,
54        }
55    }
56
57    fn get_nu_version(&self) -> String {
58        match self.nushell_version {
59            Some(MaybeVersion::None) | None => "default".to_string(),
60            Some(MaybeVersion::VersionOrBranch(version)) => version.replace('/', "_"),
61        }
62    }
63
64    #[must_use]
65    fn get_features(&self) -> String {
66        self.build_features
67            .iter()
68            .map(|feat| feat.trim())
69            .collect::<Vec<_>>()
70            .join(",")
71    }
72
73    fn scripts_mount(&self, dest: &str) -> String {
74        format!(
75            "--mount=type=bind,src={},dst={dest},{}",
76            self.build_scripts_dir.display(),
77            match self.build_engine {
78                BuildEngine::Oci => "Z",
79                BuildEngine::Docker => "ro",
80            }
81        )
82    }
83}
84
85#[derive(Debug, Clone, Template, Builder)]
86#[template(path = "github_issue.j2", escape = "md")]
87#[builder(on(Cow<'_, str>, into))]
88pub struct GithubIssueTemplate<'a> {
89    bb_version: Cow<'a, str>,
90    build_rust_channel: Cow<'a, str>,
91    build_time: Cow<'a, str>,
92    git_commit_hash: Cow<'a, str>,
93    os_name: Cow<'a, str>,
94    os_version: Cow<'a, str>,
95    pkg_branch_tag: Cow<'a, str>,
96    recipe: Cow<'a, str>,
97    rust_channel: Cow<'a, str>,
98    rust_version: Cow<'a, str>,
99    shell_name: Cow<'a, str>,
100    shell_version: Cow<'a, str>,
101    terminal_name: Cow<'a, str>,
102    terminal_version: Cow<'a, str>,
103}
104
105#[derive(Debug, Clone, Template, Builder)]
106#[template(path = "init/README.j2", escape = "md")]
107#[builder(on(Cow<'_, str>, into))]
108pub struct InitReadmeTemplate<'a> {
109    repo_name: Cow<'a, str>,
110    registry: Cow<'a, str>,
111    image_name: Cow<'a, str>,
112}
113
114#[derive(Debug, Clone, Template, Builder)]
115#[template(path = "init/gitlab-ci.yml.j2", escape = "none")]
116#[builder(on(Cow<'_, str>, into))]
117pub struct GitlabCiTemplate<'a> {
118    version: Cow<'a, str>,
119}
120
121fn has_cosign_file() -> bool {
122    trace!("has_cosign_file()");
123    std::env::current_dir()
124        .map(|p| p.join(COSIGN_PUB_PATH).exists())
125        .unwrap_or(false)
126}
127
128#[must_use]
129fn print_containerfile(containerfile: &str) -> String {
130    trace!("print_containerfile({containerfile})");
131    debug!("Loading containerfile contents for {containerfile}");
132
133    let legacy_path = Path::new(CONFIG_PATH);
134    let containerfiles_path = Path::new(CONTAINERFILES_PATH);
135
136    let path = if containerfiles_path.exists() && containerfiles_path.is_dir() {
137        containerfiles_path.join(format!("{containerfile}/{CONTAINER_FILE}"))
138    } else {
139        warn!(
140            "Use of {CONFIG_PATH} is deprecated for the containerfile module, please move your containerfile directories into {CONTAINERFILES_PATH}"
141        );
142        legacy_path.join(format!("containerfiles/{containerfile}/{CONTAINER_FILE}"))
143    };
144
145    let file = fs::read_to_string(&path).unwrap_or_else(|e| {
146        error!("Failed to read file {}: {e}", path.display());
147        process::exit(1);
148    });
149
150    debug!("Containerfile contents {}:\n{file}", path.display());
151
152    file
153}
154
155fn modules_exists() -> bool {
156    let mod_path = Path::new("modules");
157    mod_path.exists() && mod_path.is_dir()
158}
159
160fn files_dir_exists() -> bool {
161    let path = Path::new(FILES_PATH);
162    path.exists() && path.is_dir()
163}
164
165fn config_dir_exists() -> bool {
166    let path = Path::new(CONFIG_PATH);
167    let exists = path.exists() && path.is_dir();
168
169    if exists {
170        warn!(
171            "Use of the {CONFIG_PATH} directory is deprecated. Please move your non-recipe files into {FILES_PATH}"
172        );
173    }
174
175    exists
176}
177
178fn should_color() -> bool {
179    ShouldColorize::from_env().should_colorize()
180}
181
182#[must_use]
183fn package_cache_mount_name(recipe_name: &str, image_version: &Tag, stage_name: &str) -> String {
184    format!("cache-{recipe_name}-{image_version}-stage-{stage_name}")
185}
186
187mod filters {
188    #![expect(clippy::unnecessary_wraps)]
189    use askama::Values;
190
191    #[askama::filter_fn]
192    pub fn replace<T: std::fmt::Display>(
193        input: T,
194        _: &dyn Values,
195        from: char,
196        to: &str,
197    ) -> askama::Result<String> {
198        Ok(format!("{input}").replace(from, to))
199    }
200}