如何使用Makefiles来实现重复性任务的自动化

180 阅读7分钟

简介

如果你有在Linux服务器上从源代码安装软件的经验,你可能已经遇到了make 工具。这个工具主要用于编译和构建程序。它允许源代码的作者列出构建该特定项目所需的步骤。

尽管make是为自动化软件编译而创建的,但该工具的设计足够灵活,几乎可以用来自动化任何你可以从命令行中完成的任务。在本指南中,我们将讨论如何重新利用make来自动化依次发生的重复性任务。

前提条件

任何类型的Linux环境都适用于本教程。为Ubuntu/Debian Linux和Red Hat/Rocky Linux提供了软件包安装说明。

安装Make

大多数Linux发行版允许你用一个命令来安装一个编译器,但默认情况下并不提供。

在Ubuntu上,你可以安装一个叫做build-essential 的软件包,它将提供一个现代的、支持良好的编译器环境所需的所有软件包。更新你的软件包来源,并安装apt

sudo apt update
sudo apt build-essential

在Rocky Linux或其他Red Hat的衍生产品上,你可以安装一组名为Development Tools的软件包,以提供同样的编译器功能。用dnf 安装这些软件包。

dnf groups mark install "Development Tools"
dnf groupinstall "Development Tools"

你可以通过检查你的系统上是否存在make 命令来验证编译器是否可用。为了做到这一点,请使用which 命令。

which make
Output
/usr/bin/make

现在你有了这些工具,可以让你在通常情况下也能利用make的功能。

了解Makefiles

make命令接收指令的主要方式是通过使用Makefile

Makefile是特定于目录的,这意味着make将在它被调用的目录中搜索这些文件。我们应该把Makefile放在我们将要执行的任何任务的根目录下,或者放在调用我们将要编写的脚本最合理的地方。

在Makefile中,我们遵循一个特定的格式。Make以这种方式使用目标、源和命令的概念。

Makefile

target: source
    command

其中的对齐方式和格式非常重要。我们将在这里讨论这些组件的格式和含义。

目标

目标是一个用户指定的名称,用来指代一组命令。可以把它看作是类似于编程语言中的函数。

一个目标在左边一栏对齐,是一个连续的词(没有空格),并以冒号(:)结束。

在调用make时,我们可以通过输入指定一个目标。

make target_name

然后,Make将检查Makefile并执行与该目标相关的命令。

源是对文件或其他目标的引用。它们代表它们所关联的目标的先决条件或依赖性。

例如,你可以在你的Makefile中设置一个看起来像这样的部分。

Makefile

target1: target2
    target1_command

target2:
    target2_command

在这个例子中,我们可以像这样调用目标1。

make target1

然后,Make将进入Makefile并搜索target1 目标。然后,它将检查是否有任何指定的源。

它将找到target2 源的依赖关系,并暂时跳到该目标。

从那里,它将检查target2是否有任何源被列出。它没有,所以它将继续执行target2_command 。在这一点上,make将到达target2 命令列表的末尾,并将控制权传回给target1 目标。然后,它将执行target1_command 并退出。

源可以是文件,也可以是目标本身。Make使用文件的时间戳来查看一个文件在其最后一次调用后是否被修改过。如果一个源文件被修改了,该目标将被重新运行。否则,它就将该依赖关系标记为已完成,并继续执行下一个源文件,如果那是唯一的源文件,则继续执行命令。

一般的想法是,通过添加源文件,我们可以建立一个必须在当前目标之前执行的依存关系的顺序集。你可以在任何目标之后指定一个以上的源,用空格分隔。你可以开始看到你如何指定详细的任务序列。

命令

使make命令具有如此大的灵活性的原因是,语法中的命令部分是非常开放的。你可以指定在目标下运行的任何命令。你可以根据需要添加任意多的命令。

命令被指定在目标声明后的那一行。它们被缩进了一个制表符。有些版本的make对于命令部分的缩进方式很灵活,但一般来说,你应该坚持使用一个制表符,以确保make能够识别你的意图。

