1pub use crate::reference::Reference;
16
17use crate::Backend;
18use sha2::{Digest, Sha256};
19use std::path::PathBuf;
20use std::str::FromStr;
21
22#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
24pub struct BuildArgumentKey(String);
25
26impl BuildArgumentKey {
27 #[must_use]
28 pub fn as_str(&self) -> &str {
29 &self.0
30 }
31}
32
33impl FromStr for BuildArgumentKey {
34 type Err = BuildArgumentKeyError;
35
36 fn from_str(input: &str) -> Result<Self, Self::Err> {
37 if input.is_empty() {
38 return Err(BuildArgumentKeyError::Empty);
39 }
40 if input.contains('=') {
41 return Err(BuildArgumentKeyError::ContainsEquals);
42 }
43 Ok(BuildArgumentKey(input.to_string()))
44 }
45}
46
47#[derive(Debug, Clone, thiserror::Error)]
48pub enum BuildArgumentKeyError {
49 #[error("Build argument key cannot be empty")]
50 Empty,
51 #[error("Build argument key cannot contain '=' character")]
52 ContainsEquals,
53}
54
55#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
57pub struct BuildArgumentValue(String);
58
59impl BuildArgumentValue {
60 #[must_use]
61 pub fn as_str(&self) -> &str {
62 &self.0
63 }
64}
65
66impl FromStr for BuildArgumentValue {
67 type Err = std::convert::Infallible;
68
69 fn from_str(input: &str) -> Result<Self, Self::Err> {
70 Ok(BuildArgumentValue(input.to_string()))
71 }
72}
73
74impl From<String> for BuildArgumentValue {
75 fn from(string: String) -> Self {
76 BuildArgumentValue(string)
77 }
78}
79
80impl From<&str> for BuildArgumentValue {
81 fn from(string: &str) -> Self {
82 BuildArgumentValue(string.to_string())
83 }
84}
85
86#[derive(Clone, Debug, Eq, PartialEq)]
88pub enum BuildSource {
89 Directory(PathBuf),
91 Instructions(String),
93}
94
95#[derive(Clone, Debug, Eq, PartialEq)]
97pub enum BuildTarget {
98 Fixed(Reference),
100 ContentAddressed(crate::reference::Name),
102}
103
104#[derive(Clone, Debug, Eq, PartialEq)]
106pub struct BuildDefinition {
107 backend: Backend,
108 target: BuildTarget,
109 source: BuildSource,
110 build_arguments: std::collections::BTreeMap<BuildArgumentKey, BuildArgumentValue>,
111}
112
113impl BuildDefinition {
114 pub fn from_directory(
116 backend: &Backend,
117 reference: Reference,
118 path: impl Into<PathBuf>,
119 ) -> Self {
120 Self {
121 backend: backend.clone(),
122 target: BuildTarget::Fixed(reference),
123 source: BuildSource::Directory(path.into()),
124 build_arguments: std::collections::BTreeMap::new(),
125 }
126 }
127
128 pub fn from_instructions(
130 backend: &Backend,
131 reference: Reference,
132 instructions: impl Into<String>,
133 ) -> Self {
134 Self {
135 backend: backend.clone(),
136 target: BuildTarget::Fixed(reference),
137 source: BuildSource::Instructions(instructions.into()),
138 build_arguments: std::collections::BTreeMap::new(),
139 }
140 }
141
142 pub fn from_directory_hash(
144 backend: &Backend,
145 name: crate::reference::Name,
146 path: impl Into<PathBuf>,
147 ) -> Self {
148 Self {
149 backend: backend.clone(),
150 target: BuildTarget::ContentAddressed(name),
151 source: BuildSource::Directory(path.into()),
152 build_arguments: std::collections::BTreeMap::new(),
153 }
154 }
155
156 pub fn from_instructions_hash(
158 backend: &Backend,
159 name: crate::reference::Name,
160 instructions: impl Into<String>,
161 ) -> Self {
162 Self {
163 backend: backend.clone(),
164 target: BuildTarget::ContentAddressed(name),
165 source: BuildSource::Instructions(instructions.into()),
166 build_arguments: std::collections::BTreeMap::new(),
167 }
168 }
169
170 pub fn build_argument(
172 mut self,
173 key: BuildArgumentKey,
174 value: impl Into<BuildArgumentValue>,
175 ) -> Self {
176 self.build_arguments.insert(key, value.into());
177 self
178 }
179
180 pub fn build_arguments<V: Into<BuildArgumentValue>>(
182 mut self,
183 arguments: impl IntoIterator<Item = (BuildArgumentKey, V)>,
184 ) -> Self {
185 self.build_arguments.extend(
186 arguments
187 .into_iter()
188 .map(|(key, value)| (key, value.into())),
189 );
190 self
191 }
192
193 pub async fn build(&self) -> Reference {
195 self.build_image(self.compute_final_reference()).await
196 }
197
198 pub async fn build_if_absent(&self) -> Reference {
200 let target_reference = self.compute_final_reference();
201
202 if self.backend.is_image_present(&target_reference).await {
203 target_reference
204 } else {
205 self.build_image(target_reference).await
206 }
207 }
208
209 async fn build_image(&self, target_reference: Reference) -> Reference {
210 let mut arguments = vec!["build".into(), "--tag".into(), target_reference.to_string()];
211
212 for (key, value) in &self.build_arguments {
213 arguments.push("--build-arg".into());
214 arguments.push(format!("{}={}", key.as_str(), value.as_str()));
215 }
216
217 let command = match &self.source {
218 BuildSource::Directory(path) => {
219 arguments.push(path.to_string_lossy().into());
220 self.backend.command().arguments(arguments)
221 }
222 BuildSource::Instructions(content) => {
223 arguments.push("-".into());
224 self.backend
225 .command()
226 .arguments(arguments)
227 .stdin_bytes(content.as_bytes().to_vec())
228 }
229 };
230
231 command.status().await.unwrap();
232
233 target_reference
234 }
235
236 fn compute_final_reference(&self) -> Reference {
238 match &self.target {
239 BuildTarget::Fixed(reference) => reference.clone(),
240 BuildTarget::ContentAddressed(name) => {
241 let hash = match &self.source {
242 BuildSource::Directory(path) => {
243 compute_directory_hash(path, &self.build_arguments)
244 }
245 BuildSource::Instructions(content) => {
246 compute_content_hash(content, &self.build_arguments)
247 }
248 };
249 Reference {
250 name: name.clone(),
251 tag: Some(hash.into()),
252 digest: None,
253 }
254 }
255 }
256 }
257}
258
259fn compute_content_hash(
260 content: &str,
261 build_arguments: &std::collections::BTreeMap<BuildArgumentKey, BuildArgumentValue>,
262) -> sha2::digest::Output<Sha256> {
263 let mut hasher = Sha256::new();
264 hasher.update(content.as_bytes());
265
266 for (key, value) in build_arguments {
267 hasher.update(key.as_str().as_bytes());
268 hasher.update(b"=");
269 hasher.update(value.as_str().as_bytes());
270 }
271
272 hasher.finalize()
273}
274
275fn compute_directory_hash(
276 path: &PathBuf,
277 build_arguments: &std::collections::BTreeMap<BuildArgumentKey, BuildArgumentValue>,
278) -> sha2::digest::Output<Sha256> {
279 use walkdir::WalkDir;
280
281 let mut hasher = Sha256::new();
282
283 for entry in WalkDir::new(path)
284 .sort_by_file_name()
285 .into_iter()
286 .filter_map(|result| result.ok())
287 {
288 if entry.file_type().is_file() {
289 let relative_path = entry.path().strip_prefix(path).unwrap();
290 hasher.update(relative_path.to_string_lossy().as_bytes());
291
292 let content = std::fs::read(entry.path()).expect("Failed to read file");
293 hasher.update(&content);
294 }
295 }
296
297 for (key, value) in build_arguments {
298 hasher.update(key.as_str().as_bytes());
299 hasher.update(b"=");
300 hasher.update(value.as_str().as_bytes());
301 }
302
303 hasher.finalize()
304}