Taskfile vs Makefile: Which Build Tool Reigns Supreme?

Taskfile vs Makefile: Which Build Tool Reigns Supreme?

1. What is Taskfile

Taskfile describes various execution tasks using YAML and is primarily written in Go. Compared to Makefile which uses tab-separated and bash syntax, Taskfile appears more modern and user-friendly (although it may turn you into a YAML engineer). Taskfile has built-in advanced features such as dynamic variables and recognition of operating system environment variables, making it more aligned with modern coding practices.

Overall, if you are not very familiar with Makefile and wish to accomplish batch tasks using a similar tool, Taskfile is more beginner-friendly with a lower learning curve and sufficient speed.

2. Installation and Usage

Installing go-task

For Mac users, the official installation method is provided via brew:

$ brew install go-task/tap/go-task

For Linux users, the official site provides installation packages for some Linux distributions. However, since it consists of only one binary file, a quick installation script is also provided:

# For Default Installation to ./bin with debug logging
$ sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d

# For Installation To /usr/local/bin for userwide access with debug logging
# May require sudo sh
$ sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin

If you already have a Go development environment set up locally, you can also install it directly using the go command:

$ go install github.com/go-task/task/v3/cmd/task@latest

Quick Start

After installation, you only need to create a Taskfile.yml YAML file and you can run the corresponding tasks using the task command:

version: '3'

tasks:
  build:
    cmds:
      - echo "Executing build task"
      
  docker:
    cmds:
      - echo "Packaging docker image"
Taskfile vs Makefile: Which Build Tool Reigns Supreme?

If you need to set a default task, simply create a task named default:

version: '3'

tasks:
  default:
    cmds:
      - echo "This is the default task"

  build:
    cmds:
      - echo "Executing build task"

  docker:
    cmds:
      - echo "Packaging docker image"
Taskfile vs Makefile: Which Build Tool Reigns Supreme?

3. Advanced Usage

Environment Variables

Taskfile supports referencing three types of environment variables:

  • Shell environment variables
  • Environment variables defined within Taskfile
  • Environment variables defined in variable files

To reference a shell environment variable, simply use the $ variable_name syntax:

version: '3'

tasks:
  default:
    cmds:
      - echo "$ABCD"
Taskfile vs Makefile: Which Build Tool Reigns Supreme?

You can also define environment variables within Taskfile:

version: '3'

env:
  TENV2: "t2" # Global environment variable

tasks:
  default:
    cmds:
      - echo "$TENV1"
      - echo "$TENV2"
    env:
      TENV1: "t1" # Single task environment variable
Taskfile vs Makefile: Which Build Tool Reigns Supreme?

In addition to directly referencing variables, Taskfile also supports loading environment variables from an env file, similar to docker-compose; Taskfile will automatically load the .env file in the same directory, and you can configure specific files using the dotenv command within Taskfile:

version: '3'

dotenv: [".env", ".testenv"]

tasks:
  default:
    cmds:
      - echo "$ABCD"
      - echo "$TESTENV"
Taskfile vs Makefile: Which Build Tool Reigns Supreme?

Enhanced Variables

In addition to standard environment variables, Taskfile also includes a widely used enhanced variable called vars; this variable pattern can be read (interpolated) using Go’s template engine and has special features not available in environment variables. Below is an example of vars variable:

version: '3'

# Global var variable
vars:
  GLOBAL_VAR: "global var"

tasks:
  testvar:
    # task var variable
    vars:
      TASK_VAR: "task var"
    cmds:
      - "echo {{.GLOBAL_VAR}}"
      - "echo {{.TASK_VAR}}"

In addition to the usage similar to environment variables, vars enhanced variables also support dynamic definitions; a common scenario is to obtain the current git commit id every time a task is executed, which can be achieved using the dynamic definition feature of vars:

version: '3'

tasks:
  build:
    cmds:
      - go build -ldflags="-X main.Version={{.GIT_COMMIT}}" main.go
    vars:
      # Every time the task is executed, GIT_COMMIT will call a shell command to generate this variable
      GIT_COMMIT:
        sh: git log -n 1 --format=%h