Make认为目标定义下的每一个缩进行都是一个独立的命令。你可以根据自己的需要添加任意多的缩进行和命令。Make会一个一个地浏览它们。

我们可以在命令前放置一些东西,告诉make以不同的方式处理它们。

  • -:命令前的破折号告诉make在遇到错误时不要放弃。例如,如果你想在一个文件上执行一个命令,如果它是存在的,这可能很有用,如果它不存在,则什么也不做。

  • @:如果你用@ 符号引导一个命令,命令调用本身将不会被打印到标准输出。这主要是用来清理make 产生的输出。

附加功能

一些附加功能可以帮助你在你的Makefile中创建更复杂的规则链。

变量

Make可以识别变量(或宏),这些变量在你的Makefile中起着占位符的作用。最好是在文件的顶部声明这些变量。

每个变量的名称都要完全大写。在名称后面,一个等号将该名称分配给右边的值。例如,如果我们想将一个安装目录定义为/usr/bin ,我们可以在文件的顶部添加INSTALLDIR=/usr/bin

在文件的后面,我们可以通过使用$(INSTALLDIR) 语法来引用这个位置。

转移换行

我们可以做的另一件有用的事情是允许命令跨越多行。

我们可以在命令部分使用任何命令或shell功能。这包括通过用\ 来结束这一行来转义换行字符。

Makefile

target: source
    command1 arg1 arg2 arg3 arg4 \
    arg5 arg6

如果你利用shell的一些程序化功能,比如if-then语句,这就变得更加重要。

Makefile

target: source
    if [ "condition_1" == "condition_2" ];\
    then\
        command to execute;\
        another command;\
    else\
        alternative command;\
    fi

这将会像执行一个单行命令一样执行这个块。事实上,我们本可以把它写成一行,但像这样分解,可读性会大大增强。

如果你要转义行末字符,请确保在\ ,否则你会得到一个错误。

文件后缀规则

你可以在文件处理中使用的另一项功能是文件后缀。这些是一般的规则,提供了一种根据文件扩展名处理文件的方法。

例如,如果你想处理一个目录中的所有.jpg文件,并使用ImageMagick套件将它们转换为.png文件,我们可以在我们的Makefile中有这样的内容。

Makefile

.SUFFIXES: .jpg .png

.jpg.png:
    @echo converting $< to $@
    convert $< $@

这里有几件事我们需要看一下。

第一部分是.SUFFIXES: 声明。这告诉make ,我们将在文件后缀中使用所有的后缀。一些在编译源代码时经常使用的后缀,如.c.o 文件,是默认包含的,不需要在这个声明中标注。

下一个部分是实际后缀规则的声明。它的形式是 original_extension.target_extension:.

这不是一个实际的目标,但它将匹配任何对第二个后缀的文件的调用,并从第一个后缀的文件中构建它们。

在我们的例子中,如果我们的目录中有一个file.jpg ,我们可以这样调用make ,以建立一个名为file.png 的文件。

make file.png

make 将在 声明中找到png文件,并看到创建 文件的规则。然后,它将在目录中寻找目标文件,其中的 被 替换。然后,它将执行后面的命令。.SUFFIXES .png .png .jpg

后缀规则使用了一些我们还没有介绍过的变量。这些有助于根据它目前所处的过程的哪个部分,做出不同的信息替代。

  • $?: 这个变量包含了当前目标的依赖性列表,这些依赖性比目标更近。这些将是在执行这个目标下的命令之前必须重新做的目标。

  • $@:这个变量是当前目标的名称。这允许我们引用你要做的文件,即使这个规则是通过模式匹配的。

  • $<: 这是当前依赖关系的名称。在后缀规则的情况下,这是用于创建目标的文件的名称。在我们的例子中,这将包含file.jpg

  • $*:这个文件是当前依赖关系的名称,去掉了匹配的扩展名。把它看作是目标文件和源文件之间的一个中间阶段。

创建一个转换的Makefile

