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}