@@ -284,6 +284,138 @@ def image(image: str, present: bool = True, force: bool = False):
284284 host .noop ("There is no {0} image!" .format (image ))
285285
286286
287+ @operation ()
288+ def build (
289+ path : str ,
290+ tags : str | list [str ] | None = None ,
291+ dockerfile : str | None = None ,
292+ build_args : dict [str , str ] | None = None ,
293+ labels : dict [str , str ] | None = None ,
294+ target : str | None = None ,
295+ platform : str | None = None ,
296+ network : str | None = None ,
297+ cache_from : str | list [str ] | None = None ,
298+ cache_to : str | list [str ] | None = None ,
299+ secrets : list [str ] | None = None ,
300+ pull : bool = False ,
301+ no_cache : bool = False ,
302+ force : bool = False ,
303+ builder : str | None = None ,
304+ build_cmd : str = "docker build" ,
305+ ):
306+ """
307+ Build a Docker image from a context directory or URL.
308+
309+ + path: build context (local directory or git/http URL)
310+ + tags: tag or list of tags to apply to the built image (maps to ``-t``)
311+ + dockerfile: path to the Dockerfile (maps to ``-f``; relative to the context)
312+ + build_args: ``--build-arg`` values as a mapping
313+ + labels: ``--label`` values as a mapping
314+ + target: ``--target`` stage for multi-stage Dockerfiles
315+ + platform: ``--platform`` (e.g. ``linux/amd64``)
316+ + network: ``--network`` (e.g. ``host``)
317+ + cache_from: ``--cache-from`` value or list of values; accepts image
318+ references and full backend specs (e.g. ``type=registry,ref=...``,
319+ ``type=gha``, ``type=local,src=/tmp/.buildx-cache``)
320+ + cache_to: ``--cache-to`` value or list of values; same backend syntax as
321+ ``cache_from`` (BuildKit / ``docker buildx build`` only)
322+ + secrets: list of raw ``--secret`` specs (e.g. ``id=mysecret,src=/run/secret``)
323+ + pull: pass ``--pull`` to always fetch newer base images
324+ + no_cache: pass ``--no-cache``
325+ + force: rebuild even if all provided tags already exist locally
326+ + builder: optional name passed via ``--builder`` to select a buildx
327+ builder instance (BuildKit only)
328+ + build_cmd: command used to invoke the build, defaults to ``docker build``;
329+ set to ``"docker buildx build"`` to force BuildKit
330+
331+ Idempotency is tag-based: when ``tags`` is provided and ``force`` is false, the
332+ operation no-ops if every tag already exists on the target. Without ``tags``
333+ the build always runs (Docker's own layer cache still applies).
334+
335+ **Examples:**
336+
337+ .. code:: python
338+
339+ from pyinfra.operations import docker
340+
341+ # Build and tag a local context
342+ docker.build(
343+ name="Build app image",
344+ path="/srv/app",
345+ tags=["app:1.0", "app:latest"],
346+ build_args={"VERSION": "1.0"},
347+ pull=True,
348+ )
349+
350+ # BuildKit cross-platform build from a custom Dockerfile,
351+ # using a named buildx builder and a registry cache backend
352+ docker.build(
353+ name="Build multi-arch app",
354+ path="/srv/app",
355+ tags="registry.io/app:arm64",
356+ dockerfile="docker/Dockerfile.prod",
357+ platform="linux/arm64",
358+ build_cmd="docker buildx build",
359+ builder="multiarch",
360+ cache_from="type=registry,ref=registry.io/app:cache",
361+ cache_to="type=registry,ref=registry.io/app:cache,mode=max",
362+ )
363+ """
364+ if not path :
365+ raise OperationError ("docker.build requires a context path" )
366+
367+ tag_list : list [str ] = (
368+ [tags ] if isinstance (tags , str ) else list (tags ) if tags is not None else []
369+ )
370+ cache_from_list : list [str ] = (
371+ [cache_from ]
372+ if isinstance (cache_from , str )
373+ else list (cache_from )
374+ if cache_from is not None
375+ else []
376+ )
377+ cache_to_list : list [str ] = (
378+ [cache_to ] if isinstance (cache_to , str ) else list (cache_to ) if cache_to is not None else []
379+ )
380+
381+ if tag_list and not force :
382+ missing = [tag for tag in tag_list if not host .get_fact (DockerImage , object_id = tag )]
383+ if not missing :
384+ host .noop ("Image(s) {0} already exist!" .format (", " .join (tag_list )))
385+ return
386+
387+ command_bits : list [str | QuoteString ] = [build_cmd ]
388+ if builder :
389+ command_bits .extend (["--builder" , QuoteString (builder )])
390+ for tag in tag_list :
391+ command_bits .extend (["-t" , QuoteString (tag )])
392+ if dockerfile :
393+ command_bits .extend (["-f" , QuoteString (dockerfile )])
394+ for key , value in (build_args or {}).items ():
395+ command_bits .extend (["--build-arg" , QuoteString ("{0}={1}" .format (key , value ))])
396+ for key , value in (labels or {}).items ():
397+ command_bits .extend (["--label" , QuoteString ("{0}={1}" .format (key , value ))])
398+ if target :
399+ command_bits .extend (["--target" , QuoteString (target )])
400+ if platform :
401+ command_bits .extend (["--platform" , QuoteString (platform )])
402+ if network :
403+ command_bits .extend (["--network" , QuoteString (network )])
404+ for cache in cache_from_list :
405+ command_bits .extend (["--cache-from" , QuoteString (cache )])
406+ for cache in cache_to_list :
407+ command_bits .extend (["--cache-to" , QuoteString (cache )])
408+ for secret in secrets or []:
409+ command_bits .extend (["--secret" , QuoteString (secret )])
410+ if pull :
411+ command_bits .append ("--pull" )
412+ if no_cache :
413+ command_bits .append ("--no-cache" )
414+ command_bits .append (QuoteString (path ))
415+
416+ yield StringCommand (* command_bits )
417+
418+
287419@operation ()
288420def volume (volume : str , driver : str = "" , labels : list [str ] | None = None , present : bool = True ):
289421 """
0 commit comments