1use super::{CommandExecutor, DockerCommand};
7use crate::error::Result;
8use async_trait::async_trait;
9use std::collections::HashMap;
10use std::ffi::OsStr;
11use std::path::PathBuf;
12
13#[derive(Debug, Clone)]
15#[allow(clippy::struct_excessive_bools)]
16pub struct BuildCommand {
17 context: String,
19 executor: CommandExecutor,
21 add_hosts: Vec<String>,
23 build_args: HashMap<String, String>,
25 cache_from: Vec<String>,
27 cgroup_parent: Option<String>,
29 compress: bool,
31 cpu_period: Option<i64>,
33 cpu_quota: Option<i64>,
34 cpu_shares: Option<i64>,
35 cpuset_cpus: Option<String>,
36 cpuset_mems: Option<String>,
37 disable_content_trust: bool,
39 file: Option<PathBuf>,
41 force_rm: bool,
43 iidfile: Option<PathBuf>,
45 isolation: Option<String>,
47 labels: HashMap<String, String>,
49 memory: Option<String>,
51 memory_swap: Option<String>,
53 network: Option<String>,
55 no_cache: bool,
57 platform: Option<String>,
59 pull: bool,
61 quiet: bool,
63 rm: bool,
65 security_opts: Vec<String>,
67 shm_size: Option<String>,
69 tags: Vec<String>,
71 target: Option<String>,
73 ulimits: Vec<String>,
75 allow: Vec<String>,
77 annotations: Vec<String>,
79 attestations: Vec<String>,
81 build_contexts: Vec<String>,
83 builder: Option<String>,
85 cache_to: Vec<String>,
87 call: Option<String>,
89 check: bool,
91 load: bool,
93 metadata_file: Option<PathBuf>,
95 no_cache_filter: Vec<String>,
97 progress: Option<String>,
99 provenance: Option<String>,
101 push: bool,
103 sbom: Option<String>,
105 secrets: Vec<String>,
107 ssh: Vec<String>,
109}
110
111#[derive(Debug, Clone)]
113pub struct BuildOutput {
114 pub stdout: String,
116 pub stderr: String,
118 pub exit_code: i32,
120 pub image_id: Option<String>,
122}
123
124impl BuildOutput {
125 #[must_use]
127 pub fn success(&self) -> bool {
128 self.exit_code == 0
129 }
130
131 #[must_use]
133 pub fn combined_output(&self) -> String {
134 if self.stderr.is_empty() {
135 self.stdout.clone()
136 } else if self.stdout.is_empty() {
137 self.stderr.clone()
138 } else {
139 format!("{}\n{}", self.stdout, self.stderr)
140 }
141 }
142
143 #[must_use]
145 pub fn stdout_is_empty(&self) -> bool {
146 self.stdout.trim().is_empty()
147 }
148
149 #[must_use]
151 pub fn stderr_is_empty(&self) -> bool {
152 self.stderr.trim().is_empty()
153 }
154
155 fn extract_image_id(output: &str) -> Option<String> {
157 for line in output.lines() {
159 if line.contains("Successfully built ") {
160 if let Some(id) = line.split("Successfully built ").nth(1) {
161 return Some(id.trim().to_string());
162 }
163 }
164 if line.starts_with("sha256:") {
165 return Some(line.trim().to_string());
166 }
167 }
168 None
169 }
170}
171
172impl BuildCommand {
173 pub fn new(context: impl Into<String>) -> Self {
183 Self {
184 context: context.into(),
185 executor: CommandExecutor::new(),
186 add_hosts: Vec::new(),
187 build_args: HashMap::new(),
188 cache_from: Vec::new(),
189 cgroup_parent: None,
190 compress: false,
191 cpu_period: None,
192 cpu_quota: None,
193 cpu_shares: None,
194 cpuset_cpus: None,
195 cpuset_mems: None,
196 disable_content_trust: false,
197 file: None,
198 force_rm: false,
199 iidfile: None,
200 isolation: None,
201 labels: HashMap::new(),
202 memory: None,
203 memory_swap: None,
204 network: None,
205 no_cache: false,
206 platform: None,
207 pull: false,
208 quiet: false,
209 rm: true, security_opts: Vec::new(),
211 shm_size: None,
212 tags: Vec::new(),
213 target: None,
214 ulimits: Vec::new(),
215 allow: Vec::new(),
216 annotations: Vec::new(),
217 attestations: Vec::new(),
218 build_contexts: Vec::new(),
219 builder: None,
220 cache_to: Vec::new(),
221 call: None,
222 check: false,
223 load: false,
224 metadata_file: None,
225 no_cache_filter: Vec::new(),
226 progress: None,
227 provenance: None,
228 push: false,
229 sbom: None,
230 secrets: Vec::new(),
231 ssh: Vec::new(),
232 }
233 }
234
235 #[must_use]
246 pub fn add_host(mut self, host: impl Into<String>) -> Self {
247 self.add_hosts.push(host.into());
248 self
249 }
250
251 #[must_use]
263 pub fn build_arg(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
264 self.build_args.insert(key.into(), value.into());
265 self
266 }
267
268 #[must_use]
283 pub fn build_args_map(mut self, args: HashMap<String, String>) -> Self {
284 self.build_args.extend(args);
285 self
286 }
287
288 #[must_use]
299 pub fn cache_from(mut self, image: impl Into<String>) -> Self {
300 self.cache_from.push(image.into());
301 self
302 }
303
304 #[must_use]
315 pub fn cgroup_parent(mut self, parent: impl Into<String>) -> Self {
316 self.cgroup_parent = Some(parent.into());
317 self
318 }
319
320 #[must_use]
330 pub fn compress(mut self) -> Self {
331 self.compress = true;
332 self
333 }
334
335 #[must_use]
346 pub fn cpu_period(mut self, period: i64) -> Self {
347 self.cpu_period = Some(period);
348 self
349 }
350
351 #[must_use]
362 pub fn cpu_quota(mut self, quota: i64) -> Self {
363 self.cpu_quota = Some(quota);
364 self
365 }
366
367 #[must_use]
378 pub fn cpu_shares(mut self, shares: i64) -> Self {
379 self.cpu_shares = Some(shares);
380 self
381 }
382
383 #[must_use]
394 pub fn cpuset_cpus(mut self, cpus: impl Into<String>) -> Self {
395 self.cpuset_cpus = Some(cpus.into());
396 self
397 }
398
399 #[must_use]
410 pub fn cpuset_mems(mut self, mems: impl Into<String>) -> Self {
411 self.cpuset_mems = Some(mems.into());
412 self
413 }
414
415 #[must_use]
426 pub fn disable_content_trust(mut self) -> Self {
427 self.disable_content_trust = true;
428 self
429 }
430
431 #[must_use]
442 pub fn file(mut self, dockerfile: impl Into<PathBuf>) -> Self {
443 self.file = Some(dockerfile.into());
444 self
445 }
446
447 #[must_use]
458 pub fn force_rm(mut self) -> Self {
459 self.force_rm = true;
460 self
461 }
462
463 #[must_use]
474 pub fn iidfile(mut self, file: impl Into<PathBuf>) -> Self {
475 self.iidfile = Some(file.into());
476 self
477 }
478
479 #[must_use]
490 pub fn isolation(mut self, isolation: impl Into<String>) -> Self {
491 self.isolation = Some(isolation.into());
492 self
493 }
494
495 #[must_use]
507 pub fn label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
508 self.labels.insert(key.into(), value.into());
509 self
510 }
511
512 #[must_use]
527 pub fn labels(mut self, labels: HashMap<String, String>) -> Self {
528 self.labels.extend(labels);
529 self
530 }
531
532 #[must_use]
543 pub fn memory(mut self, limit: impl Into<String>) -> Self {
544 self.memory = Some(limit.into());
545 self
546 }
547
548 #[must_use]
559 pub fn memory_swap(mut self, limit: impl Into<String>) -> Self {
560 self.memory_swap = Some(limit.into());
561 self
562 }
563
564 #[must_use]
575 pub fn network(mut self, mode: impl Into<String>) -> Self {
576 self.network = Some(mode.into());
577 self
578 }
579
580 #[must_use]
591 pub fn no_cache(mut self) -> Self {
592 self.no_cache = true;
593 self
594 }
595
596 #[must_use]
607 pub fn platform(mut self, platform: impl Into<String>) -> Self {
608 self.platform = Some(platform.into());
609 self
610 }
611
612 #[must_use]
623 pub fn pull(mut self) -> Self {
624 self.pull = true;
625 self
626 }
627
628 #[must_use]
639 pub fn quiet(mut self) -> Self {
640 self.quiet = true;
641 self
642 }
643
644 #[must_use]
655 pub fn no_rm(mut self) -> Self {
656 self.rm = false;
657 self
658 }
659
660 #[must_use]
671 pub fn security_opt(mut self, opt: impl Into<String>) -> Self {
672 self.security_opts.push(opt.into());
673 self
674 }
675
676 #[must_use]
687 pub fn shm_size(mut self, size: impl Into<String>) -> Self {
688 self.shm_size = Some(size.into());
689 self
690 }
691
692 #[must_use]
704 pub fn tag(mut self, tag: impl Into<String>) -> Self {
705 self.tags.push(tag.into());
706 self
707 }
708
709 #[must_use]
720 pub fn tags(mut self, tags: Vec<String>) -> Self {
721 self.tags.extend(tags);
722 self
723 }
724
725 #[must_use]
736 pub fn target(mut self, stage: impl Into<String>) -> Self {
737 self.target = Some(stage.into());
738 self
739 }
740
741 #[must_use]
752 pub fn ulimit(mut self, limit: impl Into<String>) -> Self {
753 self.ulimits.push(limit.into());
754 self
755 }
756
757 #[must_use]
768 pub fn allow(mut self, entitlement: impl Into<String>) -> Self {
769 self.allow.push(entitlement.into());
770 self
771 }
772
773 #[must_use]
784 pub fn annotation(mut self, annotation: impl Into<String>) -> Self {
785 self.annotations.push(annotation.into());
786 self
787 }
788
789 #[must_use]
800 pub fn attest(mut self, attestation: impl Into<String>) -> Self {
801 self.attestations.push(attestation.into());
802 self
803 }
804
805 #[must_use]
816 pub fn build_context(mut self, context: impl Into<String>) -> Self {
817 self.build_contexts.push(context.into());
818 self
819 }
820
821 #[must_use]
832 pub fn builder(mut self, builder: impl Into<String>) -> Self {
833 self.builder = Some(builder.into());
834 self
835 }
836
837 #[must_use]
848 pub fn cache_to(mut self, destination: impl Into<String>) -> Self {
849 self.cache_to.push(destination.into());
850 self
851 }
852
853 #[must_use]
864 pub fn call(mut self, method: impl Into<String>) -> Self {
865 self.call = Some(method.into());
866 self
867 }
868
869 #[must_use]
880 pub fn check(mut self) -> Self {
881 self.check = true;
882 self
883 }
884
885 #[must_use]
896 pub fn load(mut self) -> Self {
897 self.load = true;
898 self
899 }
900
901 #[must_use]
912 pub fn metadata_file(mut self, file: impl Into<PathBuf>) -> Self {
913 self.metadata_file = Some(file.into());
914 self
915 }
916
917 #[must_use]
928 pub fn no_cache_filter(mut self, stage: impl Into<String>) -> Self {
929 self.no_cache_filter.push(stage.into());
930 self
931 }
932
933 #[must_use]
944 pub fn progress(mut self, progress_type: impl Into<String>) -> Self {
945 self.progress = Some(progress_type.into());
946 self
947 }
948
949 #[must_use]
960 pub fn provenance(mut self, provenance: impl Into<String>) -> Self {
961 self.provenance = Some(provenance.into());
962 self
963 }
964
965 #[must_use]
976 pub fn push(mut self) -> Self {
977 self.push = true;
978 self
979 }
980
981 #[must_use]
992 pub fn sbom(mut self, sbom: impl Into<String>) -> Self {
993 self.sbom = Some(sbom.into());
994 self
995 }
996
997 #[must_use]
1008 pub fn secret(mut self, secret: impl Into<String>) -> Self {
1009 self.secrets.push(secret.into());
1010 self
1011 }
1012
1013 #[must_use]
1024 pub fn ssh(mut self, ssh: impl Into<String>) -> Self {
1025 self.ssh.push(ssh.into());
1026 self
1027 }
1028}
1029
1030impl Default for BuildCommand {
1031 fn default() -> Self {
1032 Self::new(".")
1033 }
1034}
1035
1036impl BuildCommand {
1037 fn add_basic_args(&self, args: &mut Vec<String>) {
1038 for host in &self.add_hosts {
1040 args.push("--add-host".to_string());
1041 args.push(host.clone());
1042 }
1043
1044 for (key, value) in &self.build_args {
1046 args.push("--build-arg".to_string());
1047 args.push(format!("{key}={value}"));
1048 }
1049
1050 for cache in &self.cache_from {
1052 args.push("--cache-from".to_string());
1053 args.push(cache.clone());
1054 }
1055
1056 if let Some(ref dockerfile) = self.file {
1057 args.push("--file".to_string());
1058 args.push(dockerfile.to_string_lossy().to_string());
1059 }
1060
1061 if self.no_cache {
1062 args.push("--no-cache".to_string());
1063 }
1064
1065 if self.pull {
1066 args.push("--pull".to_string());
1067 }
1068
1069 if self.quiet {
1070 args.push("--quiet".to_string());
1071 }
1072
1073 for tag in &self.tags {
1075 args.push("--tag".to_string());
1076 args.push(tag.clone());
1077 }
1078
1079 if let Some(ref target) = self.target {
1080 args.push("--target".to_string());
1081 args.push(target.clone());
1082 }
1083 }
1084
1085 fn add_resource_args(&self, args: &mut Vec<String>) {
1086 if let Some(period) = self.cpu_period {
1087 args.push("--cpu-period".to_string());
1088 args.push(period.to_string());
1089 }
1090
1091 if let Some(quota) = self.cpu_quota {
1092 args.push("--cpu-quota".to_string());
1093 args.push(quota.to_string());
1094 }
1095
1096 if let Some(shares) = self.cpu_shares {
1097 args.push("--cpu-shares".to_string());
1098 args.push(shares.to_string());
1099 }
1100
1101 if let Some(ref cpus) = self.cpuset_cpus {
1102 args.push("--cpuset-cpus".to_string());
1103 args.push(cpus.clone());
1104 }
1105
1106 if let Some(ref mems) = self.cpuset_mems {
1107 args.push("--cpuset-mems".to_string());
1108 args.push(mems.clone());
1109 }
1110
1111 if let Some(ref memory) = self.memory {
1112 args.push("--memory".to_string());
1113 args.push(memory.clone());
1114 }
1115
1116 if let Some(ref swap) = self.memory_swap {
1117 args.push("--memory-swap".to_string());
1118 args.push(swap.clone());
1119 }
1120
1121 if let Some(ref size) = self.shm_size {
1122 args.push("--shm-size".to_string());
1123 args.push(size.clone());
1124 }
1125 }
1126
1127 fn add_advanced_args(&self, args: &mut Vec<String>) {
1128 self.add_container_args(args);
1129 self.add_metadata_args(args);
1130 self.add_buildx_args(args);
1131 }
1132
1133 fn add_container_args(&self, args: &mut Vec<String>) {
1134 if let Some(ref parent) = self.cgroup_parent {
1135 args.push("--cgroup-parent".to_string());
1136 args.push(parent.clone());
1137 }
1138
1139 if self.compress {
1140 args.push("--compress".to_string());
1141 }
1142
1143 if self.disable_content_trust {
1144 args.push("--disable-content-trust".to_string());
1145 }
1146
1147 if self.force_rm {
1148 args.push("--force-rm".to_string());
1149 }
1150
1151 if let Some(ref file) = self.iidfile {
1152 args.push("--iidfile".to_string());
1153 args.push(file.to_string_lossy().to_string());
1154 }
1155
1156 if let Some(ref isolation) = self.isolation {
1157 args.push("--isolation".to_string());
1158 args.push(isolation.clone());
1159 }
1160
1161 if let Some(ref network) = self.network {
1162 args.push("--network".to_string());
1163 args.push(network.clone());
1164 }
1165
1166 if let Some(ref platform) = self.platform {
1167 args.push("--platform".to_string());
1168 args.push(platform.clone());
1169 }
1170
1171 if !self.rm {
1172 args.push("--rm=false".to_string());
1173 }
1174
1175 for opt in &self.security_opts {
1177 args.push("--security-opt".to_string());
1178 args.push(opt.clone());
1179 }
1180
1181 for limit in &self.ulimits {
1183 args.push("--ulimit".to_string());
1184 args.push(limit.clone());
1185 }
1186 }
1187
1188 fn add_metadata_args(&self, args: &mut Vec<String>) {
1189 for (key, value) in &self.labels {
1191 args.push("--label".to_string());
1192 args.push(format!("{key}={value}"));
1193 }
1194
1195 for annotation in &self.annotations {
1196 args.push("--annotation".to_string());
1197 args.push(annotation.clone());
1198 }
1199
1200 if let Some(ref file) = self.metadata_file {
1201 args.push("--metadata-file".to_string());
1202 args.push(file.to_string_lossy().to_string());
1203 }
1204 }
1205
1206 fn add_buildx_args(&self, args: &mut Vec<String>) {
1207 for allow in &self.allow {
1208 args.push("--allow".to_string());
1209 args.push(allow.clone());
1210 }
1211
1212 for attest in &self.attestations {
1213 args.push("--attest".to_string());
1214 args.push(attest.clone());
1215 }
1216
1217 for context in &self.build_contexts {
1218 args.push("--build-context".to_string());
1219 args.push(context.clone());
1220 }
1221
1222 if let Some(ref builder) = self.builder {
1223 args.push("--builder".to_string());
1224 args.push(builder.clone());
1225 }
1226
1227 for cache in &self.cache_to {
1228 args.push("--cache-to".to_string());
1229 args.push(cache.clone());
1230 }
1231
1232 if let Some(ref call) = self.call {
1233 args.push("--call".to_string());
1234 args.push(call.clone());
1235 }
1236
1237 if self.check {
1238 args.push("--check".to_string());
1239 }
1240
1241 if self.load {
1242 args.push("--load".to_string());
1243 }
1244
1245 for filter in &self.no_cache_filter {
1246 args.push("--no-cache-filter".to_string());
1247 args.push(filter.clone());
1248 }
1249
1250 if let Some(ref progress) = self.progress {
1251 args.push("--progress".to_string());
1252 args.push(progress.clone());
1253 }
1254
1255 if let Some(ref provenance) = self.provenance {
1256 args.push("--provenance".to_string());
1257 args.push(provenance.clone());
1258 }
1259
1260 if self.push {
1261 args.push("--push".to_string());
1262 }
1263
1264 if let Some(ref sbom) = self.sbom {
1265 args.push("--sbom".to_string());
1266 args.push(sbom.clone());
1267 }
1268
1269 for secret in &self.secrets {
1270 args.push("--secret".to_string());
1271 args.push(secret.clone());
1272 }
1273
1274 for ssh in &self.ssh {
1275 args.push("--ssh".to_string());
1276 args.push(ssh.clone());
1277 }
1278 }
1279}
1280
1281#[async_trait]
1282impl DockerCommand for BuildCommand {
1283 type Output = BuildOutput;
1284
1285 fn command_name(&self) -> &'static str {
1286 "build"
1287 }
1288
1289 fn build_args(&self) -> Vec<String> {
1290 let mut args = Vec::new();
1291
1292 self.add_basic_args(&mut args);
1293 self.add_resource_args(&mut args);
1294 self.add_advanced_args(&mut args);
1295
1296 args.extend(self.executor.raw_args.clone());
1298
1299 args.push(self.context.clone());
1301
1302 args
1303 }
1304
1305 async fn execute(&self) -> Result<Self::Output> {
1306 let args = self.build_args();
1307 let output = self
1308 .executor
1309 .execute_command(self.command_name(), args)
1310 .await?;
1311
1312 let image_id = if self.quiet {
1314 Some(output.stdout.trim().to_string())
1316 } else {
1317 let combined = if output.stderr.is_empty() {
1318 output.stdout.clone()
1319 } else if output.stdout.is_empty() {
1320 output.stderr.clone()
1321 } else {
1322 format!("{}\n{}", output.stdout, output.stderr)
1323 };
1324 BuildOutput::extract_image_id(&combined)
1325 };
1326
1327 Ok(BuildOutput {
1328 stdout: output.stdout,
1329 stderr: output.stderr,
1330 exit_code: output.exit_code,
1331 image_id,
1332 })
1333 }
1334
1335 fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
1336 self.executor.add_arg(arg);
1337 self
1338 }
1339
1340 fn args<I, S>(&mut self, args: I) -> &mut Self
1341 where
1342 I: IntoIterator<Item = S>,
1343 S: AsRef<OsStr>,
1344 {
1345 self.executor.add_args(args);
1346 self
1347 }
1348
1349 fn flag(&mut self, flag: &str) -> &mut Self {
1350 self.executor.add_flag(flag);
1351 self
1352 }
1353
1354 fn option(&mut self, key: &str, value: &str) -> &mut Self {
1355 self.executor.add_option(key, value);
1356 self
1357 }
1358}
1359
1360#[cfg(test)]
1361mod tests {
1362 use super::*;
1363
1364 #[test]
1365 fn test_build_command_basic() {
1366 let cmd = BuildCommand::new(".").tag("myapp:latest").no_cache().pull();
1367
1368 let args = cmd.build_args();
1369
1370 assert!(args.contains(&"--tag".to_string()));
1371 assert!(args.contains(&"myapp:latest".to_string()));
1372 assert!(args.contains(&"--no-cache".to_string()));
1373 assert!(args.contains(&"--pull".to_string()));
1374 assert!(args.contains(&".".to_string()));
1375 }
1376
1377 #[test]
1378 fn test_build_command_with_dockerfile() {
1379 let cmd = BuildCommand::new("/path/to/context")
1380 .file("Dockerfile.prod")
1381 .tag("myapp:prod");
1382
1383 let args = cmd.build_args();
1384
1385 assert!(args.contains(&"--file".to_string()));
1386 assert!(args.contains(&"Dockerfile.prod".to_string()));
1387 assert!(args.contains(&"--tag".to_string()));
1388 assert!(args.contains(&"myapp:prod".to_string()));
1389 assert!(args.contains(&"/path/to/context".to_string()));
1390 }
1391
1392 #[test]
1393 fn test_build_command_with_build_args() {
1394 let mut build_args = HashMap::new();
1395 build_args.insert("VERSION".to_string(), "1.0.0".to_string());
1396 build_args.insert("DEBUG".to_string(), "true".to_string());
1397
1398 let cmd = BuildCommand::new(".")
1399 .build_args_map(build_args)
1400 .build_arg("EXTRA", "value");
1401
1402 let args = cmd.build_args();
1403
1404 assert!(args.contains(&"--build-arg".to_string()));
1405 assert!(args.contains(&"VERSION=1.0.0".to_string()));
1406 assert!(args.contains(&"DEBUG=true".to_string()));
1407 assert!(args.contains(&"EXTRA=value".to_string()));
1408 }
1409
1410 #[test]
1411 fn test_build_command_with_labels() {
1412 let cmd = BuildCommand::new(".")
1413 .label("version", "1.0.0")
1414 .label("maintainer", "team@example.com");
1415
1416 let args = cmd.build_args();
1417
1418 assert!(args.contains(&"--label".to_string()));
1419 assert!(args.contains(&"version=1.0.0".to_string()));
1420 assert!(args.contains(&"maintainer=team@example.com".to_string()));
1421 }
1422
1423 #[test]
1424 fn test_build_command_resource_limits() {
1425 let cmd = BuildCommand::new(".")
1426 .memory("1g")
1427 .cpu_shares(512)
1428 .cpuset_cpus("0-3");
1429
1430 let args = cmd.build_args();
1431
1432 assert!(args.contains(&"--memory".to_string()));
1433 assert!(args.contains(&"1g".to_string()));
1434 assert!(args.contains(&"--cpu-shares".to_string()));
1435 assert!(args.contains(&"512".to_string()));
1436 assert!(args.contains(&"--cpuset-cpus".to_string()));
1437 assert!(args.contains(&"0-3".to_string()));
1438 }
1439
1440 #[test]
1441 fn test_build_command_advanced_options() {
1442 let cmd = BuildCommand::new(".")
1443 .platform("linux/amd64")
1444 .target("production")
1445 .network("host")
1446 .cache_from("myapp:cache")
1447 .security_opt("seccomp=unconfined");
1448
1449 let args = cmd.build_args();
1450
1451 assert!(args.contains(&"--platform".to_string()));
1452 assert!(args.contains(&"linux/amd64".to_string()));
1453 assert!(args.contains(&"--target".to_string()));
1454 assert!(args.contains(&"production".to_string()));
1455 assert!(args.contains(&"--network".to_string()));
1456 assert!(args.contains(&"host".to_string()));
1457 assert!(args.contains(&"--cache-from".to_string()));
1458 assert!(args.contains(&"myapp:cache".to_string()));
1459 assert!(args.contains(&"--security-opt".to_string()));
1460 assert!(args.contains(&"seccomp=unconfined".to_string()));
1461 }
1462
1463 #[test]
1464 fn test_build_command_multiple_tags() {
1465 let tags = vec!["myapp:latest".to_string(), "myapp:1.0.0".to_string()];
1466 let cmd = BuildCommand::new(".").tags(tags);
1467
1468 let args = cmd.build_args();
1469
1470 let tag_count = args.iter().filter(|&arg| arg == "--tag").count();
1471 assert_eq!(tag_count, 2);
1472 assert!(args.contains(&"myapp:latest".to_string()));
1473 assert!(args.contains(&"myapp:1.0.0".to_string()));
1474 }
1475
1476 #[test]
1477 fn test_build_command_quiet_mode() {
1478 let cmd = BuildCommand::new(".").quiet().tag("myapp:test");
1479
1480 let args = cmd.build_args();
1481
1482 assert!(args.contains(&"--quiet".to_string()));
1483 assert!(args.contains(&"--tag".to_string()));
1484 assert!(args.contains(&"myapp:test".to_string()));
1485 }
1486
1487 #[test]
1488 fn test_build_command_no_rm() {
1489 let cmd = BuildCommand::new(".").no_rm();
1490
1491 let args = cmd.build_args();
1492
1493 assert!(args.contains(&"--rm=false".to_string()));
1494 }
1495
1496 #[test]
1497 fn test_build_command_context_position() {
1498 let cmd = BuildCommand::new("/custom/context")
1499 .tag("test:latest")
1500 .no_cache();
1501
1502 let args = cmd.build_args();
1503
1504 assert_eq!(args.last(), Some(&"/custom/context".to_string()));
1506 }
1507
1508 #[test]
1509 fn test_build_output_helpers() {
1510 let output = BuildOutput {
1511 stdout: "Successfully built abc123def456".to_string(),
1512 stderr: String::new(),
1513 exit_code: 0,
1514 image_id: Some("abc123def456".to_string()),
1515 };
1516
1517 assert!(output.success());
1518 assert!(!output.stdout_is_empty());
1519 assert!(output.stderr_is_empty());
1520 assert_eq!(output.image_id, Some("abc123def456".to_string()));
1521 }
1522
1523 #[test]
1524 fn test_build_command_extensibility() {
1525 let mut cmd = BuildCommand::new(".");
1526
1527 cmd.flag("--some-flag");
1529 cmd.option("--some-option", "value");
1530 cmd.arg("extra-arg");
1531
1532 let args = cmd.build_args();
1533
1534 assert!(args.contains(&"--some-flag".to_string()));
1535 assert!(args.contains(&"--some-option".to_string()));
1536 assert!(args.contains(&"value".to_string()));
1537 assert!(args.contains(&"extra-arg".to_string()));
1538 }
1539
1540 #[test]
1541 fn test_image_id_extraction() {
1542 let output1 = "Step 1/3 : FROM alpine\nSuccessfully built abc123def456\n";
1543 let id1 = BuildOutput::extract_image_id(output1);
1544 assert_eq!(id1, Some("abc123def456".to_string()));
1545
1546 let output2 = "sha256:1234567890abcdef\n";
1547 let id2 = BuildOutput::extract_image_id(output2);
1548 assert_eq!(id2, Some("sha256:1234567890abcdef".to_string()));
1549
1550 let output3 = "No image ID found here";
1551 let id3 = BuildOutput::extract_image_id(output3);
1552 assert_eq!(id3, None);
1553 }
1554
1555 #[test]
1556 fn test_build_command_modern_buildx_options() {
1557 let cmd = BuildCommand::new(".")
1558 .allow("network.host")
1559 .annotation("org.opencontainers.image.title=MyApp")
1560 .attest("type=provenance,mode=max")
1561 .build_context("mycontext=../path")
1562 .builder("mybuilder")
1563 .cache_to("type=registry,ref=myregistry/cache")
1564 .call("check")
1565 .check()
1566 .load()
1567 .metadata_file("/tmp/metadata.json")
1568 .no_cache_filter("build-stage")
1569 .progress("plain")
1570 .provenance("mode=max")
1571 .push()
1572 .sbom("generator=image")
1573 .secret("id=mysecret,src=/local/secret")
1574 .ssh("default");
1575
1576 let args = cmd.build_args();
1577
1578 assert!(args.contains(&"--allow".to_string()));
1579 assert!(args.contains(&"network.host".to_string()));
1580 assert!(args.contains(&"--annotation".to_string()));
1581 assert!(args.contains(&"org.opencontainers.image.title=MyApp".to_string()));
1582 assert!(args.contains(&"--attest".to_string()));
1583 assert!(args.contains(&"type=provenance,mode=max".to_string()));
1584 assert!(args.contains(&"--build-context".to_string()));
1585 assert!(args.contains(&"mycontext=../path".to_string()));
1586 assert!(args.contains(&"--builder".to_string()));
1587 assert!(args.contains(&"mybuilder".to_string()));
1588 assert!(args.contains(&"--cache-to".to_string()));
1589 assert!(args.contains(&"type=registry,ref=myregistry/cache".to_string()));
1590 assert!(args.contains(&"--call".to_string()));
1591 assert!(args.contains(&"check".to_string()));
1592 assert!(args.contains(&"--check".to_string()));
1593 assert!(args.contains(&"--load".to_string()));
1594 assert!(args.contains(&"--metadata-file".to_string()));
1595 assert!(args.contains(&"/tmp/metadata.json".to_string()));
1596 assert!(args.contains(&"--no-cache-filter".to_string()));
1597 assert!(args.contains(&"build-stage".to_string()));
1598 assert!(args.contains(&"--progress".to_string()));
1599 assert!(args.contains(&"plain".to_string()));
1600 assert!(args.contains(&"--provenance".to_string()));
1601 assert!(args.contains(&"mode=max".to_string()));
1602 assert!(args.contains(&"--push".to_string()));
1603 assert!(args.contains(&"--sbom".to_string()));
1604 assert!(args.contains(&"generator=image".to_string()));
1605 assert!(args.contains(&"--secret".to_string()));
1606 assert!(args.contains(&"id=mysecret,src=/local/secret".to_string()));
1607 assert!(args.contains(&"--ssh".to_string()));
1608 assert!(args.contains(&"default".to_string()));
1609 }
1610}