66//! passed as the `target=` query parameter — the full Dockerfile is always sent.
77
88mod context;
9+ mod pull;
910
1011use bytes:: Bytes ;
1112use futures_util:: StreamExt ;
12- use tracing:: { debug , info, warn} ;
13+ use tracing:: { info, warn} ;
1314
1415use crate :: compose:: types:: { BuildConfig , Service } ;
1516use crate :: error:: { ComposeError , Result } ;
16- use crate :: libpod:: types:: image:: { BuildOutput , ImagePullProgress } ;
17+ use crate :: libpod:: types:: image:: BuildOutput ;
1718use crate :: libpod:: urlencoded;
1819use crate :: libpod:: API_PREFIX ;
1920use crate :: size;
@@ -26,53 +27,22 @@ use super::Engine;
2627/// specs (`id=NAME,src=ENTRY`) for the libpod build endpoint.
2728type ResolvedBuildSecrets = ( Vec < ( String , Vec < u8 > ) > , Vec < String > ) ;
2829
29- impl Engine {
30- pub ( super ) async fn pull_image ( & self , service : & Service ) -> Result < ( ) > {
31- let image = match & service. image {
32- Some ( img) => img. clone ( ) ,
33- None => return Ok ( ( ) ) ,
34- } ;
35-
36- info ! ( "pulling {image}" ) ;
37-
38- let requested = service. pull_policy . as_deref ( ) ;
39- let pull_policy = libpod_pull_policy ( requested) . unwrap_or_else ( || {
40- warn ! (
41- "unknown pull_policy '{}', defaulting to 'missing'" ,
42- requested. unwrap_or_default( )
43- ) ;
44- "missing"
45- } ) ;
46- let mut query = format ! ( "reference={}&policy={}" , urlencoded( & image) , pull_policy) ;
47- if let Some ( platform) = & service. platform {
48- query. push_str ( & format ! ( "&platform={}" , urlencoded( platform) ) ) ;
49- }
50-
51- let path = format ! ( "{API_PREFIX}/images/pull?{query}" ) ;
52- let resp = self
53- . client
54- . post_empty_stream ( & path)
55- . await
56- . map_err ( ComposeError :: Podman ) ?;
57- let mut stream = crate :: libpod:: parse_json_lines :: < ImagePullProgress > ( resp. into_body ( ) ) ;
58-
59- while let Some ( result) = stream. next ( ) . await {
60- match result {
61- Ok ( progress) => {
62- if !progress. stream . is_empty ( ) {
63- debug ! ( "{}" , progress. stream. trim_end( ) ) ;
64- }
65- if !progress. error . is_empty ( ) {
66- warn ! ( "pull error: {}" , progress. error) ;
67- }
68- }
69- Err ( e) => warn ! ( "pull warning: {e}" ) ,
70- }
71- }
72-
73- Ok ( ( ) )
74- }
30+ /// `docker compose build`-style CLI overrides. Each augments (never weakens)
31+ /// the per-service `build:` config: a flag forces the behaviour on even when
32+ /// the compose file leaves it off.
33+ #[ derive( Default , Clone ) ]
34+ pub struct BuildOptions {
35+ /// Force a cache-less build (`--no-cache`).
36+ pub no_cache : bool ,
37+ /// Always attempt to pull a newer base image (`--pull`).
38+ pub pull : bool ,
39+ /// Extra build args (`KEY=VAL`); override the compose `build.args` on conflict.
40+ pub build_args : Vec < String > ,
41+ /// Suppress build output (`-q/--quiet`).
42+ pub quiet : bool ,
43+ }
7544
45+ impl Engine {
7646 /// Build (or rebuild) images for services that have a `build:` block.
7747 ///
7848 /// If `target_services` is empty, every service with a build config is built.
@@ -81,6 +51,18 @@ impl Engine {
8151 & self ,
8252 file : & crate :: compose:: types:: ComposeFile ,
8353 target_services : & [ String ] ,
54+ ) -> Result < ( ) > {
55+ self . build_all_with_options ( file, target_services, & BuildOptions :: default ( ) )
56+ . await
57+ }
58+
59+ /// Build service images with `docker compose build`-style overrides
60+ /// (`--no-cache`, `--pull`, `--build-arg`, `--quiet`).
61+ pub async fn build_all_with_options (
62+ & self ,
63+ file : & crate :: compose:: types:: ComposeFile ,
64+ target_services : & [ String ] ,
65+ opts : & BuildOptions ,
8466 ) -> Result < ( ) > {
8567 let names: Vec < String > = if target_services. is_empty ( ) {
8668 file. services . keys ( ) . cloned ( ) . collect ( )
@@ -96,7 +78,7 @@ impl Engine {
9678 for name in & names {
9779 let service = & file. services [ name] ;
9880 if service. build . is_some ( ) {
99- self . build_service ( name, service, file) . await ?;
81+ self . build_service ( name, service, file, opts ) . await ?;
10082 }
10183 }
10284 Ok ( ( ) )
@@ -107,6 +89,7 @@ impl Engine {
10789 service_name : & str ,
10890 service : & Service ,
10991 file : & crate :: compose:: types:: ComposeFile ,
92+ opts : & BuildOptions ,
11093 ) -> Result < ( ) > {
11194 let build = match & service. build {
11295 Some ( b) => b,
@@ -178,6 +161,16 @@ impl Engine {
178161 let value = v. unwrap_or_else ( || std:: env:: var ( & k) . unwrap_or_default ( ) ) ;
179162 build_args. insert ( k, value) ;
180163 }
164+ // CLI `--build-arg KEY=VAL` overrides the compose `build.args`. A bare
165+ // `KEY` (no `=`) takes its value from the process environment, matching
166+ // docker compose.
167+ for entry in & opts. build_args {
168+ let ( k, v) = match entry. split_once ( '=' ) {
169+ Some ( ( k, v) ) => ( k. to_string ( ) , v. to_string ( ) ) ,
170+ None => ( entry. clone ( ) , std:: env:: var ( entry) . unwrap_or_default ( ) ) ,
171+ } ;
172+ build_args. insert ( k, v) ;
173+ }
181174
182175 let mut labels: std:: collections:: HashMap < String , String > =
183176 std:: collections:: HashMap :: new ( ) ;
@@ -227,10 +220,10 @@ impl Engine {
227220 let mut qs = format ! (
228221 "t={}&rm=true&nocache={}" ,
229222 urlencoded( & tag) ,
230- build. no_cache( )
223+ build. no_cache( ) || opts . no_cache
231224 ) ;
232225 qs. push_str ( & format ! ( "&dockerfile={}" , urlencoded( & dockerfile_name) ) ) ;
233- if build. pull ( ) {
226+ if build. pull ( ) || opts . pull {
234227 qs. push_str ( "&pull=true" ) ;
235228 }
236229 if let Some ( p) = & platform {
@@ -295,7 +288,7 @@ impl Engine {
295288 while let Some ( result) = stream. next ( ) . await {
296289 match result {
297290 Ok ( output) => {
298- if !output. stream . is_empty ( ) {
291+ if !opts . quiet && ! output. stream . is_empty ( ) {
299292 print ! ( "{}" , output. stream) ;
300293 }
301294 if let Some ( err) = output. error_detail . and_then ( |e| e. message ) {
@@ -385,39 +378,11 @@ fn is_remote_context(context: &str) -> bool {
385378 context. contains ( "://" ) || context. starts_with ( "git@" )
386379}
387380
388- /// Map a compose `pull_policy:` value to the libpod images/pull `policy`
389- /// parameter. `if_not_present` is the spec alias for `missing`; `build` falls
390- /// back to `missing` here (its build behavior is handled by the caller). Returns
391- /// `None` for an unrecognized value so the caller can warn and default.
392- pub ( super ) fn libpod_pull_policy ( policy : Option < & str > ) -> Option < & ' static str > {
393- match policy {
394- Some ( "always" ) => Some ( "always" ) ,
395- Some ( "newer" ) => Some ( "newer" ) ,
396- Some ( "never" ) => Some ( "never" ) ,
397- None | Some ( "missing" ) | Some ( "if_not_present" ) | Some ( "build" ) => Some ( "missing" ) ,
398- Some ( _) => None ,
399- }
400- }
401-
402381#[ cfg( test) ]
403382mod tests {
404- use super :: { is_remote_context, libpod_pull_policy , Engine } ;
383+ use super :: { is_remote_context, Engine } ;
405384 use crate :: libpod:: Client ;
406385
407- #[ test]
408- fn pull_policy_maps_every_spec_value ( ) {
409- assert_eq ! ( libpod_pull_policy( Some ( "always" ) ) , Some ( "always" ) ) ;
410- assert_eq ! ( libpod_pull_policy( Some ( "newer" ) ) , Some ( "newer" ) ) ;
411- assert_eq ! ( libpod_pull_policy( Some ( "never" ) ) , Some ( "never" ) ) ;
412- assert_eq ! ( libpod_pull_policy( Some ( "missing" ) ) , Some ( "missing" ) ) ;
413- // `if_not_present` is the spec alias for `missing`.
414- assert_eq ! ( libpod_pull_policy( Some ( "if_not_present" ) ) , Some ( "missing" ) ) ;
415- assert_eq ! ( libpod_pull_policy( Some ( "build" ) ) , Some ( "missing" ) ) ;
416- assert_eq ! ( libpod_pull_policy( None ) , Some ( "missing" ) ) ;
417- // Unknown values are reported (None) so the caller warns.
418- assert_eq ! ( libpod_pull_policy( Some ( "bogus" ) ) , None ) ;
419- }
420-
421386 fn engine ( base : std:: path:: PathBuf ) -> Engine {
422387 Engine :: with_base_dir ( Client :: new ( "/nonexistent.sock" ) , "p" . into ( ) , base)
423388 }
0 commit comments