The vars variable also has some predefined special variables, such as {{.TASK}} which always represents the current task name, and {{.CLI_ARGS}} which can reference command line inputs.

version: '3'

tasks:
  yarn:
    cmds:
      - yarn {{.CLI_ARGS}}

In this case, if you run task yarn -- install, the value of {{.CLI_ARGS}} will become install, thereby executing the yarn install command.

Additionally, the vars variable has other features, such as allowing for cross-task references and overrides, which will be introduced later.

Execution Directory

By default, tasks defined in Taskfile are executed in the current directory. If you wish to execute in another directory, you can set the execution directory directly using the dir parameter without manually writing cd commands:

version: '3'

tasks:
  test1:
    dir: /tmp # Execute in specified directory
    cmds:
      - "ls"

Task Dependencies

In CI environments, we often need to define the execution order and dependencies of tasks; Taskfile provides support for task dependencies through the deps configuration:

version: '3'

tasks:
  build-jar:
    cmds:
      - echo "Compiling jar package..."
  build-static:
    cmds:
      - echo "Compiling frontend UI..."
  build-docker:
    deps: [build-jar, build-static]
    cmds:
      - echo "Packaging docker image..."

Task Invocation

When we define multiple tasks in Taskfile, some tasks may have certain similarities. In this case, we can define template tasks by allowing tasks to call each other and dynamically override vars variables:

version: '3'

tasks:
  docker:
    cmds:
      #- docker build -t {{.IMAGE_NAME}} {{.BUILD_CONTEXT}}
      - echo {{.IMAGE_NAME}} {{.BUILD_CONTEXT}}

  build-backend:
    cmds:
      - task: docker # Reference another task
        vars: { # Dynamically pass variables
          IMAGE_NAME: "backend",
          BUILD_CONTEXT: "maven/target"
        }

  build-frontend:
    cmds:
      - task: docker
        vars: {
          IMAGE_NAME: "frontend",
          BUILD_CONTEXT: "public"
        }
  default: # Default task to call when no task name is provided
    cmds:
      - task: build-backend
      - task: build-frontend
Taskfile vs Makefile: Which Build Tool Reigns Supreme?

Including Other Files

Taskfile supports including other Taskfiles via the includes keyword, facilitating structured handling of Taskfiles.

Note that since the included files may contain multiple tasks, you need to name the included files and reference the target tasks by name:

version: '3'

includes:
  file1: ./file1.yaml # Directly reference yaml file
  dir2: ./dir2 # When referencing a directory, the Taskfile.yaml in that directory is referenced by default
Taskfile vs Makefile: Which Build Tool Reigns Supreme?

When including other Taskfiles, by default, commands will be executed in the directory of the main Taskfile. You can also control the execution directory of tasks within the included Taskfile using the dir parameter:

version: '3'

includes:
  dir1: ./dirtest.yaml # Directly execute in current directory
  dir2:
    taskfile: ./dirtest.yaml
    dir: /tmp # Execute in specified directory
Taskfile vs Makefile: Which Build Tool Reigns Supreme?

Defer Handling

Familiar with Go language should know that there is a very convenient keyword defer; this directive is used to define actions to be executed at the end of the code, commonly for resource cleanup. Taskfile also supports this directive, allowing us to perform cleanup operations during task execution:

version: '3'

tasks:
  default: # Default task to call when no task name is provided
    cmds:
      - wget -q https://github.com/containerd/nerdctl/releases/download/v0.19.0/nerdctl-full-0.19.0-linux-amd64.tar.gz
      # Define cleanup action
      - defer: rm -f nerdctl-full-0.19.0-linux-amd64.tar.gz
      - tar -zxf nerdctl-full-0.19.0-linux-amd64.tar.gz
Taskfile vs Makefile: Which Build Tool Reigns Supreme?

Of course, the defer directive can also reference other tasks for cleanup:

version: '3'

