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