Makefile 平替:跨平台构建脚本 Taskfile

摘要
这篇文章介绍自动化构建工具 go-task 的使用,涵盖工具安装、基本语法规则以及进阶使用,另外对在 Windows 平台使用进行了特殊说明。总结部分提供的 Python 虚拟环境自动构建脚本是对全文内容的综合实践,也是我真正应用到项目中,确实有带来生产效率提升的实用脚本,欢迎使用。

Task 是用 Go 语言编写的任务执行/构建工具,对比 GNU make,Task 语法规则[1]更加简单且语义化,学习成本更低,同时 Task 脚本支持跨平台执行,除了 Linux 和 Mac 外,Windows 系统通过使用 GitBash 也能完全兼容执行。使用 Task 编写构建任务、自动化脚本,可以极大提高开发协作效率。

Task 执行文件为 Taskfile,采用 yaml 格式,下面以 cowsay 为例进行快速开始演示:

1
2
3
4
5
6
7
8
9
# Taskfile.yml
version: '3'

tasks:
  default:
    desc: "This is the default task"
    silent: true
    cmds:
      - cowsay "Are you ready to go task ?"
1
2
3
4
5
6
7
8
9
$ task
 ___________________________
< Are you ready to go task ? >
 ---------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

default 任务非必须,是 Task 不带任务参数执行时的默认选项,可以通过 task --list 或者 task -l 查看所有可执行任务。

工具安装

Task 安装非常方便,参考官方安装文档[2],支持多种包管理工具,比如 Mac / Linux 平台的 Homebrew、Tea,Windows 平台的 Chocolatey、Scoop,Node 的 npm,也可以使用安装包、安装脚本或者直接从源码编译安装。以 brew 为例:

1
2
3
$ brew install go-task
$ task --version
Task version: 3.32.0

基本使用

Task 有非常完善的使用文档[3],可以先快速浏览,然后在实际使用时作为手册查阅。

命令参数

Task 的核心是任务,对应 cmds 参数,支持多种格式[4],最常用的是执行 shell 命令或者执行其他 task。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
version: '3'

tasks:
  example:
    desc: example task
    cmds:
      # 执行 shell 命令
      - cmd: echo "hello world"
      # 直接输入字符串与 cmd 等价
      - echo "hello world"
      # 多行 shell 脚本
      - |
        cat << EOF > output.txt
        hello world
        EOF        
      # 执行其它 task
      - task: another
  
  another:
    desc: another example task
    cmds:
      - cat output.txt

任务依赖

除了可以使用 cmds.task 直接调用执行其他任务外,还可以通过 deps 声明任务依赖,在当前任务开始前,所有依赖会先执行完成,多个依赖项并行执行。

如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
version: '3'

tasks:
  default:
    deps: [before_1, before_2]
    cmds:
      - echo "after"

  before_1:
    cmds:
      - echo "before 1"

  before_2:
    cmds:
      - echo "before 2"
1
2
3
4
$ task
before 2
before 1
after

环境变量

通过设置环境变量可以控制命令行为(比如调整 pypi 镜像)。 使用 env 设置全局环境变量,使用 Task.env 设置任务局部环境变量,环境变量使用 $ 符号访问。

如下示例中,install 任务识别全局环境变量使用清华源加速,install-test 任务识别单独配置环境变量使用阿里云源加速:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
version: '3'

env:
  PIP_INDEX_URL: https://pypi.tuna.tsinghua.edu.cn/simple

tasks:
  install:
    desc: install requirements.txt
    cmds:
      - pip install -r requirements.txt

  install-test:
    desc: install test packages
    env:
      PIP_INDEX_URL: https://mirrors.aliyun.com/pypi/simple
    cmds:
      - cmd: echo using index $PIP_INDEX_URL
        silent: true
      - pip install pytest pytest-cov

变量

变量在 Taskfile 脚本中使用,可以增强任务灵活度,方便任务复用。使用 vars 设置全局变量,使用 Task.vars 设置任务局部变量,变量使用 go 模板如 {{.VAR_NAME}} 访问。

变量取值按优先级从高到低依次为:

  1. Task.vars 中定义的任务局部变量值
  2. 被其他任务直接调用时在 Command.vars 中定义的变量值
  3. 通过命令行参数传入的变量值
  4. 通过 includes 导入的外部 Taskfile 中定义的全局变量值
  5. 当前 Taskfile 中定义的全局变量值
  6. 通过 includes 导入的外部 Taskfile 中定义的全局环境变量值
  7. 当前 Taskfile 中定义的全局环境变量值
  8. 通过命令行参数传入的环境变量值
  9. 系统环境变量值