我们将创建一个Makefile,它将做一些图像处理,然后将文件上传到我们的文件服务器,这样我们的网站就可以显示它们。

如果你想跟着做,在你开始之前,确保你已经安装了ImageMagick软件包。这些是处理图像的命令行工具,我们将在我们的脚本中使用它们。

在Ubuntu或Debian上,更新你的软件包来源,用apt

sudo apt-get update
sudo apt-get install imagemagick

在Red Hat或Rocky上,你需要添加epel-release repo来获得像这样的额外软件包,然后用dnf 来安装。

dnf install epel-release
dnf install ImageMagick

在你的当前目录中,创建一个名为Makefile 的文件。

nano Makefile

在这个文件中,我们将开始实现我们的转换目标。

将所有JPG文件转换为PNG

我们的服务器已经被设置为只提供.png图片。正因为如此,我们需要在上传之前将任何.jpg文件转换为.png。

正如我们在上面学到的,后缀规则是一个很好的方法。我们将从.SUFFIX 声明开始,它将列出我们要转换的格式:.SUFFIXES: .jpg .png

之后,我们可以制定一个规则,将.jpg文件改为.png文件。我们可以使用ImageMagick套件中的convert 命令来做到这一点。转换命令的语法是 convert from_file to_file.

为了实现这个命令,我们需要后缀规则来指定我们开始使用的格式和结束使用的格式。

Makefile

.SUFFIXES: .jpg .png

.jpg.png:           ## This is the suffix rule declaration

现在我们有了匹配的规则,我们需要实现实际的转换步骤。

因为我们不知道到底什么文件名将在这里被匹配,我们需要使用我们学过的变量。具体来说,我们需要把$< 作为原始文件,把$@ 作为我们要转换的文件。如果我们把这与我们对转换命令的了解结合起来,我们就得到了这个规则。

Makefile

.SUFFIXES: .jpg .png

.jpg.png:
    convert $< $@

让我们添加一些功能,这样我们就可以通过echo语句明确地被告知正在发生什么。我们将在新命令和我们已经有的命令前加入@ 符号,以便在执行实际命令时使其不被打印出来。

Makefile

.SUFFIXES: .jpg .png

.jpg.png:
    @echo converting $< to $@ using ImageMagick...
    @convert $< $@
    @echo conversion to $@ successful!

在这一点上,我们应该保存并关闭该文件,以便我们可以测试它。

在当前目录中获取一个JPG文件。如果你手头没有文件,你可以使用wget ,从DigitalOcean网站下载一个。

wget https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_blue.png
mv DO_Powered_by_Badge_blue.png badge.jpg

你可以通过要求它创建一个badge.png ,来测试你的make文件到目前为止是否工作。

make badge.png
Output
converting badge.jpg to badge.png using ImageMagick...
conversion to badge.png successful!

Make会进入Makefile,看到.SUFFIXES 声明中的.png,然后转到匹配的后缀规则。然后它就会运行列出的命令。

创建一个文件列表

在这一点上,如果我们明确地告诉它我们想要那个文件,make可以创建一个.png文件。

如果它只是在当前目录下创建一个.jpg文件的列表,然后转换这些文件,那就更好了。我们可以通过创建一个变量来实现这一点,这个变量可以容纳我们所有要转换的文件。

最好的方法是使用通配符指令,如JPG_FILES=$(wildcard *.jpg)

我们可以直接用bash通配符指定一个目标,如JPG_FILES=*.jpg ,但这有一个缺点。如果没有.jpg文件,这实际上是试图在一个名为*.jpg 的文件上运行转换命令,这将失败。

我们上面提到的通配符语法在当前目录下编译了一个.jpg文件的列表,如果不存在,它不会将变量设置为任何东西。

当我们这样做的时候,我们应该尝试处理.jpg文件中常见的一个小变化。这些图像文件经常以.jpeg作为扩展名,而不是.jpg。为了自动处理这些,我们可以在我们的程序中把它们的名字改为.jpg文件。