tasks:
  cleanup:
    cmds:
      - rm -f {{.FILE}}
  default: # Default task to call when no task name is provided
    cmds:
      - wget -q https://github.com/containerd/nerdctl/releases/download/v0.19.0/nerdctl-full-0.19.0-linux-amd64.tar.gz
      # Reference other task for cleanup, while also passing dynamic variables
      - defer: {task: cleanup, vars: {FILE: nerdctl-full-0.19.0-linux-amd64.tar.gz}}
      - tar -zxf nerdctl-full-0.19.0-linux-amd64.tar.gz

4. Advanced Applications

Dynamic Detection

Output Detection

Sometimes, we may want to handle caching for certain tasks, such as not downloading a file again if it has already been downloaded. For this requirement, Taskfile allows us to define source files and generated files, using the hash values of these files to determine whether to execute the task:

version: '3'

tasks:
  default:
    cmds:
      - wget -q https://github.com/containerd/nerdctl/releases/download/v0.19.0/nerdctl-full-0.19.0-linux-amd64.tar.gz
    sources:
      - testfile
    generates:
      - nerdctl-full-0.19.0-linux-amd64.tar.gz
Taskfile vs Makefile: Which Build Tool Reigns Supreme?

As seen in the above image, when the task is executed for the first time, a .task directory will be generated, containing the hash values of the files; when the task is executed again, if the hash values do not change, the actual task will not be executed. Taskfile has two default file detection methods: checksum and timestamp. The checksum method performs hash detection on the files (default), which only requires defining the sources configuration; the timestamp method performs timestamp detection on the files, which requires defining both sources and generates configurations.

version: '3'

tasks:
  build:
    cmds:
      - go build .
    sources:
      - ./*.go
    generates:
      - app{{exeExt}}
    method: checksum # Specify detection method

In addition to the two built-in detection modes, we can also define our own detection commands using the status configuration. If the command execution result is 0, the file is considered up-to-date, and the task does not need to be executed:

version: '3'

tasks:
  generate-files:
    cmds:
      - mkdir directory
      - touch directory/file1.txt
      - touch directory/file2.txt
    # test existence of files
    status:
      - test -d directory
      - test -f directory/file1.txt
      - test -f directory/file2.txt

Input Detection

The above output detection checks the results of files generated by tasks. In some cases, we may want to determine a condition before running a task, without executing anything. For this, we can use the preconditions configuration directive:

version: '3'

tasks:
  generate-files:
    cmds:
      - mkdir directory
      - touch directory/file1.txt
      - touch directory/file2.txt
    # test existence of files
    preconditions:
      - test -f .env
      - sh: "[ 1 = 0 ]"
        msg: "One doesn't equal Zero, Halting"

Go Template Engine

In the variable section above, a portion of the template engine usage has been demonstrated. In fact, Taskfile integrates the slim-sprig[1] library, which provides some convenient methods that can be used within the template engine:

version: '3'

tasks:
  print-date:
    cmds:
      - echo {{now | date "2006-01-02"}}

For more information on these methods and the usage of the template engine, please refer to the Go Template documentation and the slim-sprig[2] documentation.

Interactive Terminal

Some task commands may require an interactive terminal to execute. In this case, you can set the interactive option for the task; when interactive is set to true, the task can open an interactive terminal during execution:

version: '3'

tasks:
  cmds:
    - vim my-file.txt
  interactive: true

For more details on using Taskfile, please refer to its official documentation[3]. This article does not cover everything due to space constraints.

References

[1]

slim-sprig: https://go-task.github.io/slim-sprig/

[2]

slim-sprig: https://go-task.github.io/slim-sprig/

[3]

Official documentation: https://taskfile.dev/

Link: https://mritd.com/2022/04/25/taskfile-a-better-build-tool-than-makefile/

(All rights reserved by the original author, please delete if infringed)

Taskfile vs Makefile: Which Build Tool Reigns Supreme?

Taskfile vs Makefile: Which Build Tool Reigns Supreme?

Leave a Comment