User-defined commands (UDCs)
User-defined commands (UDCs) are templates (much like functions in regular programming languages), which can be used to define a series of steps to be executed in sequence. In other words, it is a way to import common build steps which can be reused in multiple contexts.
Unlike targets, UDCs inherit the (1) build context and (2) the build environment from the caller. Meaning that
  1. 1.
    Any local COPY operation will use the directory where the calling Earthfile exists, as the source.
  2. 2.
    Any files, directories and dependencies created by a previous step of the caller are available to the UDC to operate on; and any file changes resulting from executing the UDC commands are passed back to the caller as part of the build environment.
Thus, when importing and reusing UDCs across a complex build, it is very much like reusing libraries in a regular programming language.

Usage

UDCs are defined similarly to regular targets, with a couple of exceptions: the name is in all-uppercase, snake-case and the recipe must start with COMMAND. For example:
1
MY_COPY:
2
COMMAND
3
ARG src
4
ARG dest=./
5
ARG recursive=false
6
RUN cp $(if $recursive = "true"; then printf -- -r; fi) "$src" "$dest"
Copied!
This UDC can be invoked from a target via DO
1
build:
2
FROM alpine:3.15
3
WORKDIR /udc-example
4
RUN echo "hello" >./foo
5
DO +MY_COPY --src=./foo --dest=./bar
6
RUN cat ./bar # prints "hello"
Copied!
A few things to note about this example:
  • The definition of MY_COPY does not contain a FROM so the build environment it operates in is the build environment of the caller.
  • This means that +MY_COPY has access to the file ./foo.
  • Although the copy file operation is performed within +MY_COPY, its effects are seen in the environment of the caller - so the resulting ./bar is available to the caller.

Scope

UDCs create their own ARG scope, which is distinct from the caller. Any ARG that needs to be passed from the caller needs to be passed explicitly via DO +COMMAND --<build-arg-key>=<build-arg-value>, as in the following example.
1
build:
2
ARG var=value-in-build
3
# prints "something-else"
4
DO +PRINT_VAR
5
# prints "value-in-build"
6
DO +PRINT_VAR --var=$var
7
​
8
PRINT_VAR:
9
COMMAND
10
ARG var=something-else
11
RUN echo "$var"
Copied!
Global imports and global args are inherited from the base target of the same Earthfile where the command is defined in (this may be distinct from the base target of the caller).
1
VERSION 0.6
2
​
3
ARG a_global_var=value-in-global
4
​
5
build:
6
# prints "value-in-global"
7
DO +PRINT_VAR
8
​
9
PRINT_VAR:
10
COMMAND
11
RUN echo "$a_global_var"
Copied!

Targets vs UDCs

Earthly targets and UDCs are Earthly's core primitives for organizing build recipes. They encapsulate build logic, and from afar they look pretty similar. However, the use-cases for each are vastly different.
In general, targets are used to produce specific build results, while UDCs are used as a way to reuse build logic, when certain commands are repeated in multiple places. UDCs work like functions or methods in an imperative programming language. Much like function calls it's helpful to imagine UDCs being executed by being inlined into the call site but in a separate variable scope.
As a real-world analogy, targets are more like factories, while UDCs are more like components that are used to put together factories.
Here is a comparison of the two primitives:
Text
Targets
UDCs
Represents a collection of Earthly commands
βœ…
βœ…
Can reference other targets in its body
βœ…
βœ…
Can reference other UDCs in its body
βœ…
βœ…
Build context
The directory where the Earthfile resides
Inherited from the caller
Build environment, when no FROM is specified
Inherited from the base of its own Earthfile
Inherited from the caller
IMPORT statements
Inherited from the base of its own Earthfile
Inherited from the base of its own Earthfile
ARG context
Creates its own scope
Creates its own scope
Requires that ARGs be passed in explicitly
βœ…
βœ…
Global ARG context
Inherited from the base of its own Earthfile
Inherited from the base of its own Earthfile
Can output artifacts
βœ…
❌ - can issue SAVE ARTIFACT, but it's the caller that emits the artifacts
Can output images
βœ…
❌ - can issue SAVE IMAGE, but it's the caller that emits the images
Can be called via earthly CLI
βœ…
❌
Can be used via in conjunction with an IMPORT (IMPORT github.com/my-co/my-proj/some-import)
βœ… - FROM some-import+my-target
βœ… - DO some-import+MY_UDC
Commands that can reference it
FROM, BUILD, COPY, WITH DOCKER --load, FROM DOCKERFILE
DO
Copy link