我们将使用这两行来代替上面的内容。

Makefile

JPEG=$(wildcard *.jpg *.jpeg)     ## Has .jpeg and .jpg files
JPG=$(JPEG:.jpeg=.jpg)            ## Only has .jpg files

第一行编译了当前目录下的.jpg和.jpeg文件的列表,并将它们存储在一个名为JPEG 的变量中。

第二行引用这个变量并进行名称转换,将JPEG 变量中以.jpeg结尾的名称转换成以.jpg结尾的名称。这样做的语法是:$(VARNAME:.convert_from=.convert_to)

在这两行结束时,我们将有一个新的变量,叫做JPG ,其中只包含.jpg文件名。其中一些文件可能在系统中并不存在,因为它们实际上是.jpeg文件(没有发生实际的重命名)。这没关系,因为我们只是用这个列表来制作一个我们想要创建的.png文件的列表。

Makefile

JPEG=$(wildcard *.jpg *.jpeg)
JPG=$(JPEG:.jpeg=.jpg)
PNG=$(JPG:.jpg=.png)

现在,我们在变量PNG 中有一个我们想要的文件列表。这个列表只包含.png文件名,因为我们做了另一个名称转换。现在,这个目录中的每个文件都是.jpg或.jpeg文件,都被用来编制我们想要创建的.png文件列表。

我们还需要更新.SUFFIXES 声明和后缀规则,以反映我们现在正在处理.jpeg文件。

Makefile

JPEG=$(wildcard *.jpg *.jpeg)
JPG=$(JPEG:.jpeg=.jpg)
PNG=$(JPG:.jpg=.png)
.SUFFIXES: .jpg .jpeg .png

.jpeg.png .jpg.png:
    @echo converting $< to $@ using ImageMagick...
    @convert $< $@
    @echo conversion to $@ successful!

正如你所看到的,我们已经将.jpeg添加到后缀列表中,还为我们的规则添加了另一个后缀匹配。

创建一些目标

现在我们的Makefile里有很多东西,但是我们还没有任何正常的目标。让我们来解决这个问题,这样我们就可以把我们的PNG 列表传递给我们的后缀规则。

Makefile

JPEG=$(wildcard *.jpg *.jpeg)
JPG=$(JPEG:.jpeg=.jpg)
PNG=$(JPG:.jpg=.png)
.SUFFIXES: .jpg .jpeg .png

convert: $(PNG)

.jpeg.png .jpg.png:
    @echo converting $< to $@ using ImageMagick...
    @convert $< $@
    @echo conversion to $@ successful!

这个新目标所做的就是列出我们收集的.png文件名,作为一个要求。然后,Make查看是否有办法获得这些.png文件,并使用后缀规则来这样做。

现在,我们可以使用这个命令将我们所有的.jpg和.jpeg文件转换为.png文件。

make convert

让我们再添加一个目标。在将图像上传到服务器时,通常要做的另一项工作是调整它们的大小。让你的图片有正确的尺寸,将使你的用户在要求图片时不必临时调整尺寸。

ImageMagick的一个名为mogrify 的命令可以按照我们需要的方式调整图像的大小。比方说,我们网站上显示图片的区域是500px宽。我们可以用该命令对这个区域进行转换。

mogrify -resize 500\> file.png

这将调整任何大于500px宽的图片的大小以适应这个区域,但不会触及更小的图片。这就是我们想要的。作为一个目标,我们可以添加这个规则。

Makefile

resize: $(PNG)
    @echo resizing file...
    @mogrify -resize 648\> $(PNG)
    @echo resizing is complete!

我们可以像这样把它添加到我们的文件中。

Makefile

JPEG=$(wildcard *.jpg *.jpeg)
JPG=$(JPEG:.jpeg=.jpg)
PNG=$(JPG:.jpg=.png)
.SUFFIXES: .jpg .jpeg .png

convert: $(PNG)

resize: $(PNG)
    @echo resizing file...
    @mogrify -resize 648\> $(PNG)
    @echo resizing is complete!

