1use 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 #[arg(long, value_name = "NAME")]
27 pub image: Option<String>,
28
29 #[arg(long)]
31 pub all: bool,
32
33 #[arg(long = "no-cache")]
35 pub no_cache: bool,
36}
37
38pub 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}