docker_wrapper/command/builder/
build.rs

1//! Docker builder build command
2//!
3//! Alternative interface to start a build (similar to `docker build`)
4
5use crate::command::build::{BuildCommand, BuildOutput};
6use crate::command::{CommandExecutor, DockerCommand};
7use crate::error::Result;
8use async_trait::async_trait;
9
10/// `docker builder build` command - alternative interface to docker build
11///
12/// This is essentially the same as `docker build` but accessed through
13/// the builder subcommand interface.
14///
15/// # Example
16/// ```no_run
17/// use docker_wrapper::command::builder::BuilderBuildCommand;
18/// use docker_wrapper::DockerCommand;
19///
20/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
21/// let result = BuilderBuildCommand::new(".")
22///     .tag("myapp:latest")
23///     .no_cache()
24///     .execute()
25///     .await?;
26///
27/// if let Some(id) = result.image_id {
28///     println!("Built image: {}", id);
29/// }
30/// # Ok(())
31/// # }
32/// ```
33#[derive(Debug, Clone)]
34pub struct BuilderBuildCommand {
35    /// Underlying build command
36    inner: BuildCommand,
37}
38
39impl BuilderBuildCommand {
40    /// Create a new builder build command
41    ///
42    /// # Arguments
43    /// * `context` - Build context path (e.g., ".", "/path/to/dir")
44    pub fn new(context: impl Into<String>) -> Self {
45        Self {
46            inner: BuildCommand::new(context),
47        }
48    }
49
50    /// Set the Dockerfile to use
51    #[must_use]
52    pub fn dockerfile(mut self, path: impl Into<String>) -> Self {
53        self.inner = self.inner.file(path.into());
54        self
55    }
56
57    /// Tag the image
58    #[must_use]
59    pub fn tag(mut self, tag: impl Into<String>) -> Self {
60        self.inner = self.inner.tag(tag);
61        self
62    }
63
64    /// Do not use cache when building
65    #[must_use]
66    pub fn no_cache(mut self) -> Self {
67        self.inner = self.inner.no_cache();
68        self
69    }
70
71    /// Set build-time variables
72    #[must_use]
73    pub fn build_arg(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
74        self.inner = self.inner.build_arg(key, value);
75        self
76    }
77
78    /// Set target build stage
79    #[must_use]
80    pub fn target(mut self, target: impl Into<String>) -> Self {
81        self.inner = self.inner.target(target);
82        self
83    }
84
85    /// Set platform for multi-platform builds
86    #[must_use]
87    pub fn platform(mut self, platform: impl Into<String>) -> Self {
88        self.inner = self.inner.platform(platform);
89        self
90    }
91
92    /// Enable `BuildKit` backend
93    #[must_use]
94    pub fn buildkit(mut self) -> Self {
95        // This would normally set DOCKER_BUILDKIT=1 environment variable
96        // For now, we'll add it as a raw arg (in practice, this would be an env var)
97        self.inner
98            .executor
99            .raw_args
100            .push("DOCKER_BUILDKIT=1".to_string());
101        self
102    }
103
104    /// Enable quiet mode
105    #[must_use]
106    pub fn quiet(mut self) -> Self {
107        self.inner = self.inner.quiet();
108        self
109    }
110
111    /// Always remove intermediate containers
112    #[must_use]
113    pub fn force_rm(mut self) -> Self {
114        self.inner = self.inner.force_rm();
115        self
116    }
117
118    /// Remove intermediate containers after successful build (default)
119    #[must_use]
120    pub fn rm(self) -> Self {
121        // rm is the default behavior, this is a no-op
122        self
123    }
124
125    /// Do not remove intermediate containers after build
126    #[must_use]
127    pub fn no_rm(mut self) -> Self {
128        self.inner = self.inner.no_rm();
129        self
130    }
131
132    /// Always attempt to pull newer version of base image
133    #[must_use]
134    pub fn pull(mut self) -> Self {
135        self.inner = self.inner.pull();
136        self
137    }
138}
139
140#[async_trait]
141impl DockerCommand for BuilderBuildCommand {
142    type Output = BuildOutput;
143
144    fn get_executor(&self) -> &CommandExecutor {
145        &self.inner.executor
146    }
147
148    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
149        &mut self.inner.executor
150    }
151
152    fn build_command_args(&self) -> Vec<String> {
153        // Get the args from the inner build command
154        let mut inner_args = self.inner.build_command_args();
155
156        // Replace "build" with "builder build"
157        if !inner_args.is_empty() && inner_args[0] == "build" {
158            inner_args[0] = "builder".to_string();
159            inner_args.insert(1, "build".to_string());
160        }
161
162        inner_args
163    }
164
165    async fn execute(&self) -> Result<Self::Output> {
166        // The builder build command has the same output as regular build
167        let args = self.build_command_args();
168        let output = self.inner.executor.execute_command("docker", args).await?;
169
170        // Extract image ID from output
171        let image_id = extract_image_id(&output.stdout);
172
173        Ok(BuildOutput {
174            stdout: output.stdout,
175            stderr: output.stderr,
176            exit_code: output.exit_code,
177            image_id,
178        })
179    }
180}
181
182/// Extract image ID from build output
183fn extract_image_id(stdout: &str) -> Option<String> {
184    // Look for "Successfully built <id>" or "writing image sha256:<id>"
185    for line in stdout.lines().rev() {
186        if line.contains("Successfully built") {
187            return line.split_whitespace().last().map(String::from);
188        }
189        if line.contains("writing image sha256:") {
190            if let Some(id) = line.split("sha256:").nth(1) {
191                return Some(format!(
192                    "sha256:{}",
193                    id.split_whitespace()
194                        .next()?
195                        .trim_end_matches('"')
196                        .trim_end_matches('}')
197                ));
198            }
199        }
200    }
201    None
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn test_builder_build_basic() {
210        let cmd = BuilderBuildCommand::new(".");
211        let args = cmd.build_command_args();
212        assert_eq!(&args[0..2], &["builder", "build"]);
213        assert!(args.contains(&".".to_string()));
214    }
215
216    #[test]
217    fn test_builder_build_with_options() {
218        let cmd = BuilderBuildCommand::new("/app")
219            .tag("myapp:latest")
220            .dockerfile("custom.Dockerfile")
221            .no_cache()
222            .build_arg("VERSION", "1.0");
223
224        let args = cmd.build_command_args();
225        assert_eq!(&args[0..2], &["builder", "build"]);
226        assert!(args.contains(&"--tag".to_string()));
227        assert!(args.contains(&"myapp:latest".to_string()));
228        assert!(args.contains(&"--file".to_string()));
229        assert!(args.contains(&"custom.Dockerfile".to_string()));
230        assert!(args.contains(&"--no-cache".to_string()));
231        assert!(args.contains(&"--build-arg".to_string()));
232        assert!(args.contains(&"VERSION=1.0".to_string()));
233    }
234
235    #[test]
236    fn test_builder_build_buildkit() {
237        let mut cmd = BuilderBuildCommand::new(".");
238        cmd = cmd.buildkit();
239
240        // Check that DOCKER_BUILDKIT was added as a raw arg
241        assert!(cmd
242            .inner
243            .executor
244            .raw_args
245            .contains(&"DOCKER_BUILDKIT=1".to_string()));
246    }
247
248    #[test]
249    fn test_builder_build_platform() {
250        let cmd = BuilderBuildCommand::new(".")
251            .platform("linux/amd64")
252            .target("production");
253
254        let args = cmd.build_command_args();
255        assert!(args.contains(&"--platform".to_string()));
256        assert!(args.contains(&"linux/amd64".to_string()));
257        assert!(args.contains(&"--target".to_string()));
258        assert!(args.contains(&"production".to_string()));
259    }
260
261    #[test]
262    fn test_builder_build_extensibility() {
263        let mut cmd = BuilderBuildCommand::new(".");
264        cmd.inner
265            .executor
266            .raw_args
267            .push("--custom-flag".to_string());
268
269        let args = cmd.build_command_args();
270        assert!(args.contains(&"--custom-flag".to_string()));
271    }
272}