通过下面的示例来演示变量取值,任务 echo 在控制台输出变量 MESSAGE 的值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Taskfile.yml
version: '3'

includes:
  docs: Taskfile1.yml

vars:
  MESSAGE: "v5"

env:
  MESSAGE: "v7"

tasks:
  do-echo:
    internal: true
    vars:
      MESSAGE: "v1"
    cmds:
      - echo {{.MESSAGE}}

  echo:
    desc: echo message
    cmds:
      - task: do-echo
        vars:
          MESSAGE: "v2"
1
2
3
4
5
6
7
8
# Taskfile1.yml
version: '3'

vars:
  MESSAGE: "v4"

env:
  MESSAGE: "v6"

根据顺序执行,{{.MESSAGE}} 取值按照优先级从高到低依次为 v1v9,其中 v3 是命令行参数传入的变量值,v8 是命令行参数传入的环境变量值,v9 是系统环境变量值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
$ export MESSAGE=v9
$ MESSAGE=v8 task echo MESSAGE=v3
v1
# 注释掉 v1 后执行
$ MESSAGE=v8 task echo MESSAGE=v3
v2
# 注释掉 v2 后执行
$ MESSAGE=v8 task echo MESSAGE=v3
v3
$ MESSAGE=v8 task echo
v4
# 依次注释掉 v4,v5,v6 后执行
$ MESSAGE=v8 task echo
# 结果依次为 v5,v6,v7
# 注释掉 v7 后执行
$ MESSAGE=v8 task echo
v8
$ task echo
v9

工作目录

默认情况下,所有任务命令会在 Taskfile 所在目录执行,可以通过 dir 指定命令执行目录,比如下面的任务会在 client 目录执行客户端构建:

1
2
3
4
5
6
7
8
9
version: '3'

tasks:
  client:build:
    desc: build client dist file
    dir: client
    cmds:
      - yarn
      - yarn build

引用其他 Taskfile

Taskfile 引用可以在多种场景中发挥作用,比方说引入通用工具类,或者根据功能不同将任务按文件进行分组等。

可以像如下示例中的 docs 一样直接使用路径进行引入,也可以像 dev 一样定义参数引入,需要注意的是,引入后的所有命令默认都在当前 Taskfile 目录下执行,除非通过 dir 参数修改引入脚本的执行目录。

1
2
3
4
5
6
7
8
9
# Taskfile.yml
version: '3'

includes:
  docs: docs/Taskfile.yml
  dev:
    taskfile: dev/Taskfile.yml
    vars:
      REQ_FILE: requirements-dev.txt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# dev/Taskfile.yml
version: '3'

vars:
  REQ_FILE: requirements.txt

tasks:
  build:
    desc: build dev environment
    cmds:
      - echo "build with {{.REQ_FILE}}"
1
2
3
4
5
6
7
8
# docs/Taskfile.yml
version: '3'

tasks:
  build:
    desc: build docs
    cmds:
      - echo "build docs"
1
2
3
4
$ task --list
task: Available tasks for this project:
* dev:build:        build dev environment
* docs:build:       build docs

进阶使用

动态变量

可以通过 shell 脚本在运行时动态设置变量的值。比如下面的任务会获取当前 Git 提交哈希值设置变量 IMAGE_TAG

1
2
3
4
5
version: '3'

vars:
  IMAGE_TAG:
    sh: git rev-parse --short HEAD

非必要不执行

对于有中间产物生成的任务,可以通过设置条件判断中间产物是否需要更新,从而减少不必要的任务执行。

通过 task --force 或者 task -f 可以强制执行任务。

文件内容比对

Task 通过 sourcesgenerates 指定源文件和中间产物,支持文件路径或者 glob 表达式,提供两种校验方式:

  • checksum: 中间产物生成后,Task 记录源文件的校验和,只有当源文件校验和发生变更时任务才需要重新执行
  • timestamp:Task 记录最后一次任务执行时间,只有当任一源文件修改时间比最后执行任务时间新时任务才需要重新执行

默认方式为 checksum,两种校验方式可以通过观察项目根目录下的 .task 文件夹内容验证。使用示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
version: '3'

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