.jpeg.png .jpg.png:
    @echo converting $< to $@ using ImageMagick...
    @convert $< $@
    @echo conversion to $@ successful!

现在,我们可以把这两个目标串起来,作为另一个目标的依赖关系。

Makefile

JPEG=$(wildcard *.jpg *.jpeg)
JPG=$(JPEG:.jpeg=.jpg)
PNG=$(JPG:.jpg=.png)
.SUFFIXES: .jpg .jpeg .png

webify: convert resize

convert: $(PNG)

resize: $(PNG)
    @echo resizing file...
    @mogrify -resize 648\> $(PNG)
    @echo resizing is complete!

.jpeg.png .jpg.png:
    @echo converting $< to $@ using ImageMagick...
    @convert $< $@
    @echo conversion to $@ successful!

你可能注意到,resize隐含地将运行与convert相同的命令。但我们还是要同时指定它们,以防并非总是如此。转换在将来可能会包含更多的处理。

webify目标现在可以转换和调整图片的大小了。

上传文件到远程服务器

现在我们已经为网络准备好了图片,我们可以创建一个目标,将它们上传到我们服务器上的静态图片目录。我们可以通过将转换后的文件列表传递给scp 来实现这一目标。

我们的目标将看起来像这样。

Makefile

upload: webify
    scp $(PNG) root@ip_address:/path/to/static/images

这将把我们所有的文件上传到远程服务器。我们的文件现在看起来像这样。

Makefile

JPEG=$(wildcard *.jpg *.jpeg)
JPG=$(JPEG:.jpeg=.jpg)
PNG=$(JPG:.jpg=.png)
.SUFFIXES: .jpg .jpeg .png

upload: webify
    scp $(PNG) root@ip_address:/path/to/static/images

webify: convert resize

convert: $(PNG)

resize: $(PNG)
    @echo resizing file...
    @mogrify -resize 648\> $(PNG)
    @echo resizing is complete!

.jpeg.png .jpg.png:
    @echo converting $< to $@ using ImageMagick...
    @convert $< $@
    @echo conversion to $@ successful!

清理

让我们添加一个清理选项,在本地的.png文件被上传到远程服务器后,把它们全部清除掉。

制作文件:Makefile

clean:
    rm *.png

现在,我们可以在顶部添加另一个目标,在我们将文件上传到远程服务器后调用这个目标。这将是最完整的目标,也是我们希望成为默认的目标。

为了指定这一点,我们将把它作为第一个可用的目标。这将被用作默认值。我们将按照惯例把它称为all

Makefile

JPEG=$(wildcard *.jpg *.jpeg)
JPG=$(JPEG:.jpeg=.jpg)
PNG=$(JPG:.jpg=.png)
.SUFFIXES: .jpg .jpeg .png

all: upload clean

upload: webify
    scp $(PNG) root@ip_address:/path/to/static/images

webify: convert resize

convert: $(PNG)

resize: $(PNG)
    @echo resizing file...
    @mogrify -resize 648\> $(PNG)
    @echo resizing is complete!

clean:
    rm *.png

.jpeg.png .jpg.png:
    @echo converting $< to $@ using ImageMagick...
    @convert $< $@
    @echo conversion to $@ successful!

有了这些最后的润色,如果你进入带有Makefile和.jpg或.jpeg文件的目录,你就可以在没有任何参数的情况下调用make来处理你的文件,把它们发送到你的服务器,然后删除你上传的.png文件。

make

正如你所看到的,可以把任务串起来,也可以在某一点上挑选一个进程。例如,如果你只想转换你的文件,并需要将它们托管在不同的服务器上,你可以只使用webify

结论

在这一点上,你应该对如何使用Makefiles有一个很好的概念。更具体地说,你应该知道如何使用make作为自动化大多数程序的工具。

虽然在某些情况下,写一个脚本可能效果更好,但Makefiles是一种在进程之间建立结构化、层次化关系的方法。学习如何利用这一工具可以帮助使重复性任务更易于管理。