docker_wrapper/command/
build.rs

1//! Docker build command implementation.
2//!
3//! This module provides a comprehensive implementation of the `docker build` command
4//! with support for all native options and an extensible architecture for any additional options.
5
6use 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/// Docker build command builder with fluent API
16#[derive(Debug, Clone)]
17#[allow(clippy::struct_excessive_bools)]
18pub struct BuildCommand {
19    /// Build context (path, URL, or stdin)
20    context: String,
21    /// Command executor for extensibility
22    pub executor: CommandExecutor,
23    /// Custom host-to-IP mappings
24    add_hosts: Vec<String>,
25    /// Build-time variables
26    build_args: HashMap<String, String>,
27    /// Images to consider as cache sources
28    cache_from: Vec<String>,
29    /// Parent cgroup for RUN instructions
30    cgroup_parent: Option<String>,
31    /// Compress the build context using gzip
32    compress: bool,
33    /// CPU limits
34    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    /// Skip image verification
40    disable_content_trust: bool,
41    /// Name of the Dockerfile
42    file: Option<PathBuf>,
43    /// Always remove intermediate containers
44    force_rm: bool,
45    /// Write the image ID to file
46    iidfile: Option<PathBuf>,
47    /// Container isolation technology
48    isolation: Option<String>,
49    /// Set metadata for an image
50    labels: HashMap<String, String>,
51    /// Memory limit
52    memory: Option<String>,
53    /// Memory + swap limit
54    memory_swap: Option<String>,
55    /// Networking mode for RUN instructions
56    network: Option<String>,
57    /// Do not use cache when building
58    no_cache: bool,
59    /// Set platform for multi-platform builds
60    platform: Option<String>,
61    /// Always attempt to pull newer base images
62    pull: bool,
63    /// Suppress build output and print image ID on success
64    quiet: bool,
65    /// Remove intermediate containers after successful build
66    rm: bool,
67    /// Security options
68    security_opts: Vec<String>,
69    /// Size of /dev/shm
70    shm_size: Option<String>,
71    /// Name and tag for the image
72    tags: Vec<String>,
73    /// Target build stage
74    target: Option<String>,
75    /// Ulimit options
76    ulimits: Vec<String>,
77    /// Extra privileged entitlements
78    allow: Vec<String>,
79    /// Annotations to add to the image
80    annotations: Vec<String>,
81    /// Attestation parameters
82    attestations: Vec<String>,
83    /// Additional build contexts
84    build_contexts: Vec<String>,
85    /// Override the configured builder
86    builder: Option<String>,
87    /// Cache export destinations
88    cache_to: Vec<String>,
89    /// Method for evaluating build
90    call: Option<String>,
91    /// Shorthand for "--call=check"
92    check: bool,
93    /// Shorthand for "--output=type=docker"
94    load: bool,
95    /// Write build result metadata to file
96    metadata_file: Option<PathBuf>,
97    /// Do not cache specified stages
98    no_cache_filter: Vec<String>,
99    /// Type of progress output
100    progress: Option<String>,
101    /// Shorthand for "--attest=type=provenance"
102    provenance: Option<String>,
103    /// Shorthand for "--output=type=registry"
104    push: bool,
105    /// Shorthand for "--attest=type=sbom"
106    sbom: Option<String>,
107    /// Secrets to expose to the build
108    secrets: Vec<String>,
109    /// SSH agent socket or keys to expose
110    ssh: Vec<String>,
111}
112
113/// Output from docker build command
114#[derive(Debug, Clone)]
115pub struct BuildOutput {
116    /// The raw stdout from the command
117    pub stdout: String,
118    /// The raw stderr from the command
119    pub stderr: String,
120    /// Exit code from the command
121    pub exit_code: i32,
122    /// Built image ID (extracted from output)
123    pub image_id: Option<String>,
124}
125
126impl BuildOutput {
127    /// Check if the build executed successfully
128    #[must_use]
129    pub fn success(&self) -> bool {
130        self.exit_code == 0
131    }
132
133    /// Get combined output (stdout + stderr)
134    #[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    /// Check if stdout is empty (ignoring whitespace)
146    #[must_use]
147    pub fn stdout_is_empty(&self) -> bool {
148        self.stdout.trim().is_empty()
149    }
150
151    /// Check if stderr is empty (ignoring whitespace)
152    #[must_use]
153    pub fn stderr_is_empty(&self) -> bool {
154        self.stderr.trim().is_empty()
155    }
156
157    /// Extract image ID from build output (best effort)
158    fn extract_image_id(output: &str) -> Option<String> {
159        // Look for patterns like "Successfully built abc123def456" or "sha256:..."
160        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    /// Create a new build command for the specified context
176    ///
177    /// # Examples
178    ///
179    /// ```
180    /// use docker_wrapper::BuildCommand;
181    ///
182    /// let build_cmd = BuildCommand::new(".");
183    /// ```
184    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, // Default is true
212            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    /// Add a custom host-to-IP mapping
238    ///
239    /// # Examples
240    ///
241    /// ```
242    /// use docker_wrapper::BuildCommand;
243    ///
244    /// let build_cmd = BuildCommand::new(".")
245    ///     .add_host("myhost:192.168.1.100");
246    /// ```
247    #[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    /// Set a build-time variable
254    ///
255    /// # Examples
256    ///
257    /// ```
258    /// use docker_wrapper::BuildCommand;
259    ///
260    /// let build_cmd = BuildCommand::new(".")
261    ///     .build_arg("VERSION", "1.0.0")
262    ///     .build_arg("DEBUG", "true");
263    /// ```
264    #[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    /// Set multiple build-time variables
271    ///
272    /// # Examples
273    ///
274    /// ```
275    /// use docker_wrapper::BuildCommand;
276    /// use std::collections::HashMap;
277    ///
278    /// let mut args = HashMap::new();
279    /// args.insert("VERSION".to_string(), "1.0.0".to_string());
280    /// args.insert("DEBUG".to_string(), "true".to_string());
281    ///
282    /// let build_cmd = BuildCommand::new(".").build_args_map(args);
283    /// ```
284    #[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    /// Add an image to consider as cache source
291    ///
292    /// # Examples
293    ///
294    /// ```
295    /// use docker_wrapper::BuildCommand;
296    ///
297    /// let build_cmd = BuildCommand::new(".")
298    ///     .cache_from("myapp:cache");
299    /// ```
300    #[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    /// Set the parent cgroup for RUN instructions
307    ///
308    /// # Examples
309    ///
310    /// ```
311    /// use docker_wrapper::BuildCommand;
312    ///
313    /// let build_cmd = BuildCommand::new(".")
314    ///     .cgroup_parent("/docker");
315    /// ```
316    #[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    /// Compress the build context using gzip
323    ///
324    /// # Examples
325    ///
326    /// ```
327    /// use docker_wrapper::BuildCommand;
328    ///
329    /// let build_cmd = BuildCommand::new(".").compress();
330    /// ```
331    #[must_use]
332    pub fn compress(mut self) -> Self {
333        self.compress = true;
334        self
335    }
336
337    /// Set CPU period limit
338    ///
339    /// # Examples
340    ///
341    /// ```
342    /// use docker_wrapper::BuildCommand;
343    ///
344    /// let build_cmd = BuildCommand::new(".")
345    ///     .cpu_period(100000);
346    /// ```
347    #[must_use]
348    pub fn cpu_period(mut self, period: i64) -> Self {
349        self.cpu_period = Some(period);
350        self
351    }
352
353    /// Set CPU quota limit
354    ///
355    /// # Examples
356    ///
357    /// ```
358    /// use docker_wrapper::BuildCommand;
359    ///
360    /// let build_cmd = BuildCommand::new(".")
361    ///     .cpu_quota(50000);
362    /// ```
363    #[must_use]
364    pub fn cpu_quota(mut self, quota: i64) -> Self {
365        self.cpu_quota = Some(quota);
366        self
367    }
368
369    /// Set CPU shares (relative weight)
370    ///
371    /// # Examples
372    ///
373    /// ```
374    /// use docker_wrapper::BuildCommand;
375    ///
376    /// let build_cmd = BuildCommand::new(".")
377    ///     .cpu_shares(512);
378    /// ```
379    #[must_use]
380    pub fn cpu_shares(mut self, shares: i64) -> Self {
381        self.cpu_shares = Some(shares);
382        self
383    }
384
385    /// Set CPUs in which to allow execution
386    ///
387    /// # Examples
388    ///
389    /// ```
390    /// use docker_wrapper::BuildCommand;
391    ///
392    /// let build_cmd = BuildCommand::new(".")
393    ///     .cpuset_cpus("0-3");
394    /// ```
395    #[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    /// Set MEMs in which to allow execution
402    ///
403    /// # Examples
404    ///
405    /// ```
406    /// use docker_wrapper::BuildCommand;
407    ///
408    /// let build_cmd = BuildCommand::new(".")
409    ///     .cpuset_mems("0-1");
410    /// ```
411    #[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    /// Skip image verification
418    ///
419    /// # Examples
420    ///
421    /// ```
422    /// use docker_wrapper::BuildCommand;
423    ///
424    /// let build_cmd = BuildCommand::new(".")
425    ///     .disable_content_trust();
426    /// ```
427    #[must_use]
428    pub fn disable_content_trust(mut self) -> Self {
429        self.disable_content_trust = true;
430        self
431    }
432
433    /// Set the name/path of the Dockerfile
434    ///
435    /// # Examples
436    ///
437    /// ```
438    /// use docker_wrapper::BuildCommand;
439    ///
440    /// let build_cmd = BuildCommand::new(".")
441    ///     .file("Dockerfile.prod");
442    /// ```
443    #[must_use]
444    pub fn file(mut self, dockerfile: impl Into<PathBuf>) -> Self {
445        self.file = Some(dockerfile.into());
446        self
447    }
448
449    /// Always remove intermediate containers
450    ///
451    /// # Examples
452    ///
453    /// ```
454    /// use docker_wrapper::BuildCommand;
455    ///
456    /// let build_cmd = BuildCommand::new(".")
457    ///     .force_rm();
458    /// ```
459    #[must_use]
460    pub fn force_rm(mut self) -> Self {
461        self.force_rm = true;
462        self
463    }
464
465    /// Write the image ID to a file
466    ///
467    /// # Examples
468    ///
469    /// ```
470    /// use docker_wrapper::BuildCommand;
471    ///
472    /// let build_cmd = BuildCommand::new(".")
473    ///     .iidfile("/tmp/image_id.txt");
474    /// ```
475    #[must_use]
476    pub fn iidfile(mut self, file: impl Into<PathBuf>) -> Self {
477        self.iidfile = Some(file.into());
478        self
479    }
480
481    /// Set container isolation technology
482    ///
483    /// # Examples
484    ///
485    /// ```
486    /// use docker_wrapper::BuildCommand;
487    ///
488    /// let build_cmd = BuildCommand::new(".")
489    ///     .isolation("hyperv");
490    /// ```
491    #[must_use]
492    pub fn isolation(mut self, isolation: impl Into<String>) -> Self {
493        self.isolation = Some(isolation.into());
494        self
495    }
496
497    /// Set metadata label for the image
498    ///
499    /// # Examples
500    ///
501    /// ```
502    /// use docker_wrapper::BuildCommand;
503    ///
504    /// let build_cmd = BuildCommand::new(".")
505    ///     .label("version", "1.0.0")
506    ///     .label("maintainer", "team@example.com");
507    /// ```
508    #[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    /// Set multiple metadata labels
515    ///
516    /// # Examples
517    ///
518    /// ```
519    /// use docker_wrapper::BuildCommand;
520    /// use std::collections::HashMap;
521    ///
522    /// let mut labels = HashMap::new();
523    /// labels.insert("version".to_string(), "1.0.0".to_string());
524    /// labels.insert("env".to_string(), "production".to_string());
525    ///
526    /// let build_cmd = BuildCommand::new(".").labels(labels);
527    /// ```
528    #[must_use]
529    pub fn labels(mut self, labels: HashMap<String, String>) -> Self {
530        self.labels.extend(labels);
531        self
532    }
533
534    /// Set memory limit
535    ///
536    /// # Examples
537    ///
538    /// ```
539    /// use docker_wrapper::BuildCommand;
540    ///
541    /// let build_cmd = BuildCommand::new(".")
542    ///     .memory("1g");
543    /// ```
544    #[must_use]
545    pub fn memory(mut self, limit: impl Into<String>) -> Self {
546        self.memory = Some(limit.into());
547        self
548    }
549
550    /// Set memory + swap limit
551    ///
552    /// # Examples
553    ///
554    /// ```
555    /// use docker_wrapper::BuildCommand;
556    ///
557    /// let build_cmd = BuildCommand::new(".")
558    ///     .memory_swap("2g");
559    /// ```
560    #[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    /// Set networking mode for RUN instructions
567    ///
568    /// # Examples
569    ///
570    /// ```
571    /// use docker_wrapper::BuildCommand;
572    ///
573    /// let build_cmd = BuildCommand::new(".")
574    ///     .network("host");
575    /// ```
576    #[must_use]
577    pub fn network(mut self, mode: impl Into<String>) -> Self {
578        self.network = Some(mode.into());
579        self
580    }
581
582    /// Do not use cache when building
583    ///
584    /// # Examples
585    ///
586    /// ```
587    /// use docker_wrapper::BuildCommand;
588    ///
589    /// let build_cmd = BuildCommand::new(".")
590    ///     .no_cache();
591    /// ```
592    #[must_use]
593    pub fn no_cache(mut self) -> Self {
594        self.no_cache = true;
595        self
596    }
597
598    /// Set platform for multi-platform builds
599    ///
600    /// # Examples
601    ///
602    /// ```
603    /// use docker_wrapper::BuildCommand;
604    ///
605    /// let build_cmd = BuildCommand::new(".")
606    ///     .platform("linux/amd64");
607    /// ```
608    #[must_use]
609    pub fn platform(mut self, platform: impl Into<String>) -> Self {
610        self.platform = Some(platform.into());
611        self
612    }
613
614    /// Always attempt to pull newer base images
615    ///
616    /// # Examples
617    ///
618    /// ```
619    /// use docker_wrapper::BuildCommand;
620    ///
621    /// let build_cmd = BuildCommand::new(".")
622    ///     .pull();
623    /// ```
624    #[must_use]
625    pub fn pull(mut self) -> Self {
626        self.pull = true;
627        self
628    }
629
630    /// Suppress build output and print image ID on success
631    ///
632    /// # Examples
633    ///
634    /// ```
635    /// use docker_wrapper::BuildCommand;
636    ///
637    /// let build_cmd = BuildCommand::new(".")
638    ///     .quiet();
639    /// ```
640    #[must_use]
641    pub fn quiet(mut self) -> Self {
642        self.quiet = true;
643        self
644    }
645
646    /// Remove intermediate containers after successful build (default: true)
647    ///
648    /// # Examples
649    ///
650    /// ```
651    /// use docker_wrapper::BuildCommand;
652    ///
653    /// let build_cmd = BuildCommand::new(".")
654    ///     .no_rm(); // Don't remove intermediate containers
655    /// ```
656    #[must_use]
657    pub fn no_rm(mut self) -> Self {
658        self.rm = false;
659        self
660    }
661
662    /// Add a security option
663    ///
664    /// # Examples
665    ///
666    /// ```
667    /// use docker_wrapper::BuildCommand;
668    ///
669    /// let build_cmd = BuildCommand::new(".")
670    ///     .security_opt("seccomp=unconfined");
671    /// ```
672    #[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    /// Set size of /dev/shm
679    ///
680    /// # Examples
681    ///
682    /// ```
683    /// use docker_wrapper::BuildCommand;
684    ///
685    /// let build_cmd = BuildCommand::new(".")
686    ///     .shm_size("128m");
687    /// ```
688    #[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    /// Add a name and tag for the image
695    ///
696    /// # Examples
697    ///
698    /// ```
699    /// use docker_wrapper::BuildCommand;
700    ///
701    /// let build_cmd = BuildCommand::new(".")
702    ///     .tag("myapp:latest")
703    ///     .tag("myapp:1.0.0");
704    /// ```
705    #[must_use]
706    pub fn tag(mut self, tag: impl Into<String>) -> Self {
707        self.tags.push(tag.into());
708        self
709    }
710
711    /// Set multiple tags for the image
712    ///
713    /// # Examples
714    ///
715    /// ```
716    /// use docker_wrapper::BuildCommand;
717    ///
718    /// let tags = vec!["myapp:latest".to_string(), "myapp:1.0.0".to_string()];
719    /// let build_cmd = BuildCommand::new(".").tags(tags);
720    /// ```
721    #[must_use]
722    pub fn tags(mut self, tags: Vec<String>) -> Self {
723        self.tags.extend(tags);
724        self
725    }
726
727    /// Set the target build stage
728    ///
729    /// # Examples
730    ///
731    /// ```
732    /// use docker_wrapper::BuildCommand;
733    ///
734    /// let build_cmd = BuildCommand::new(".")
735    ///     .target("production");
736    /// ```
737    #[must_use]
738    pub fn target(mut self, stage: impl Into<String>) -> Self {
739        self.target = Some(stage.into());
740        self
741    }
742
743    /// Add a ulimit option
744    ///
745    /// # Examples
746    ///
747    /// ```
748    /// use docker_wrapper::BuildCommand;
749    ///
750    /// let build_cmd = BuildCommand::new(".")
751    ///     .ulimit("nofile=65536:65536");
752    /// ```
753    #[must_use]
754    pub fn ulimit(mut self, limit: impl Into<String>) -> Self {
755        self.ulimits.push(limit.into());
756        self
757    }
758
759    /// Add an extra privileged entitlement
760    ///
761    /// # Examples
762    ///
763    /// ```
764    /// use docker_wrapper::BuildCommand;
765    ///
766    /// let build_cmd = BuildCommand::new(".")
767    ///     .allow("network.host");
768    /// ```
769    #[must_use]
770    pub fn allow(mut self, entitlement: impl Into<String>) -> Self {
771        self.allow.push(entitlement.into());
772        self
773    }
774
775    /// Add an annotation to the image
776    ///
777    /// # Examples
778    ///
779    /// ```
780    /// use docker_wrapper::BuildCommand;
781    ///
782    /// let build_cmd = BuildCommand::new(".")
783    ///     .annotation("org.opencontainers.image.title=MyApp");
784    /// ```
785    #[must_use]
786    pub fn annotation(mut self, annotation: impl Into<String>) -> Self {
787        self.annotations.push(annotation.into());
788        self
789    }
790
791    /// Add attestation parameters
792    ///
793    /// # Examples
794    ///
795    /// ```
796    /// use docker_wrapper::BuildCommand;
797    ///
798    /// let build_cmd = BuildCommand::new(".")
799    ///     .attest("type=provenance,mode=max");
800    /// ```
801    #[must_use]
802    pub fn attest(mut self, attestation: impl Into<String>) -> Self {
803        self.attestations.push(attestation.into());
804        self
805    }
806
807    /// Add additional build context
808    ///
809    /// # Examples
810    ///
811    /// ```
812    /// use docker_wrapper::BuildCommand;
813    ///
814    /// let build_cmd = BuildCommand::new(".")
815    ///     .build_context("mycontext=../path");
816    /// ```
817    #[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    /// Override the configured builder
824    ///
825    /// # Examples
826    ///
827    /// ```
828    /// use docker_wrapper::BuildCommand;
829    ///
830    /// let build_cmd = BuildCommand::new(".")
831    ///     .builder("mybuilder");
832    /// ```
833    #[must_use]
834    pub fn builder(mut self, builder: impl Into<String>) -> Self {
835        self.builder = Some(builder.into());
836        self
837    }
838
839    /// Add cache export destination
840    ///
841    /// # Examples
842    ///
843    /// ```
844    /// use docker_wrapper::BuildCommand;
845    ///
846    /// let build_cmd = BuildCommand::new(".")
847    ///     .cache_to("type=registry,ref=myregistry/cache");
848    /// ```
849    #[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    /// Set method for evaluating build
856    ///
857    /// # Examples
858    ///
859    /// ```
860    /// use docker_wrapper::BuildCommand;
861    ///
862    /// let build_cmd = BuildCommand::new(".")
863    ///     .call("check");
864    /// ```
865    #[must_use]
866    pub fn call(mut self, method: impl Into<String>) -> Self {
867        self.call = Some(method.into());
868        self
869    }
870
871    /// Enable check mode (shorthand for "--call=check")
872    ///
873    /// # Examples
874    ///
875    /// ```
876    /// use docker_wrapper::BuildCommand;
877    ///
878    /// let build_cmd = BuildCommand::new(".")
879    ///     .check();
880    /// ```
881    #[must_use]
882    pub fn check(mut self) -> Self {
883        self.check = true;
884        self
885    }
886
887    /// Enable load mode (shorthand for "--output=type=docker")
888    ///
889    /// # Examples
890    ///
891    /// ```
892    /// use docker_wrapper::BuildCommand;
893    ///
894    /// let build_cmd = BuildCommand::new(".")
895    ///     .load();
896    /// ```
897    #[must_use]
898    pub fn load(mut self) -> Self {
899        self.load = true;
900        self
901    }
902
903    /// Write build result metadata to file
904    ///
905    /// # Examples
906    ///
907    /// ```
908    /// use docker_wrapper::BuildCommand;
909    ///
910    /// let build_cmd = BuildCommand::new(".")
911    ///     .metadata_file("/tmp/metadata.json");
912    /// ```
913    #[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    /// Do not cache specified stage
920    ///
921    /// # Examples
922    ///
923    /// ```
924    /// use docker_wrapper::BuildCommand;
925    ///
926    /// let build_cmd = BuildCommand::new(".")
927    ///     .no_cache_filter("build-stage");
928    /// ```
929    #[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    /// Set type of progress output
936    ///
937    /// # Examples
938    ///
939    /// ```
940    /// use docker_wrapper::BuildCommand;
941    ///
942    /// let build_cmd = BuildCommand::new(".")
943    ///     .progress("plain");
944    /// ```
945    #[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    /// Set provenance attestation (shorthand for "--attest=type=provenance")
952    ///
953    /// # Examples
954    ///
955    /// ```
956    /// use docker_wrapper::BuildCommand;
957    ///
958    /// let build_cmd = BuildCommand::new(".")
959    ///     .provenance("mode=max");
960    /// ```
961    #[must_use]
962    pub fn provenance(mut self, provenance: impl Into<String>) -> Self {
963        self.provenance = Some(provenance.into());
964        self
965    }
966
967    /// Enable push mode (shorthand for "--output=type=registry")
968    ///
969    /// # Examples
970    ///
971    /// ```
972    /// use docker_wrapper::BuildCommand;
973    ///
974    /// let build_cmd = BuildCommand::new(".")
975    ///     .push();
976    /// ```
977    #[must_use]
978    pub fn push(mut self) -> Self {
979        self.push = true;
980        self
981    }
982
983    /// Set SBOM attestation (shorthand for "--attest=type=sbom")
984    ///
985    /// # Examples
986    ///
987    /// ```
988    /// use docker_wrapper::BuildCommand;
989    ///
990    /// let build_cmd = BuildCommand::new(".")
991    ///     .sbom("generator=image");
992    /// ```
993    #[must_use]
994    pub fn sbom(mut self, sbom: impl Into<String>) -> Self {
995        self.sbom = Some(sbom.into());
996        self
997    }
998
999    /// Add secret to expose to the build
1000    ///
1001    /// # Examples
1002    ///
1003    /// ```
1004    /// use docker_wrapper::BuildCommand;
1005    ///
1006    /// let build_cmd = BuildCommand::new(".")
1007    ///     .secret("id=mysecret,src=/local/secret");
1008    /// ```
1009    #[must_use]
1010    pub fn secret(mut self, secret: impl Into<String>) -> Self {
1011        self.secrets.push(secret.into());
1012        self
1013    }
1014
1015    /// Add SSH agent socket or keys to expose
1016    ///
1017    /// # Examples
1018    ///
1019    /// ```
1020    /// use docker_wrapper::BuildCommand;
1021    ///
1022    /// let build_cmd = BuildCommand::new(".")
1023    ///     .ssh("default");
1024    /// ```
1025    #[must_use]
1026    pub fn ssh(mut self, ssh: impl Into<String>) -> Self {
1027        self.ssh.push(ssh.into());
1028        self
1029    }
1030
1031    /// Get a reference to the command executor
1032    #[must_use]
1033    pub fn get_executor(&self) -> &CommandExecutor {
1034        &self.executor
1035    }
1036
1037    /// Get a mutable reference to the command executor  
1038    #[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        // Add host mappings
1053        for host in &self.add_hosts {
1054            args.push("--add-host".to_string());
1055            args.push(host.clone());
1056        }
1057
1058        // Add build arguments
1059        for (key, value) in &self.build_args {
1060            args.push("--build-arg".to_string());
1061            args.push(format!("{key}={value}"));
1062        }
1063
1064        // Add cache sources
1065        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        // Add tags
1088        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        // Add security options
1190        for opt in &self.security_opts {
1191            args.push("--security-opt".to_string());
1192            args.push(opt.clone());
1193        }
1194
1195        // Add ulimits
1196        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        // Add labels
1204        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        // Add any additional raw arguments
1315        args.extend(self.executor.raw_args.clone());
1316
1317        // Add build context (must be last)
1318        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        // Extract image ID from output
1328        let image_id = if self.quiet {
1329            // In quiet mode, the output should be just the image ID
1330            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// Streaming support for BuildCommand
1352#[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    /// Run the build command with streaming output
1380    ///
1381    /// # Examples
1382    ///
1383    /// ```no_run
1384    /// use docker_wrapper::BuildCommand;
1385    /// use docker_wrapper::StreamHandler;
1386    ///
1387    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1388    /// let result = BuildCommand::new(".")
1389    ///     .tag("myapp:latest")
1390    ///     .stream(StreamHandler::print())
1391    ///     .await?;
1392    /// # Ok(())
1393    /// # }
1394    /// ```
1395    ///
1396    /// # Errors
1397    ///
1398    /// Returns an error if the build fails or encounters an I/O error
1399    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        // Context should be the last argument
1552        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        // Test extensibility methods
1575        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}