目录

Make 的替代品

用 Task(Taskfile)以 YAML 重写构建流程

Make 诞生于 1976 年,几乎是 Unix 生态里事实上的构建工具标准。它的 Makefile 语法以「Tab 缩进敏感」「跨平台 shell 行为不一致」「变量与模式规则陡峭」著称。在 Go、Node.js、Rust 这类现代化项目里,直接用 Make 经常会踩到平台差异和语法陷阱。

社区因此涌现了一批 Make 的替代品,其中较有代表性的一款是 Task(常被称作 Taskfile)。它用 Go 实现,以单二进制分发,配置文件 Taskfile.yml 用 YAML 描述,跨平台、零依赖,语法对现代开发者更友好。

Task 的官方定义是「A fast, cross-platform build tool inspired by Make, designed for modern workflows」——一款受 Make 启发、为现代工作流设计的快速跨平台构建工具。其核心特点包括:

  • YAML 配置:任务定义在 Taskfile.yml 中,告别 Makefile 的 Tab 缩进和宏语言。
  • 跨平台一致:同一份 Taskfile.yml 可以在 Linux、macOS、Windows 上运行,内部通过 mvdan/sh(原生 Go 实现的 shell 解释器)执行命令,Windows 上也能跑类 bash 语法。
  • 单二进制、零依赖:官方提供单一可执行文件,不需要预装 make、bash、coreutils。
  • 增量构建:通过 sources / generates / method 字段做文件指纹(timestamp 或 checksum)比对,自动跳过无需重跑的任务。
  • 任务依赖与并行执行:deps 声明前置任务,默认并行运行。
  • 模块化:includes 把多个 Taskfile 组合起来,适合 monorepo。

截至撰稿时,Task 最新稳定版为 v3.52.0,采用 MIT 协议开源,GitHub 上有 15k+ star。文档站点是 taskfile.dev

知名采用方包括 Docker、Vercel、HashiCorp、Microsoft、Google Cloud、AWS、Anthropic、MongoDB,以及 Go 生态里的 GoReleaserArduino CLI 等项目。

Task 提供了非常多的安装途径。常见方式如下:

# Homebrew(macOS / Linuxbrew)
brew install go-task/tap/go-task
# 或核心仓库
brew install go-task

# Go 安装(需要本地有 Go 工具链)
go install github.com/go-task/task/v3/cmd/task@latest

# npm
npm install -g @go-task/cli

# 官方安装脚本(适合 CI 或自定义目录)
sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b ~/.local/bin

此外还支持 Snap(snap install task --classic)、Scoop(scoop install task)、WinGet(winget install Task.Task)、APT/DNF(通过 Cloudsmith 提供的仓库)、Arch 的 pacman -S go-task、FreeBSD 的 pkg install task 等,详见官方安装文档。GitHub Actions 也有现成的 go-task/setup-task Action。

安装完成后用 task --version 验证。

在项目根目录运行:

task --init

会生成一份最小可用的 Taskfile.yml:

version: '3'

vars:
  GREETING: Hello, World!

tasks:
  default:
    desc: Print a greeting message
    cmds:
      - echo "{{.GREETING}}"
    silent: true

几点说明:

  • version: '3' 声明所要求的最低 Task 版本(目前主版本是 3)。
  • vars 定义全局变量,任务体中用 Go template 语法 {{.VAR}} 引用。
  • tasks 下每一个键就是一个任务,defaulttask 不带参数时执行的默认任务。
  • desc 是任务的一句话描述,会出现在 task --list 输出里。
  • cmds 是该任务依次执行的命令列表。
  • silent: true 抑制 Task 自身的「task: …」提示行。

查看当前可调用的任务:

task --list

运行某个任务:

task build
# 或显式指定默认任务
task default

除了顶层 vars,Task 还支持环境变量、动态变量(通过 sh: 执行 shell 取值)、命令行 --set 传入变量等。模板引擎是 Go 标准库的 text/template,可以写条件、循环、函数调用。

version: '3'

vars:
  VERSION:
    sh: git describe --tags --always

tasks:
  build:
    cmds:
      - go build -ldflags "-X main.version={{.VERSION}}" -o bin/app ./cmd/app

deps 列出的任务会在主任务之前执行,且默认并行。这一行为和 Make 的串行先决条件不同,需要写串行脚本时要用 deps: 配合 cmds: 或者显式拆分。

tasks:
  build:
    deps: [assets]
    cmds:
      - go build -v -o bin/app ./cmd/app

  assets:
    cmds:
      - esbuild --bundle --minify css/index.css > public/bundle.css

需要把变量传给依赖任务,可以用对象写法:

tasks:
  default:
    deps:
      - task: echo
        vars: { TEXT: 'before 1' }

