Introduction
GNU Make originated as a dependency-tracking build tool that automates the build process of source files and libraries. Although many modern languages provide built-in dependency management and build tools, a Makefile still finds its place in these projects. In these cases, Makefiles are used as a collection of aliases for actions like test, build, and lint. In this article, I will show you that there is a better option for these use-cases: task
.
But Make is standard practice!
Yes, Make has been continuously developed over nearly 50 years, leading to a robust set of features and a rich history in software engineering. It will likely be around forever, but these deep roots are the same thing preventing it from being user-friendly and intuitive.
- Makefiles often become a mess with lots of environment variables, macros, and special symbols
- You MUST use tabs (ugh)
- You can't pass arguments without using environment variables
- An annoying convention in Makefiles is to string together a lot of environment variables to construct commands
-
.PHONY
is required when using non-file targets as command aliases - Many features are designed around building files, which often isn't relevant in these modern scenarios
When we aren't using it for the core features Make is built and designed around, we have to ask ourselves if there is an alternative.
Introduction to Task
This is where the Taskfile comes in. The YAML format creates a self-documenting file that states how each "task" will behave.
The verbose format is a welcome feature in comparison to the Makefile and will result in a more approachable file. When integrated in an open-source project, it can make it easier for new contributors to get started.
After following the simple install process, you just need to run task --init
to create a simple example file. This example shows how to use environment variables and execute commands.
version: '3'
vars:
GREETING: Hello, World!
tasks:
default:
cmds:
- echo "{{.GREETING}}"
silent: true
At this point, you have all the information needed to cover the basic use-cases. Rather than providing a tutorial here, I encourage you to check out the official documentation. The well-written guide starts by showing the basics with concrete examples and graduates into more complex functionality as you scroll. This is a welcome sight in comparison to the large plain HTML page provided by Make.
Rewriting a Makefile to Taskfile
I recently read the Guide to using Makefile with Go by Ramseyjiang. This is an interesting read and makes good points about providing a consistent interface for common tasks and builds. It left me thinking about how the developer experience could be further improved by using a Taskfile instead.
This is the example Makefile created by the article's author:
APP_NAME = myapp
GO_FILES = $(wildcard *.go)
GO_CMD = go
GO_BUILD = $(GO_CMD) build
GO_TEST = $(GO_CMD) test
all: build
executable
build: $(APP_NAME)
$(APP_NAME): $(GO_FILES)
$(GO_BUILD) -o $(APP_NAME) $(GO_FILES)
test:
$(GO_TEST) -v ./... -cover
.PHONY: all build test
Here is my translation to Taskfile:
version: "3"
vars:
APP_NAME: myapp
tasks:
default:
cmds:
- task: build
build:
cmds:
- go build -o {{.APP_NAME}} *.go
sources:
- "*.go"
generates:
- "{{.APP_NAME}}"
test:
cmds:
- go test -v ./... -cover
If you're counting lines, you might notice the Taskfile has a few more. This could be equalized by using cmd
with a string instead of cmds
and the array, but my priority here is to create something easy to read and build upon.
Task Features
Task covers all of the main features of Make:
- Intelligent tracking of input/output files to skip unnecessary builds
- Dependencies between tasks or targets
- Include other files
- Access environment variables
In addition, here are a few of my favorite Task features:
- Automatic CLI usage output and autocompletion
- Run multiple tasks in parallel. I use this one to start a Go backend service and
npm run dev
for the frontend with a single command - Control of output syntax which is useful for grouping output in CI environments
- Forward CLI arguments to task commands. I use this to run
task up -d
which will start Docker containers in detached mode - Global Taskfile:
task
will walk up the filesystem tree until it finds a Taskfile, or use-g
/--global
to search your home directory
I like to add a docs
task to my global Taskfile that will open the usage guide in my default browser:
version: '3'
tasks:
docs:
cmd: open https://taskfile.dev/usage/
Conclusion
Although I am arguing here that a Taskfile is better than a Makefile for many modern projects, I am not saying that Taskfile is a replacement for Makefile in all cases. The two tools have a lot of overlap, but are ultimately designed for different use-cases. There will always be a place for Makefiles, but your modern project is probably better off with a Taskfile. I hope your next step from here is the installation docs for Task so you can give it a try!
Top comments (5)
I mostly make use of quirks in GNU make. In particular, I use sinclude with a wildcard, which, in gmake, lets me treat a .make/ directory as a plugin directory with optional make components, and do common build patterns in those plugins, restricting the top Makefile to those targets actually unique for the application. I also make extensive use of path vars that are set from the environment, such as in debian packaging, like DESTDIR, PREFIX, DATADIR, and default to prefix calculated paths. Can I do similar things with a taskfile?
Hi David, Task is able to do similar things to what you are looking for, but it is less flexible in the case of including directories or wildcard patterns.
Using variables is a core feature of a Taskfile. You can use environment variables, define "global" variables, and task-scoped variables. There are also dynamic variables that can receive the output of commands. I think this should cover the same functionality of make.
Unfortunately there is not a way to include an entire directory of Taskfiles or include wildcard patterns. It is possible to include multiple individual Taskfiles by filename or parent directory and you can add
optional: true
. I'm sure this feature would happily be accepted in a PR!Admittedly, It sounds like your use of make is more advanced than my experience with it, and C/C++ projects are probably good candidates to keep using make since it is purpose-built for those scenarios.
I actually principally use Make with golang, and mostly to create project level behaviors that are somewhat like cargo does for rust, such as having an externally visible project version, a defined target/ output directory, and standardized build types (debug and release). I also use it to setup standard install target paths and to create detached golang source tarballs with a populated vendor directory in a consistent way that I can feed into package builders, such as a debian buildd. Many public package build sites explicitly sandbox the build without internet access. This lets me produce generic os system packages out of go projects very easily.
Plugins do make it easier to define common Makefile code I use to define behavior that I will reuse in multiple projects which won't change, and so have it less visible. For example, I always want to use staticcheck and let it re-write sources when building an "all" debug target.
Interesting! I hadn't heard of
Taskfile
before, will be sure to check it out. This has definitely annoyed me in the past too when setting up dev tooling:Also, welcome to the DEV community! 👋
Thanks for the warm welcome!