Makefile基本使用

Makefile简介

Makefile 是一个文本文件(默认情况下使用名为 Makefile 的文件),其中列出了按所谓 “target”分组的命令,你可以将其视为应一起执行的命令组。我们用 TAB 缩进字符来表示属于某个目标的命令。
默认情况下,make 会同时打印执行的命令和命令本身的结果。如果需要,你可以关闭这种行为,不过通常情况下,你确实希望看到被执行的内容。一开始,同时看到命令和输出结果可能会让人有点困惑,但你的眼睛很快就会习惯的。

Makefiles支持我们开发工作流,我们希望可以实现以下功能:

  1. 依赖管理—-跳过已经完成的步骤
  2. 重新进入—-从特定点重新启动工作流
  3. 自动化—-使用相同的代码并行运行多个相同的分析

一个Makefile文件的基本结构如下:

1
2
3
4
5
6
foo:
echo Hello John!

bar:
echo Hello Jane!
echo Hello Everyone!

执行全部代码:

1
make -f Makefile

执行指定代码:

1
make -f Makefile foo

Tip: 如果文件名字是Makefile,也可以直接省略-f参数和文件名。

需要注意的是,Makefile 中的命令必须使用 Tab 键缩进,不能使用空格。不过我们可以通过设置 .RECIPEPREFIX 变量来修改这个行为,例如:

1
2
3
4
5
6
7
8
9
# Sets the prefix for commands.
.RECIPEPREFIX = >

foo:
> echo Hello John!

bar:
> echo Hello Jane!
> echo Hello Everyone!

Makefile使用技巧

好的默认设置

1
2
3
4
5
6
7
8
9
10
11
SHELL := bash
.ONESHELL:
.SHELLFLAGS := -eu -o pipefail -c
.DELETE_ON_ERROR:
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules

ifeq ($(origin .RECIPEPREFIX), undefined)
$(error This Make does not support .RECIPEPREFIX. Please use GNU Make 4.0 or later)
endif
.RECIPEPREFIX = >
1
2
# Deletes dependencies if the command fails.
.DELETE_ON_ERROR:

作用:
• 这个指示符告诉 make 如果在执行目标的命令时发生错误,应该删除目标文件。

好处:
• 保持构建环境的整洁:如果某个构建目标在执行过程中失败,它会删除部分生成的文件,从而避免留下半成品或不完整的文件。
• 避免误用不完整的构建:当构建失败时,目标文件可能是部分生成的,保留这些文件可能会导致后续的构建过程误以为这些文件是完整的,从而造成错误。删除这些文件可以避免这种情况。

1
2
# Warns about undefined variables.
MAKEFLAGS += --warn-undefined-variables --no-builtin-rules

作用:
• —warn-undefined-variables: 这个选项会使 make 在使用未定义变量时发出警告。
• —no-builtin-rules: 这个选项会禁用所有的内建规则和变量,使用者需要在 Makefile 中明确地定义所有需要的规则。

好处:
• 提高可调试性:
• —warn-undefined-variables: 当使用未定义变量时发出警告,有助于在构建过程中快速发现和修复拼写错误或遗漏定义的变量。这对于维护大型和复杂的 Makefile 特别有用。
• 增强可控性和可预测性:
• —no-builtin-rules: 禁用内建规则,确保所有的构建规则都是显式定义的,从而提高 Makefile 的可读性和可维护性。这样可以避免一些隐式规则导致的意外行为,使构建过程更加透明和可预测。

1
SHELL := bash

作用:
• 这个指示符告诉 make 使用 bash 作为 shell。

1
.ONESHELL:

作用:
• 这个指示符告诉 make 在执行目标的命令时,将所有的命令放在同一个 shell 进程中执行。

1
.SHELLFLAGS := -eu -o pipefail -c

作用:
-e: 如果任何命令行返回非零状态(失败),则立即退出脚本。
-u: 当执行到未设置的变量时,shell将会报错并退出。
-o pipefail: 这个选项会导致管道(|)中的命令如果有任何一个失败了,整个管道的返回状态都会是失败的。默认情况下,只有最后一个命令的状态会影响整个管道的状态。
-c: 这个标志告诉shell从字符串中读取命令。

“Dry-run”模式

如果你想要查看 make 命令将执行的命令,而不实际执行它们,可以使用 -n--just-print 选项。这个选项会显示 make 将执行的命令,但不会实际执行它们。

1
make fastq align -n

变量命名

Makefile一般通过${foo}来引用变量,这和bash脚本中是一样的。
但在Makefile中,可以使用?=来定义变量,如此定义的变量我们可以在命令控制行中进行控制。

例如:

1
2
3
4
5
6
7
8
# Set the value of FOO to bar (if not already set).
NAME ?= Jane

# Sets the prefix for commands.
.RECIPEPREFIX = >

usage:
> echo Hello ${NAME}

我们可以对NAME进行赋值,如:

1
make usage NAME=Joe

隐藏命令

一般情况下,make会打印出执行的命令,但有时候我们不希望看到这些命令,可以使用@符号来隐藏命令。

1
2
3
4
5
# Sets the prefix for commands.
.RECIPEPREFIX = >

usage:
> @echo "Hey Jude"

文本替换

有时候我们想从路径中提取目录名或文件名,如:

  • PATH = data/reads/abc.txt
    则:
  • DIR = $(dir ${PATH})将包含data/reads/
  • FNAME = $(notdir ${PATH})将包含abc.txt

一些常见的使用如下:

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
#
# How to generate patterns
#

# Sets the prefix for commands.
.RECIPEPREFIX = >

# Set a filename
FILE = data/refs/ebola.fa.gz

demo:

# Prints: data/refs/ebola.fa.gz
> @echo ${FILE}

# Prints: data/refs
> @echo $(dir ${FILE})

# Prints: ebola.fa.gz
> @echo $(notdir ${FILE})

# Prints: data/refs/human.fa.gz
# 使用 subst 函数将 FILE 变量中的 ebola 替换为 human,结果为 data/refs/
> @echo $(subst ebola,human,${FILE})

# Prints: data/refs/ebola.fa
# 使用 patsubst 函数将 FILE 变量中的以 .gz 结尾的字符串替换为没有 .gz 的字符串,结果为 data/refs/ebola.fa
> @echo $(patsubst %.gz,%,${FILE})

打印说明

有时候我们想知道自己编写的Makefiles中有哪些作用,我们可以编写一个说明来帮助我们理解。

1
2
3
4
5
6
7
8
9
10
11
# Set the prefix from tabs to >
.RECIPEPREFIX = >

# Set the project number.
PRJN ?= PRJEB31790

# Default action is to print usage
usage:
> @echo ""
> @echo "Usage: make data trim PRJN=${PRJN}"
> @echo ""

我们可以运行make usage来查看说明。另外,如果我们没有指定“target“,则默认执行第一个“target”。

依赖管理

Makefile中的依赖管理是非常重要的,我们可以通过依赖管理来跳过已经完成的步骤,从而提高构建速度。

1
2
3
4
5
6
7
8
9
10
11
# Set the prefix from tabs to >
.RECIPEPREFIX = >

counts.txt:
> echo 100 > counts.txt

names.txt:
> echo Joe > names.txt

results.txt: counts.txt names.txt
> cat counts.txt names.txt > results.txt

如果不存在results.txt文件,make会先生成counts.txt和names.txt文件,然后再生成results.txt文件。