命令执行比对

可以通过 status 运行命令测试,根据命令执行结果判断中间产物是否需要更新,这种方式具有非常大的灵活度。

比如下面的例子判断只要 directory 目录以及其下的两个文件存在,任务就不需要重复执行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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

go 模板引擎

Task 脚本在执行之前会使用 Go 模板引擎[5]进行解析,支持全部 slim-sprig 库函数[6]以及 Task 额外增加的平台相关等函数。

使用示例如下:

1
2
3
4
5
6
7
8
9
version: '3'

tasks:
  print-os:
    cmds:
      - echo '{{OS}} {{ARCH}}'
      - echo '{{if eq OS "windows"}}windows-command{{else}}unix-command{{end}}'
      # This will be path/to/file on Unix but path\to\file on Windows
      - echo '{{fromSlash "path/to/file"}}'

Windows 环境使用

开头介绍 Task 时说到 Windows 完全兼容执行需要依赖 GitBash,实际上 Task 使用 go 原生 sh 解析库 mvdan/sh [7]解析 shell 脚本,其提供非完全跨平台的能力,使用 GitBash 是为了补齐 Windows 不支持的部分内置 shell 命令,如 rmmv 等,所以这里进行单独说明,Windows 平台安装使用指南如下:

1
2
3
4
5
6
7
8
9
# 使用 管理员权限 打开 powershell
# 一行命令安装 Chocolatey
$ Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
# 验证 Chocolatey
$ choco
# 使用 Chocolatey 安装 go-task
$ choco install go-task
# 打开 GitBash 窗口,执行 task 命令
$ task --list

总结

使用 Task 将项目构建过程定义为自动化脚本,在提升工作效率的同时,也是在进行知识沉淀,类似的脚本可以复用,不断丰富自己的工具箱。同时可参考 Taskfile 规范[8],编写高质量的脚本代码。

下面是我在 Python 项目中基本都会使用到的一段脚本,主要功能包括:

  1. 根据 conda 虚拟环境目录是否存在,自动执行虚拟环境创建或者虚拟环境更新;
  2. 识别 environment.ymlrequirements.txt 配置文件修改时间,判断是否需要重新执行依赖安装;
  3. 设置 PYTHONPATH 环境变量为项目根目录,遵循 Python 模块路径导入规范;

使用这段脚本后,不管是初次克隆项目还是项目代码更新,只需要简单执行 task 命令然后回车,脚本就会自动处理虚拟环境设置工作,多人协作时大家使用相同的脚本构建可以保证环境一致性。同时因为监听了配置文件修改时间,脚本判断只有在必要的时候才真正执行环境构建,多次重复执行 task 命令也不会有负担。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
version: '3'

vars:
  CONDA_PREFIX: ./venv
  CONDA_ENV_FILE: environment.yml
  PIP_REQ_FILE: requirements.txt

tasks:
  default:
    desc: build or update venv
    cmds:
      - task: venv:build
    silent: true

  venv:build:
    desc: build conda venv when config file updated
    cmds:
      - task: venv:inner-build
      - task: venv:config-vars
    silent: true

  venv:inner-build:
    internal: true
    silent: true
    cmds:
      - test ! -d {{.CONDA_PREFIX}} || conda env update -f {{.CONDA_ENV_FILE}} -p {{.CONDA_PREFIX}}
      - test -d {{.CONDA_PREFIX}} || conda env create -f {{.CONDA_ENV_FILE}} -p {{.CONDA_PREFIX}}
      - touch {{.CONDA_PREFIX}}
    status:
      - test {{.CONDA_PREFIX}} -nt {{.CONDA_ENV_FILE}}
      - test {{.CONDA_PREFIX}} -nt {{.PIP_REQ_FILE}}

  venv:config-vars:
    desc: config environment variables of venv
    cmds:
      - conda env config vars set PYTHONPATH="{{.PYTHONPATH}}" -p {{.CONDA_PREFIX}}
    vars:
      PYTHONPATH:
        sh: pwd

参考资料

[1]. Taskfile 语法规则
[2]. Task 官方安装文档
[3]. Task 官方使用指南
[4]. Task 命令参数
[5]. Task Go 模板引擎
[6]. slim-sprig 库函数
[7]. GitHub:mvdan/sh
[8]. Taskfile 编写规范

0%