1use super::{CommandExecutor, DockerCommand};
7use crate::error::Result;
8use crate::stream::{OutputLine, StreamResult, StreamableCommand};
9use async_trait::async_trait;
10use std::collections::HashMap;
11use std::path::PathBuf;
12use tokio::process::Command as TokioCommand;
13use tokio::sync::mpsc;
14
15#[derive(Debug, Clone)]
17#[allow(clippy::struct_excessive_bools)]
18pub struct BuildCommand {
19 context: String,
21 pub executor: CommandExecutor,
23 add_hosts: Vec<String>,
25 build_args: HashMap<String, String>,
27 cache_from: Vec<String>,
29 cgroup_parent: Option<String>,
31 compress: bool,
33 cpu_period: Option<i64>,
35 cpu_quota: Option<i64>,
36 cpu_shares: Option<i64>,
37 cpuset_cpus: Option<String>,
38 cpuset_mems: Option<String>,
39 disable_content_trust: bool,
41 file: Option<PathBuf>,
43 force_rm: bool,
45 iidfile: Option<PathBuf>,
47 isolation: Option<String>,
49 labels: HashMap<String, String>,
51 memory: Option<String>,
53 memory_swap: Option<String>,
55 network: Option<String>,
57 no_cache: bool,
59 platform: Option<String>,
61 pull: bool,
63 quiet: bool,
65 rm: bool,
67 security_opts: Vec<String>,
69 shm_size: Option<String>,
71 tags: Vec<String>,
73 target: Option<String>,
75 ulimits: Vec<String>,
77 allow: Vec<String>,
79 annotations: Vec<String>,
81 attestations: Vec<String>,
83 build_contexts: Vec<String>,
85 builder: Option<String>,
87 cache_to: Vec<String>,
89 call: Option<String>,
91 check: bool,
93 load: bool,
95 metadata_file: Option<PathBuf>,
97 no_cache_filter: Vec<String>,
99 progress: Option<String>,
101 provenance: Option<String>,
103 push: bool,
105 sbom: Option<String>,
107 secrets: Vec<String>,
109 ssh: Vec<String>,
111}
112
113#[derive(Debug, Clone)]
115pub struct BuildOutput {
116 pub stdout: String,
118 pub stderr: String,
120 pub exit_code: i32,
122 pub image_id: Option<String>,
124}
125
126impl BuildOutput {
127 #[must_use]
129 pub fn success(&self) -> bool {
130 self.exit_code == 0
131 }
132
133 #[must_use]
135 pub fn combined_output(&self) -> String {
136 if self.stderr.is_empty() {
137 self.stdout.clone()
138 } else if self.stdout.is_empty() {
139 self.stderr.clone()
140 } else {
141 format!("{}\n{}", self.stdout, self.stderr)
142 }
143 }
144
145 #[must_use]
147 pub fn stdout_is_empty(&self) -> bool {
148 self.stdout.trim().is_empty()
149 }
150
151 #[must_use]
153 pub fn stderr_is_empty(&self) -> bool {
154 self.stderr.trim().is_empty()
155 }
156
157 fn extract_image_id(output: &str) -> Option<String> {
159 for line in output.lines() {
161 if line.contains("Successfully built ") {
162 if let Some(id) = line.split("Successfully built ").nth(1) {
163 return Some(id.trim().to_string());
164 }
165 }
166 if line.starts_with("sha256:") {
167 return Some(line.trim().to_string());
168 }
169 }
170 None
171 }
172}
173
174impl BuildCommand {
175 pub fn new(context: impl Into<String>) -> Self {
185 Self {
186 context: context.into(),
187 executor: CommandExecutor::new(),
188 add_hosts: Vec::new(),
189 build_args: HashMap::new(),
190 cache_from: Vec::new(),
191 cgroup_parent: None,
192 compress: false,
193 cpu_period: None,
194 cpu_quota: None,
195 cpu_shares: None,
196 cpuset_cpus: None,
197 cpuset_mems: None,
198 disable_content_trust: false,
199 file: None,
200 force_rm: false,
201 iidfile: None,
202 isolation: None,
203 labels: HashMap::new(),
204 memory: None,
205 memory_swap: None,
206 network: None,
207 no_cache: false,
208 platform: None,
209 pull: false,
210 quiet: false,
211 rm: true, security_opts: Vec::new(),
213 shm_size: None,
214 tags: Vec::new(),
215 target: None,
216 ulimits: Vec::new(),
217 allow: Vec::new(),
218 annotations: Vec::new(),
219 attestations: Vec::new(),
220 build_contexts: Vec::new(),
221 builder: None,
222 cache_to: Vec::new(),
223 call: None,
224 check: false,
225 load: false,
226 metadata_file: None,
227 no_cache_filter: Vec::new(),
228 progress: None,
229 provenance: None,
230 push: false,
231 sbom: None,
232 secrets: Vec::new(),
233 ssh: Vec::new(),
234 }
235 }
236
237 #[must_use]
248 pub fn add_host(mut self, host: impl Into<String>) -> Self {
249 self.add_hosts.push(host.into());
250 self
251 }
252
253 #[must_use]
265 pub fn build_arg(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
266 self.build_args.insert(key.into(), value.into());
267 self
268 }
269
270 #[must_use]
285 pub fn build_args_map(mut self, args: HashMap<String, String>) -> Self {
286 self.build_args.extend(args);
287 self
288 }
289
290 #[must_use]
301 pub fn cache_from(mut self, image: impl Into<String>) -> Self {
302 self.cache_from.push(image.into());
303 self
304 }
305
306 #[must_use]
317 pub fn cgroup_parent(mut self, parent: impl Into<String>) -> Self {
318 self.cgroup_parent = Some(parent.into());
319 self
320 }
321
322 #[must_use]
332 pub fn compress(mut self) -> Self {
333 self.compress = true;
334 self
335 }
336
337 #[must_use]
348 pub fn cpu_period(mut self, period: i64) -> Self {
349 self.cpu_period = Some(period);
350 self
351 }
352
353 #[must_use]
364 pub fn cpu_quota(mut self, quota: i64) -> Self {
365 self.cpu_quota = Some(quota);
366 self
367 }
368
369 #[must_use]
380 pub fn cpu_shares(mut self, shares: i64) -> Self {
381 self.cpu_shares = Some(shares);
382 self
383 }
384
385 #[must_use]
396 pub fn cpuset_cpus(mut self, cpus: impl Into<String>) -> Self {
397 self.cpuset_cpus = Some(cpus.into());
398 self
399 }
400
401 #[must_use]
412 pub fn cpuset_mems(mut self, mems: impl Into<String>) -> Self {
413 self.cpuset_mems = Some(mems.into());
414 self
415 }
416
417 #[must_use]
428 pub fn disable_content_trust(mut self) -> Self {
429 self.disable_content_trust = true;
430 self
431 }
432
433 #[must_use]
444 pub fn file(mut self, dockerfile: impl Into<PathBuf>) -> Self {
445 self.file = Some(dockerfile.into());
446 self
447 }
448
449 #[must_use]
460 pub fn force_rm(mut self) -> Self {
461 self.force_rm = true;
462 self
463 }
464
465 #[must_use]
476 pub fn iidfile(mut self, file: impl Into<PathBuf>) -> Self {
477 self.iidfile = Some(file.into());
478 self
479 }
480
481 #[must_use]
492 pub fn isolation(mut self, isolation: impl Into<String>) -> Self {
493 self.isolation = Some(isolation.into());
494 self
495 }
496
497 #[must_use]
509 pub fn label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
510 self.labels.insert(key.into(), value.into());
511 self
512 }
513
514 #[must_use]
529 pub fn labels(mut self, labels: HashMap<String, String>) -> Self {
530 self.labels.extend(labels);
531 self
532 }
533
534 #[must_use]
545 pub fn memory(mut self, limit: impl Into<String>) -> Self {
546 self.memory = Some(limit.into());
547 self
548 }
549
550 #[must_use]
561 pub fn memory_swap(mut self, limit: impl Into<String>) -> Self {
562 self.memory_swap = Some(limit.into());
563 self
564 }
565
566 #[must_use]
577 pub fn network(mut self, mode: impl Into<String>) -> Self {
578 self.network = Some(mode.into());
579 self
580 }
581
582 #[must_use]
593 pub fn no_cache(mut self) -> Self {
594 self.no_cache = true;
595 self
596 }
597
598 #[must_use]
609 pub fn platform(mut self, platform: impl Into<String>) -> Self {
610 self.platform = Some(platform.into());
611 self
612 }
613
614 #[must_use]
625 pub fn pull(mut self) -> Self {
626 self.pull = true;
627 self
628 }
629
630 #[must_use]
641 pub fn quiet(mut self) -> Self {
642 self.quiet = true;
643 self
644 }
645
646 #[must_use]
657 pub fn no_rm(mut self) -> Self {
658 self.rm = false;
659 self
660 }
661
662 #[must_use]
673 pub fn security_opt(mut self, opt: impl Into<String>) -> Self {
674 self.security_opts.push(opt.into());
675 self
676 }
677
678 #[must_use]
689 pub fn shm_size(mut self, size: impl Into<String>) -> Self {
690 self.shm_size = Some(size.into());
691 self
692 }
693
694 #[must_use]
706 pub fn tag(mut self, tag: impl Into<String>) -> Self {
707 self.tags.push(tag.into());
708 self
709 }
710
711 #[must_use]
722 pub fn tags(mut self, tags: Vec<String>) -> Self {
723 self.tags.extend(tags);
724 self
725 }
726
727 #[must_use]
738 pub fn target(mut self, stage: impl Into<String>) -> Self {
739 self.target = Some(stage.into());
740 self
741 }
742
743 #[must_use]
754 pub fn ulimit(mut self, limit: impl Into<String>) -> Self {
755 self.ulimits.push(limit.into());
756 self
757 }
758
759 #[must_use]
770 pub fn allow(mut self, entitlement: impl Into<String>) -> Self {
771 self.allow.push(entitlement.into());
772 self
773 }
774
775 #[must_use]
786 pub fn annotation(mut self, annotation: impl Into<String>) -> Self {
787 self.annotations.push(annotation.into());
788 self
789 }
790
791 #[must_use]
802 pub fn attest(mut self, attestation: impl Into<String>) -> Self {
803 self.attestations.push(attestation.into());
804 self
805 }
806
807 #[must_use]
818 pub fn build_context(mut self, context: impl Into<String>) -> Self {
819 self.build_contexts.push(context.into());
820 self
821 }
822
823 #[must_use]
834 pub fn builder(mut self, builder: impl Into<String>) -> Self {
835 self.builder = Some(builder.into());
836 self
837 }
838
839 #[must_use]
850 pub fn cache_to(mut self, destination: impl Into<String>) -> Self {
851 self.cache_to.push(destination.into());
852 self
853 }
854
855 #[must_use]
866 pub fn call(mut self, method: impl Into<String>) -> Self {
867 self.call = Some(method.into());
868 self
869 }
870
871 #[must_use]
882 pub fn check(mut self) -> Self {
883 self.check = true;
884 self
885 }
886
887 #[must_use]
898 pub fn load(mut self) -> Self {
899 self.load = true;
900 self
901 }
902
903 #[must_use]
914 pub fn metadata_file(mut self, file: impl Into<PathBuf>) -> Self {
915 self.metadata_file = Some(file.into());
916 self
917 }
918
919 #[must_use]
930 pub fn no_cache_filter(mut self, stage: impl Into<String>) -> Self {
931 self.no_cache_filter.push(stage.into());
932 self
933 }
934
935 #[must_use]
946 pub fn progress(mut self, progress_type: impl Into<String>) -> Self {
947 self.progress = Some(progress_type.into());
948 self
949 }
950
951 #[must_use]
962 pub fn provenance(mut self, provenance: impl Into<String>) -> Self {
963 self.provenance = Some(provenance.into());
964 self
965 }
966
967 #[must_use]
978 pub fn push(mut self) -> Self {
979 self.push = true;
980 self
981 }
982
983 #[must_use]
994 pub fn sbom(mut self, sbom: impl Into<String>) -> Self {
995 self.sbom = Some(sbom.into());
996 self
997 }
998
999 #[must_use]
1010 pub fn secret(mut self, secret: impl Into<String>) -> Self {
1011 self.secrets.push(secret.into());
1012 self
1013 }
1014
1015 #[must_use]
1026 pub fn ssh(mut self, ssh: impl Into<String>) -> Self {
1027 self.ssh.push(ssh.into());
1028 self
1029 }
1030
1031 #[must_use]
1033 pub fn get_executor(&self) -> &CommandExecutor {
1034 &self.executor
1035 }
1036
1037 #[must_use]
1039 pub fn get_executor_mut(&mut self) -> &mut CommandExecutor {
1040 &mut self.executor
1041 }
1042}
1043
1044impl Default for BuildCommand {
1045 fn default() -> Self {
1046 Self::new(".")
1047 }
1048}
1049
1050impl BuildCommand {
1051 fn add_basic_args(&self, args: &mut Vec<String>) {
1052 for host in &self.add_hosts {
1054 args.push("--add-host".to_string());
1055 args.push(host.clone());
1056 }
1057
1058 for (key, value) in &self.build_args {
1060 args.push("--build-arg".to_string());
1061 args.push(format!("{key}={value}"));
1062 }
1063
1064 for cache in &self.cache_from {
1066 args.push("--cache-from".to_string());
1067 args.push(cache.clone());
1068 }
1069
1070 if let Some(ref dockerfile) = self.file {
1071 args.push("--file".to_string());
1072 args.push(dockerfile.to_string_lossy().to_string());
1073 }
1074
1075 if self.no_cache {
1076 args.push("--no-cache".to_string());
1077 }
1078
1079 if self.pull {
1080 args.push("--pull".to_string());
1081 }
1082
1083 if self.quiet {
1084 args.push("--quiet".to_string());
1085 }
1086
1087 for tag in &self.tags {
1089 args.push("--tag".to_string());
1090 args.push(tag.clone());
1091 }
1092
1093 if let Some(ref target) = self.target {
1094 args.push("--target".to_string());
1095 args.push(target.clone());
1096 }
1097 }
1098
1099 fn add_resource_args(&self, args: &mut Vec<String>) {
1100 if let Some(period) = self.cpu_period {
1101 args.push("--cpu-period".to_string());
1102 args.push(period.to_string());
1103 }
1104
1105 if let Some(quota) = self.cpu_quota {
1106 args.push("--cpu-quota".to_string());
1107 args.push(quota.to_string());
1108 }
1109
1110 if let Some(shares) = self.cpu_shares {
1111 args.push("--cpu-shares".to_string());
1112 args.push(shares.to_string());
1113 }
1114
1115 if let Some(ref cpus) = self.cpuset_cpus {
1116 args.push("--cpuset-cpus".to_string());
1117 args.push(cpus.clone());
1118 }
1119
1120 if let Some(ref mems) = self.cpuset_mems {
1121 args.push("--cpuset-mems".to_string());
1122 args.push(mems.clone());
1123 }
1124
1125 if let Some(ref memory) = self.memory {
1126 args.push("--memory".to_string());
1127 args.push(memory.clone());
1128 }
1129
1130 if let Some(ref swap) = self.memory_swap {
1131 args.push("--memory-swap".to_string());
1132 args.push(swap.clone());
1133 }
1134
1135 if let Some(ref size) = self.shm_size {
1136 args.push("--shm-size".to_string());
1137 args.push(size.clone());
1138 }
1139 }
1140
1141 fn add_advanced_args(&self, args: &mut Vec<String>) {
1142 self.add_container_args(args);
1143 self.add_metadata_args(args);
1144 self.add_buildx_args(args);
1145 }
1146
1147 fn add_container_args(&self, args: &mut Vec<String>) {
1148 if let Some(ref parent) = self.cgroup_parent {
1149 args.push("--cgroup-parent".to_string());
1150 args.push(parent.clone());
1151 }
1152
1153 if self.compress {
1154 args.push("--compress".to_string());
1155 }
1156
1157 if self.disable_content_trust {
1158 args.push("--disable-content-trust".to_string());
1159 }
1160
1161 if self.force_rm {
1162 args.push("--force-rm".to_string());
1163 }
1164
1165 if let Some(ref file) = self.iidfile {
1166 args.push("--iidfile".to_string());
1167 args.push(file.to_string_lossy().to_string());
1168 }
1169
1170 if let Some(ref isolation) = self.isolation {
1171 args.push("--isolation".to_string());
1172 args.push(isolation.clone());
1173 }
1174
1175 if let Some(ref network) = self.network {
1176 args.push("--network".to_string());
1177 args.push(network.clone());
1178 }
1179
1180 if let Some(ref platform) = self.platform {
1181 args.push("--platform".to_string());
1182 args.push(platform.clone());
1183 }
1184
1185 if !self.rm {
1186 args.push("--rm=false".to_string());
1187 }
1188
1189 for opt in &self.security_opts {
1191 args.push("--security-opt".to_string());
1192 args.push(opt.clone());
1193 }
1194
1195 for limit in &self.ulimits {
1197 args.push("--ulimit".to_string());
1198 args.push(limit.clone());
1199 }
1200 }
1201
1202 fn add_metadata_args(&self, args: &mut Vec<String>) {
1203 for (key, value) in &self.labels {
1205 args.push("--label".to_string());
1206 args.push(format!("{key}={value}"));
1207 }
1208
1209 for annotation in &self.annotations {
1210 args.push("--annotation".to_string());
1211 args.push(annotation.clone());
1212 }
1213
1214 if let Some(ref file) = self.metadata_file {
1215 args.push("--metadata-file".to_string());
1216 args.push(file.to_string_lossy().to_string());
1217 }
1218 }
1219
1220 fn add_buildx_args(&self, args: &mut Vec<String>) {
1221 for allow in &self.allow {
1222 args.push("--allow".to_string());
1223 args.push(allow.clone());
1224 }
1225
1226 for attest in &self.attestations {
1227 args.push("--attest".to_string());
1228 args.push(attest.clone());
1229 }
1230
1231 for context in &self.build_contexts {
1232 args.push("--build-context".to_string());
1233 args.push(context.clone());
1234 }
1235
1236 if let Some(ref builder) = self.builder {
1237 args.push("--builder".to_string());
1238 args.push(builder.clone());
1239 }
1240
1241 for cache in &self.cache_to {
1242 args.push("--cache-to".to_string());
1243 args.push(cache.clone());
1244 }
1245
1246 if let Some(ref call) = self.call {
1247 args.push("--call".to_string());
1248 args.push(call.clone());
1249 }
1250
1251 if self.check {
1252 args.push("--check".to_string());
1253 }
1254
1255 if self.load {
1256 args.push("--load".to_string());
1257 }
1258
1259 for filter in &self.no_cache_filter {
1260 args.push("--no-cache-filter".to_string());
1261 args.push(filter.clone());
1262 }
1263
1264 if let Some(ref progress) = self.progress {
1265 args.push("--progress".to_string());
1266 args.push(progress.clone());
1267 }
1268
1269 if let Some(ref provenance) = self.provenance {
1270 args.push("--provenance".to_string());
1271 args.push(provenance.clone());
1272 }
1273
1274 if self.push {
1275 args.push("--push".to_string());
1276 }
1277
1278 if let Some(ref sbom) = self.sbom {
1279 args.push("--sbom".to_string());
1280 args.push(sbom.clone());
1281 }
1282
1283 for secret in &self.secrets {
1284 args.push("--secret".to_string());
1285 args.push(secret.clone());
1286 }
1287
1288 for ssh in &self.ssh {
1289 args.push("--ssh".to_string());
1290 args.push(ssh.clone());
1291 }
1292 }
1293}
1294
1295#[async_trait]
1296impl DockerCommand for BuildCommand {
1297 type Output = BuildOutput;
1298
1299 fn get_executor(&self) -> &CommandExecutor {
1300 &self.executor
1301 }
1302
1303 fn get_executor_mut(&mut self) -> &mut CommandExecutor {
1304 &mut self.executor
1305 }
1306
1307 fn build_command_args(&self) -> Vec<String> {
1308 let mut args = vec!["build".to_string()];
1309
1310 self.add_basic_args(&mut args);
1311 self.add_resource_args(&mut args);
1312 self.add_advanced_args(&mut args);
1313
1314 args.extend(self.executor.raw_args.clone());
1316
1317 args.push(self.context.clone());
1319
1320 args
1321 }
1322
1323 async fn execute(&self) -> Result<Self::Output> {
1324 let args = self.build_command_args();
1325 let output = self.executor.execute_command("docker", args).await?;
1326
1327 let image_id = if self.quiet {
1329 Some(output.stdout.trim().to_string())
1331 } else {
1332 let combined = if output.stderr.is_empty() {
1333 output.stdout.clone()
1334 } else if output.stdout.is_empty() {
1335 output.stderr.clone()
1336 } else {
1337 format!("{}\n{}", output.stdout, output.stderr)
1338 };
1339 BuildOutput::extract_image_id(&combined)
1340 };
1341
1342 Ok(BuildOutput {
1343 stdout: output.stdout,
1344 stderr: output.stderr,
1345 exit_code: output.exit_code,
1346 image_id,
1347 })
1348 }
1349}
1350
1351#[async_trait]
1353impl StreamableCommand for BuildCommand {
1354 async fn stream<F>(&self, handler: F) -> Result<StreamResult>
1355 where
1356 F: FnMut(OutputLine) + Send + 'static,
1357 {
1358 let mut cmd = TokioCommand::new("docker");
1359
1360 for arg in self.build_command_args() {
1361 cmd.arg(arg);
1362 }
1363
1364 crate::stream::stream_command(cmd, handler).await
1365 }
1366
1367 async fn stream_channel(&self) -> Result<(mpsc::Receiver<OutputLine>, StreamResult)> {
1368 let mut cmd = TokioCommand::new("docker");
1369
1370 for arg in self.build_command_args() {
1371 cmd.arg(arg);
1372 }
1373
1374 crate::stream::stream_command_channel(cmd).await
1375 }
1376}
1377
1378impl BuildCommand {
1379 pub async fn stream<F>(&self, handler: F) -> Result<StreamResult>
1400 where
1401 F: FnMut(OutputLine) + Send + 'static,
1402 {
1403 <Self as StreamableCommand>::stream(self, handler).await
1404 }
1405}
1406
1407#[cfg(test)]
1408mod tests {
1409 use super::*;
1410
1411 #[test]
1412 fn test_build_command_basic() {
1413 let cmd = BuildCommand::new(".").tag("myapp:latest").no_cache().pull();
1414
1415 let args = cmd.build_command_args();
1416
1417 assert!(args.contains(&"--tag".to_string()));
1418 assert!(args.contains(&"myapp:latest".to_string()));
1419 assert!(args.contains(&"--no-cache".to_string()));
1420 assert!(args.contains(&"--pull".to_string()));
1421 assert!(args.contains(&".".to_string()));
1422 }
1423
1424 #[test]
1425 fn test_build_command_with_dockerfile() {
1426 let cmd = BuildCommand::new("/path/to/context")
1427 .file("Dockerfile.prod")
1428 .tag("myapp:prod");
1429
1430 let args = cmd.build_command_args();
1431
1432 assert!(args.contains(&"--file".to_string()));
1433 assert!(args.contains(&"Dockerfile.prod".to_string()));
1434 assert!(args.contains(&"--tag".to_string()));
1435 assert!(args.contains(&"myapp:prod".to_string()));
1436 assert!(args.contains(&"/path/to/context".to_string()));
1437 }
1438
1439 #[test]
1440 fn test_build_command_with_build_args() {
1441 let mut build_args = HashMap::new();
1442 build_args.insert("VERSION".to_string(), "1.0.0".to_string());
1443 build_args.insert("DEBUG".to_string(), "true".to_string());
1444
1445 let cmd = BuildCommand::new(".")
1446 .build_args_map(build_args)
1447 .build_arg("EXTRA", "value");
1448
1449 let args = cmd.build_command_args();
1450
1451 assert!(args.contains(&"--build-arg".to_string()));
1452 assert!(args.contains(&"VERSION=1.0.0".to_string()));
1453 assert!(args.contains(&"DEBUG=true".to_string()));
1454 assert!(args.contains(&"EXTRA=value".to_string()));
1455 }
1456
1457 #[test]
1458 fn test_build_command_with_labels() {
1459 let cmd = BuildCommand::new(".")
1460 .label("version", "1.0.0")
1461 .label("maintainer", "team@example.com");
1462
1463 let args = cmd.build_command_args();
1464
1465 assert!(args.contains(&"--label".to_string()));
1466 assert!(args.contains(&"version=1.0.0".to_string()));
1467 assert!(args.contains(&"maintainer=team@example.com".to_string()));
1468 }
1469
1470 #[test]
1471 fn test_build_command_resource_limits() {
1472 let cmd = BuildCommand::new(".")
1473 .memory("1g")
1474 .cpu_shares(512)
1475 .cpuset_cpus("0-3");
1476
1477 let args = cmd.build_command_args();
1478
1479 assert!(args.contains(&"--memory".to_string()));
1480 assert!(args.contains(&"1g".to_string()));
1481 assert!(args.contains(&"--cpu-shares".to_string()));
1482 assert!(args.contains(&"512".to_string()));
1483 assert!(args.contains(&"--cpuset-cpus".to_string()));
1484 assert!(args.contains(&"0-3".to_string()));
1485 }
1486
1487 #[test]
1488 fn test_build_command_advanced_options() {
1489 let cmd = BuildCommand::new(".")
1490 .platform("linux/amd64")
1491 .target("production")
1492 .network("host")
1493 .cache_from("myapp:cache")
1494 .security_opt("seccomp=unconfined");
1495
1496 let args = cmd.build_command_args();
1497
1498 assert!(args.contains(&"--platform".to_string()));
1499 assert!(args.contains(&"linux/amd64".to_string()));
1500 assert!(args.contains(&"--target".to_string()));
1501 assert!(args.contains(&"production".to_string()));
1502 assert!(args.contains(&"--network".to_string()));
1503 assert!(args.contains(&"host".to_string()));
1504 assert!(args.contains(&"--cache-from".to_string()));
1505 assert!(args.contains(&"myapp:cache".to_string()));
1506 assert!(args.contains(&"--security-opt".to_string()));
1507 assert!(args.contains(&"seccomp=unconfined".to_string()));
1508 }
1509
1510 #[test]
1511 fn test_build_command_multiple_tags() {
1512 let tags = vec!["myapp:latest".to_string(), "myapp:1.0.0".to_string()];
1513 let cmd = BuildCommand::new(".").tags(tags);
1514
1515 let args = cmd.build_command_args();
1516
1517 let tag_count = args.iter().filter(|&arg| arg == "--tag").count();
1518 assert_eq!(tag_count, 2);
1519 assert!(args.contains(&"myapp:latest".to_string()));
1520 assert!(args.contains(&"myapp:1.0.0".to_string()));
1521 }
1522
1523 #[test]
1524 fn test_build_command_quiet_mode() {
1525 let cmd = BuildCommand::new(".").quiet().tag("myapp:test");
1526
1527 let args = cmd.build_command_args();
1528
1529 assert!(args.contains(&"--quiet".to_string()));
1530 assert!(args.contains(&"--tag".to_string()));
1531 assert!(args.contains(&"myapp:test".to_string()));
1532 }
1533
1534 #[test]
1535 fn test_build_command_no_rm() {
1536 let cmd = BuildCommand::new(".").no_rm();
1537
1538 let args = cmd.build_command_args();
1539
1540 assert!(args.contains(&"--rm=false".to_string()));
1541 }
1542
1543 #[test]
1544 fn test_build_command_context_position() {
1545 let cmd = BuildCommand::new("/custom/context")
1546 .tag("test:latest")
1547 .no_cache();
1548
1549 let args = cmd.build_command_args();
1550
1551 assert_eq!(args.last(), Some(&"/custom/context".to_string()));
1553 }
1554
1555 #[test]
1556 fn test_build_output_helpers() {
1557 let output = BuildOutput {
1558 stdout: "Successfully built abc123def456".to_string(),
1559 stderr: String::new(),
1560 exit_code: 0,
1561 image_id: Some("abc123def456".to_string()),
1562 };
1563
1564 assert!(output.success());
1565 assert!(!output.stdout_is_empty());
1566 assert!(output.stderr_is_empty());
1567 assert_eq!(output.image_id, Some("abc123def456".to_string()));
1568 }
1569
1570 #[test]
1571 fn test_build_command_extensibility() {
1572 let mut cmd = BuildCommand::new(".");
1573
1574 cmd.flag("--some-flag");
1576 cmd.option("--some-option", "value");
1577 cmd.arg("extra-arg");
1578
1579 let args = cmd.build_command_args();
1580
1581 assert!(args.contains(&"--some-flag".to_string()));
1582 assert!(args.contains(&"--some-option".to_string()));
1583 assert!(args.contains(&"value".to_string()));
1584 assert!(args.contains(&"extra-arg".to_string()));
1585 }
1586
1587 #[test]
1588 fn test_image_id_extraction() {
1589 let output1 = "Step 1/3 : FROM alpine\nSuccessfully built abc123def456\n";
1590 let id1 = BuildOutput::extract_image_id(output1);
1591 assert_eq!(id1, Some("abc123def456".to_string()));
1592
1593 let output2 = "sha256:1234567890abcdef\n";
1594 let id2 = BuildOutput::extract_image_id(output2);
1595 assert_eq!(id2, Some("sha256:1234567890abcdef".to_string()));
1596
1597 let output3 = "No image ID found here";
1598 let id3 = BuildOutput::extract_image_id(output3);
1599 assert_eq!(id3, None);
1600 }
1601
1602 #[test]
1603 fn test_build_command_modern_buildx_options() {
1604 let cmd = BuildCommand::new(".")
1605 .allow("network.host")
1606 .annotation("org.opencontainers.image.title=MyApp")
1607 .attest("type=provenance,mode=max")
1608 .build_context("mycontext=../path")
1609 .builder("mybuilder")
1610 .cache_to("type=registry,ref=myregistry/cache")
1611 .call("check")
1612 .check()
1613 .load()
1614 .metadata_file("/tmp/metadata.json")
1615 .no_cache_filter("build-stage")
1616 .progress("plain")
1617 .provenance("mode=max")
1618 .push()
1619 .sbom("generator=image")
1620 .secret("id=mysecret,src=/local/secret")
1621 .ssh("default");
1622
1623 let args = cmd.build_command_args();
1624
1625 assert!(args.contains(&"--allow".to_string()));
1626 assert!(args.contains(&"network.host".to_string()));
1627 assert!(args.contains(&"--annotation".to_string()));
1628 assert!(args.contains(&"org.opencontainers.image.title=MyApp".to_string()));
1629 assert!(args.contains(&"--attest".to_string()));
1630 assert!(args.contains(&"type=provenance,mode=max".to_string()));
1631 assert!(args.contains(&"--build-context".to_string()));
1632 assert!(args.contains(&"mycontext=../path".to_string()));
1633 assert!(args.contains(&"--builder".to_string()));
1634 assert!(args.contains(&"mybuilder".to_string()));
1635 assert!(args.contains(&"--cache-to".to_string()));
1636 assert!(args.contains(&"type=registry,ref=myregistry/cache".to_string()));
1637 assert!(args.contains(&"--call".to_string()));
1638 assert!(args.contains(&"check".to_string()));
1639 assert!(args.contains(&"--check".to_string()));
1640 assert!(args.contains(&"--load".to_string()));
1641 assert!(args.contains(&"--metadata-file".to_string()));
1642 assert!(args.contains(&"/tmp/metadata.json".to_string()));
1643 assert!(args.contains(&"--no-cache-filter".to_string()));
1644 assert!(args.contains(&"build-stage".to_string()));
1645 assert!(args.contains(&"--progress".to_string()));
1646 assert!(args.contains(&"plain".to_string()));
1647 assert!(args.contains(&"--provenance".to_string()));
1648 assert!(args.contains(&"mode=max".to_string()));
1649 assert!(args.contains(&"--push".to_string()));
1650 assert!(args.contains(&"--sbom".to_string()));
1651 assert!(args.contains(&"generator=image".to_string()));
1652 assert!(args.contains(&"--secret".to_string()));
1653 assert!(args.contains(&"id=mysecret,src=/local/secret".to_string()));
1654 assert!(args.contains(&"--ssh".to_string()));
1655 assert!(args.contains(&"default".to_string()));
1656 }
1657}