Taskfiles#
Follow the guidelines below when writing and updating Taskfiles. Note that neither the guidelines nor examples are written as a rigorous specification, but they should be sufficient to understand the spirit of each guideline.
Variable naming#
Variables should be named using
SCREAMING_SNAKE_CASE
.Variables in the global scope (as opposed to a task’s scope) should be prefixed with
G_
to avoid conflicts with local variables.
Example
vars:
G_BUILD_DIR: "build"
binaries:
vars:
BUILD_DIR: "{{.G_BUILD_DIR}}/bin"
Paths in commands#
All paths used in commands should be quoted.
Example
my-task:
cmds:
- "touch '{{.FILE_PATH}}'"
- |-
echo "Hello, world" > "{{.FILE_PATH}}"
Built-in variables#
Don’t use the variable
{{.ROOT_TASKFILE}}
v3.35.1 has a bug that makes it equivalent to
ROOT_DIR
(i.e., the parent directory of the root Taskfile, rather than the path to the root Taskfile).
When using the variable
{{.TASK}}
in a task within a namespace (e.g., it’s in a nested Taskfile), replace the:
in the name with#
as follows:{{.TASK | replace \":\" \"#\"}}
.This ensures the task name can be used as a filename.
sources
and generates
#
The task attributes sources
and generates
are supposed to allow us to control whether a task is
run based on whether its source files or generated files have changed, but task
’s behaviour may
seem intuitive. So to understand the guidelines, you’ll first need to understand task
’s behaviour.
task
has two methods to track changes to source files, specified using the method
attribute:
checksum
which tracks changes to the source files checksums and timestamp
which tracks changes
to the source files’ modification timestamps. Note that checksums and modification timestamps are
only tracked for source files, not generated files.
If a task has a sources
attribute, then the task will run if:
it has never been run (the checksums/modification timestamps of the source files have not been cached), or
the content of any listed source file has changed.
This is true even if a sources
entry is a glob that matches one or more file paths.
If the task has a sources
and a generates
attribute, the task will run if any source file has
changed or if any generates
entry doesn’t exist. Note however, that:
task
doesn’t checksum the generated files nor check if they are older than the source files; it only checks for existence.When using globs,
task
only checks whether the glob entry is satisfied rather than whether all previously generated files exist.E.g., if the
generates
entry isbuild/nodejs/node/**/*
,task
will re-run the task if thenode
ornodejs
directory don’t exist, or if they’re empty; buttask
won’t re-run the task if all but one file insidenode
exists.
Overall, this means that task
will not detect all changes to the generated files.
Depending on generated files#
Every task with a sources
field should depend on the generated files of its dependencies.
Example
parent:
sources:
- child1-output.txt
- child2-output.txt
deps:
- child1
- child2
child1:
generates: ["child1-output.txt"]
child2:
generates: ["child2-output.txt"]
generates
and glob patterns#
Don’t use generates
entries with glob patterns unless you’ve accounted for the limitations above.
Instead, you can manually checksum the generated files using the utility tasks below:
vars:
CHECKSUM_TAR_BASE_ARGS: >-
--group=0
--mtime='UTC 1970-01-01'
--numeric-owner
--owner=0
--sort=name
compute-checksum:
desc: "Tries to compute a checksum for the given directory and output it to a file."
internal: true
silent: true
requires:
vars: ["DATA_DIR", "OUTPUT_FILE"]
cmds:
- >-
tar cf -
--directory "{{.DATA_DIR}}"
--group=0
--mtime='UTC 1970-01-01'
--numeric-owner
--owner=0
--sort=name
{{.CHECKSUM_TAR_BASE_ARGS}} . 2> /dev/null
| md5sum > {{.OUTPUT_FILE}}
# Ignore errors so that dependent tasks don't fail
ignore_error: true
validate-checksum:
desc: "Validates the checksum of the given directory matches the checksum in the given file, or
deletes the checksum file otherwise."
internal: true
silent: true
requires:
vars: ["CHECKSUM_FILE", "DATA_DIR"]
vars:
TMP_CHECKSUM_FILE: "{{.CHECKSUM_FILE}}.tmp"
cmds:
- task: "compute-checksum"
vars:
DATA_DIR: "{{.DATA_DIR}}"
OUTPUT_FILE: "{{.TMP_CHECKSUM_FILE}}"
- defer: "rm -f '{{.TMP_CHECKSUM_FILE}}'"
# Check that the directory exists and the checksum matches; otherwise delete the checksum file
- >-
(
test -d "{{.DATA_DIR}}"
&& diff -q '{{.TMP_CHECKSUM_FILE}}' '{{.CHECKSUM_FILE}}' 2> /dev/null
) || rm -f '{{.CHECKSUM_FILE}}'
You can use the utility tasks as follows:
my-task:
vars:
CHECKSUM_FILE: "checksum.txt"
OUTPUT_DIR: "build/my-task"
sources: ["source.txt"]
generates: ["{{.CHECKSUM_FILE}}"]
deps:
- task: "validate-checksum"
vars:
CHECKSUM_FILE: "{{.CHECKSUM_FILE}}"
DATA_DIR: "{{.OUTPUT_DIR}}"
cmds:
- "mkdir -p '{{.OUTPUT_DIR}}'"
- "touch '{{.OUTPUT_DIR}}/output.txt'"
# This command must be last
- task: "compute-checksum"
vars:
DATA_DIR: "{{.OUTPUT_DIR}}"
OUTPUT_FILE: "{{.CHECKSUM_FILE}}"
Thus, the task will re-run if either:
the source files change;
the checksum file doesn’t exist; or
validate-checksum
fails because a generated file was changed.
Task ordering#
All non-internal tasks should appear before internal tasks.
Example
my-task-1: cmds: - "echo {{.TASK}}" my-task-2: cmds: - "echo {{.TASK}}" my-internal-task: internal: true cmds: - "echo {{.TASK}}"
Within the internal or non-internal group of tasks, tasks should be organized either alphabetically or following some logical ordering.
Ordering of task attributes#
Task attributes should be ordered first by the categories we define below, and then alphabetically within each category. The one exception to the alphabetical ordering are the execution control attributes.
Descriptors
aliases
desc
internal
summary
Logging
label
prefix
silent
Variables
dotenv
env
vars
Inputs
requires
sources
Environment control
dir
platforms
set
shopt
Outputs
generates
method
Execution control (ordered by execution order)
prompt
run
deps
preconditions
status
cmds
ignore_error
interactive
Ordering of list/map entries#
List/map entries should be ordered alphabetically (except for vars
), case-insensitively. Where
there are several attributes, they may be broken down into categories with a comment above each
category.
Entries in the vars
attribute may violate this ordering so that variables defined later can depend
on the values of variables defined earlier.
Example
my-task:
vars:
PARENT_VAR: "parent"
CHILD_VAR: "{{.PARENT_VAR}}-child"
sources:
- "source1.txt"
- "source2.txt"
generates:
# Binaries
- "binary1"
- "binary2"
# Libraries
- "lib1"
- "lib2"