Skip to main content

outrig_cli/cli/
build.rs

1//! `outrig build`: pre-warm one (or every) image-config image.
2//!
3//! [`execute`] follows the order documented in `doc/usage/build.md`:
4//! load + merge + validate the build-relevant config, decide the target list, then for each
5//! target compute the cache tag, probe the image store, and either print
6//! a one-line "cache hit" summary or stream a `buildah build` between the
7//! verbose header and a final `image ready` line. `--all` prints a
8//! per-image summary line instead of the verbose form so the output
9//! stays scannable across many image-configs.
10
11use std::fmt::Write as _;
12use std::path::Path;
13use std::time::Instant;
14
15use clap::{ArgGroup, Parser};
16
17use crate::error::{OutrigError, Result};
18use crate::paths::repo_root_from_config_path;
19use outrig::config::{Config, ImageConfig, ImageSourceRef};
20use outrig::image::{self, ImageTag};
21
22#[derive(Debug, Parser)]
23#[command(group(ArgGroup::new("target").args(["image", "all"])))]
24pub struct BuildArgs {
25    /// Pick a `[images.<name>]` block. Defaults to `default-image` from config.
26    #[arg(long, value_name = "NAME")]
27    pub image: Option<String>,
28
29    /// Build every image-config defined in the config file.
30    #[arg(long)]
31    pub all: bool,
32
33    /// Force rebuild even on cache hit. Passes `--no-cache` to buildah.
34    #[arg(long = "no-cache")]
35    pub no_cache: bool,
36}
37
38/// Run one `outrig build` invocation end-to-end. Returns the process exit code.
39pub async fn execute(
40    repo_cfg_path: &Path,
41    global_cfg_path: &Path,
42    args: &BuildArgs,
43) -> Result<i32> {
44    let repo_root = repo_root_from_config_path(repo_cfg_path);
45    let cfg = Config::load_for_build(&repo_root, Some(global_cfg_path))?;
46
47    let targets: Vec<&str> = if args.all {
48        if cfg.images.is_empty() {
49            return Err(OutrigError::Configuration(
50                "--all requires at least one [images.<name>] block".to_string(),
51            )
52            .into());
53        }
54        cfg.images.keys().map(String::as_str).collect()
55    } else {
56        let name = args
57            .image
58            .as_deref()
59            .or(cfg.default_image.as_deref())
60            .ok_or_else(|| {
61                OutrigError::Configuration(
62                    "no --image, --all, or default-image configured".to_string(),
63                )
64            })?;
65        vec![name]
66    };
67
68    if args.all {
69        build_all(&cfg, &repo_root, &targets, args.no_cache).await
70    } else {
71        let name = targets[0];
72        let cc = cfg.images.get(name).ok_or_else(|| {
73            OutrigError::Configuration(format!(
74                "image-config {name:?} does not match any [images.<name>]"
75            ))
76        })?;
77        build_single(name, cc, &repo_root, args.no_cache).await
78    }
79}
80
81async fn build_single(
82    name: &str,
83    cc: &ImageConfig,
84    repo_root: &Path,
85    no_cache: bool,
86) -> Result<i32> {
87    match cc.source() {
88        ImageSourceRef::Image { image_name } => {
89            let tag = image::ImageTag(image_name.to_string());
90            let already_pulled = !no_cache && image::probe_pulled(&tag).await?;
91            if already_pulled {
92                eprintln!("[outrig] image ready (already pulled: {tag})");
93                return Ok(0);
94            }
95            print_image_header(name, &tag);
96            image::pull_image(&tag).await?;
97            eprintln!("[outrig] image ready: {tag}");
98            Ok(0)
99        }
100        ImageSourceRef::Build { .. } => {
101            let tag = image::compute_tag_for(name, cc, repo_root).await?;
102            let cache_hit = !no_cache && image::probe_cached(&tag).await?;
103            if cache_hit {
104                eprintln!("[outrig] image ready (cache hit: {tag})");
105                return Ok(0);
106            }
107            print_build_header(name, cc, &tag);
108            image::build_image_for(name, cc, repo_root, &tag, no_cache).await?;
109            eprintln!("[outrig] image ready: {tag}");
110            Ok(0)
111        }
112    }
113}
114
115async fn build_all(
116    cfg: &Config,
117    repo_root: &Path,
118    targets: &[&str],
119    no_cache: bool,
120) -> Result<i32> {
121    let pad = targets.iter().map(|n| n.len()).max().unwrap_or(0);
122    for name in targets {
123        let cc = cfg.images.get(*name).ok_or_else(|| {
124            OutrigError::Configuration(format!(
125                "image-config {name:?} does not match any [images.<name>]"
126            ))
127        })?;
128        match cc.source() {
129            ImageSourceRef::Image { image_name } => {
130                let tag = image::ImageTag(image_name.to_string());
131                let already_pulled = !no_cache && image::probe_pulled(&tag).await?;
132                let suffix = if already_pulled {
133                    "(already pulled)".to_string()
134                } else {
135                    let started = Instant::now();
136                    image::pull_image(&tag).await?;
137                    format!("(pulled in {}s)", started.elapsed().as_secs())
138                };
139                eprintln!("[outrig] image-config: {name:<pad$} -> {tag} {suffix}");
140            }
141            ImageSourceRef::Build { .. } => {
142                let tag = image::compute_tag_for(name, cc, repo_root).await?;
143                let cache_hit = !no_cache && image::probe_cached(&tag).await?;
144                let suffix = if cache_hit {
145                    "(cache hit)".to_string()
146                } else {
147                    let started = Instant::now();
148                    image::build_image_for(name, cc, repo_root, &tag, no_cache).await?;
149                    format!("(built in {}s)", started.elapsed().as_secs())
150                };
151                eprintln!("[outrig] image-config: {name:<pad$} -> {tag} {suffix}");
152            }
153        }
154    }
155    eprintln!("[outrig] all images ready");
156    Ok(0)
157}
158
159fn print_build_header(name: &str, cc: &ImageConfig, tag: &ImageTag) {
160    let mut buf = String::new();
161    let _ = writeln!(buf, "[outrig] image-config: {name}");
162    let _ = writeln!(
163        buf,
164        "[outrig] dockerfile:       {}",
165        cc.dockerfile.as_ref().expect("build path").display()
166    );
167    let _ = writeln!(
168        buf,
169        "[outrig] context:          {}",
170        cc.context.as_ref().expect("build path").display()
171    );
172    let _ = writeln!(buf, "[outrig] image tag:        {tag}");
173    eprint!("{buf}");
174}
175
176fn print_image_header(name: &str, tag: &ImageTag) {
177    let mut buf = String::new();
178    let _ = writeln!(buf, "[outrig] image-config: {name}");
179    let _ = writeln!(buf, "[outrig] image:            {tag}");
180    eprint!("{buf}");
181}