这是 Task 相对 Make 不逊色的关键能力。声明 sourcesgenerates 之后,Task 会对文件做指纹比对,跳过无需重跑的任务:

tasks:
  js:
    cmds:
      - esbuild --bundle --minify js/index.js > public/bundle.js
    sources:
      - src/js/**/*.js
    generates:
      - public/bundle.js
    method: checksum    # 默认值,也可改为 timestamp 或 none

method 取值:

  • checksum(默认):基于源文件与产物的内容哈希判断。
  • timestamp:基于 mtime 判断,适合大仓库。
  • none:总是执行。

源文件命中后,Task 会输出 Task "js" is up to date,与 Make 的「nothing to be done」体验一致。

preconditions 用来检查运行环境,失败则整个任务(以及依赖它的任务)中止。和 status/sources 的「跳过」语义不同,它是硬性校验。

tasks:
  deploy:
    cmds:
      - kubectl apply -f deploy.yaml
    preconditions:
      - test -f .env
      - sh: command -v kubectl
        msg: "kubectl 未安装,无法部署"

internal: true 的任务不会出现在 task --list 输出里,但仍可被其他任务调用,适合做「函数式」的复用任务:

tasks:
  build-image-1:
    cmds:
      - task: build-image
        vars: { DOCKER_IMAGE: image-1 }

  build-image:
    internal: true
    cmds:
      - docker build -t {{.DOCKER_IMAGE}} .

monorepo 或大型项目里,可以把子目录的 Taskfile.yml 引入并加上命名空间前缀:

includes:
  docs: ./documentation            # 引入 ./documentation/Taskfile.yml
  docker: ./DockerTasks.yml

# 调用:task docs:serve / task docker:build

includes 还支持 optional(缺失则忽略)、flatten(平铺到根命名空间)、excludes(排除特定任务)、vars(给被引入的 Taskfile 注入变量)、aliases(短别名)等选项。

维度MakeTask
配置文件Makefile(Tab 缩进、自定义宏语言)Taskfile.yml(YAML)
跨平台受 shell 与 coreutils 差异影响大内置 mvdan/sh,Windows 也可跑类 bash 语法
任务依赖串行先决条件默认并行 deps
增量构建基于文件 mtimechecksum / timestamp / none 三选一
变量与模板自有宏语言,语法陡峭Go text/template,可执行 shell 取值
分发形态系统包单二进制,零依赖
生态历史悠久、几乎无处不在新兴但已被 Docker/Vercel/HashiCorp 等采用

并不是说 Task 在所有维度都比 Make 强——Make 的成熟度、 phony target、模式规则、$(make) 等递归调用约定仍是其优势,大型 C/C++ 项目和许多遗留构建链离不开它。Task 更适合 Go / Node / Rust 等现代语言项目、CI 流水线、需要跨平台一致性的工具链脚本。

下面是一个常见 Go 服务项目的 Taskfile.yml,覆盖了开发、测试、构建、发布等环节:

version: '3'

vars:
  APP: myapp
  VERSION:
    sh: git describe --tags --always --dirty

tasks:
  default:
    desc: 列出所有任务
    cmds:
      - task --list

  dev:
    desc: 启动开发服务器(带热重载)
    cmds:
      - go run ./cmd/{{.APP}} --dev
    sources:
      - ./**/*.go

  test:
    desc: 运行测试
    cmds:
      - go test -race -cover ./...

  lint:
    desc: 运行 golangci-lint
    cmds:
      - golangci-lint run ./...
    preconditions:
      - sh: command -v golangci-lint
        msg: "请先安装 golangci-lint"

  build:
    desc: 构建二进制
    deps: [lint, test]
    cmds:
      - mkdir -p bin
      - go build -ldflags "-s -w -X main.version={{.VERSION}}" -o bin/{{.APP}} ./cmd/{{.APP}}
    sources:
      - ./**/*.go
    generates:
      - bin/{{.APP}}
    method: checksum

  clean:
    desc: 清理产物
    cmds:
      - rm -rf bin/

  release:
    desc: 通过 GoReleaser 发布
    internal: true
    cmds:
      - goreleaser release --clean
    preconditions:
      - test -n "{{.GITHUB_TOKEN}}"

调用 task build 会先并行跑 linttest,再构建二进制;Go 源码未改动时再次执行会被 method: checksum 跳过。

Task 不是要消灭 Make,而是为现代项目提供一份更易读、跨平台、零依赖的替代方案。如果你的团队里有 Windows 同事、CI 跑在多平台、或者你受够了 Makefile 的 Tab 缩进和宏语法,迁移到 Taskfile.yml 通常只需要一个下午。代价是引入了新工具链和 YAML 表达力有限(复杂逻辑仍要靠 sh 字段兜底),但对于绝大多数业务脚本和构建编排来说,收益远大于成本。

相关内容