Makefile是一种全能,不可思议的语言

本文翻译于The Language Agnostic, All-Purpose, Incredible, Makefile

我喜欢使用Makefiles。我喜欢在Java中使用Makefile。我喜欢在Erlang中使用Makefile。我喜欢在Elixir中使用Makefile。最近,我喜欢在Ruby中使用Makefile。我认为您也希望在您的环境中使用Makefile,如果我们中的大多数人通常使用Makefile,工程界将受益。

Make诞生于1976年,使其成为程序员工具包中最古老的工具之一。历时如此之久的任何工具都必定会有神话,故事和例子,它们会对不熟悉它的人产生困难。另外,我认为我们当中许多人已经将其注销不再相关,因为毕竟我们不是在编写C程序。请允许我向您展示为什么它不应该令人生畏,此外,它还适合您作为工程师的日常工作流程。

您可能会从Makefile中受益的指标

有许多指标表明Makefile对您有好处。这些指标很常见,我敢肯定有些指标适用于您。

  1. .bashrc中设置项目的别名或函数,例如

    alias chrome_rspec="CAPYBARA_JAVASCRIPT_DRIVER=chrome bundle exec bin/rspec"
    alias serve="bundle exec foreman start -f Procfile.dev
  2. 您在项目中使用了不同的命令集,尤其是那些使用许多不同工具,复杂参数集或环境变量的命令,例如

    RAILS_ENV=test bundle exec rails db:migrate 
    rake db:seed
    bundle exec jekyll serve --drafts --incremental
    bundle exec rspec spec/features/*.rb
    bundle exec rails c
  3. 您可以将笔记保存在Evernote,组织文件或文本文件中,以写下项目的常用命令。

  4. 您必须先运行某些命令。例如,当在Rails项目中切换分支时,您可能必须执行类似的操作

    bundle && RAILS_ENV=test bundle exec rails db:migrate \\
    && bundle exec rspec spec/features/

Makefile的好处

  1. 命令相对于项目来说是本地的(与.bashrc相反)
  2. 命令存储在项目中,因此改进是相互共享的。
  3. 命令受项目的版本控制。
  4. Makefile是项目域内有用命令的列表。您所要做的就是查看它,以了解您可以在项目中做什么。它是项目的入口点。Makefile为您提供了显式的依赖关系树。
  5. Makefile为您提供了显式的依赖关系树。
  6. 如果缺少命令,则可以添加它!您的团队将从中受益。

目前不使用Makefile的团队

如今,加入不使用Makefile的初创公司也就不足为奇了。在这种情况下,开发人员通常会使用他们所使用的软件环境随附的工具。如果使用了rails,则将使用bundle,rails和rspec命令。对于Makefile,您仍将使用这些命令,但Makefile将充当命令的包装。

即使他们不想要也可以自己使用

因为Makefile只是对常用工具的抽象,所以即使您的团队尚未看到其中的价值,使用Makefile也没有任何缺点。他们可以在使用Makefile时继续使用其工作流程。我建议您在开发了有用的Makefile之后将其提交到存储库中,然后,例如当有人问如何重新设置数据库种子时,您可以提及“只运行make db-seed”。

什么是Makefile?

有多年的用例和惯用做法可能使对Makefile的学习令人生畏。我想介绍一些基础知识,以便您可以立即开始使用Makefile。事实证明,当我们分解Makefile时,它们非常简单。

Makefile是通常位于项目根目录中的文件,名称为Makefile。

Makefile由规则(rules)组成。规则包含构成目标(target),先决条件(prerequisite)和命令(commands)。

target_1: prerequisite_1 ... prerequisite_n
    command_1
    ...
    command_n

Target

目标是您想要做的事情(运行测试,构建代码,删除数据库等)

Prerequisite

先决条件是在target可以运行之前需要运行的规则。例如,假设签出一个新的ruby项目并执行bundle exec rspec;您可能会遇到有关需要首先运行捆绑软件的错误!因此,运行包是运行rspec测试的先决条件。

如果您想“哦,但我不想每次都运行捆绑软件,请不要担心。Make可以确定是否需要运行先决条件。我们将解决这个问题。

Command

只需在shell中运行命令(默认情况下为sh)。如果您的目标是测试,则命令可能是bundle exec rspec。.

一个简单的例子

file_1: 
    touch file_1
  • The target is file_1.
  • There are no prerequisites.
  • The command touch file_1 produces file_1.

在Makefile中,它将与make file_1一起执行

一个简单的例子继续深入

添加另外一个人target:

file_2: file_1
    touch file_2
  • The target is file_2
  • The prerequisite is file_1
  • The command is touch file_2

在这一点上,我强烈建议您遵循自己的Makefile。您将边做边学。创建一个makefile,使其看起来像这样:

file_1:
    touch file_1

file_2: file_1
    touch file_2

并确保使用制表符缩进命令。这是使每个刚接触Make的人绊倒的要求。如果不使用制表符,则会收到类似于以下Makefile:5:***缺少分隔符的错误。停止。

现在,如果我们运行make file_2,它将首先检查目标file_1,如果该目标不存在,它将运行命令来实现file_1。

$ make file_2
touch file_1
touch file_2

如果随后运行make file_2,我们将看到类似make的消息:“ file_2”是最新的。Make告诉我们它不需要执行任何操作,因为file_2已经存在。

$ make file_2
make: `file_2' is up to date.

rm file_2,然后我们看到make file_2会碰到file_2但不会碰到file_1。

$ rm file_2
$ make file_2
touch file_2

您能预测此时如果我们创建file_2会发生什么吗?如果您认为它会再次更新,那么您就知道了!

$ make file_2
make: `file_2' is up to date.

好的,如果我们触摸file_1并随后创建file_2会发生什么?

$ make file_2
touch file_2

让我们探讨一下这里发生了什么。

好吧,这里快一点。当我们触摸文件时,会发生什么?如果文件不存在,它将创建该文件;但是如果该文件已经存在怎么办?

$ ls -ahl file_1
-rw-r--r--  1 ben.brodie  staff     0B Aug  9 06:48 file_1
$ touch file_1
$ ls -ahl file_1
-rw-r--r--  1 ben.brodie  staff     0B Aug  9 06:50 file_1

您发现差异了吗?它更新时间戳。让我们再来看一次。

$ ls -ahl
-rw-r--r--   1 ben.brodie  staff     0B Aug  9 06:50 file_1
-rw-r--r--   1 ben.brodie  staff     0B Aug  9 07:10 file_2
$ make file_2
make: `file_2' is up to date.
$ touch file_1
$ ls -ahl
-rw-r--r--   1 ben.brodie  staff     0B Aug  9 07:13 file_1
-rw-r--r--   1 ben.brodie  staff     0B Aug  9 07:10 file_2
$ make file_2
touch file_2
$ ls -ahl
-rw-r--r--   1 ben.brodie  staff     0B Aug  9 07:13 file_1
-rw-r--r--   1 ben.brodie  staff     0B Aug  9 07:13 file_2
$ make file_2
make: `file_2' is up to date.

你看到这里发生了什么吗?如果前提条件的时间戳比当前目标的时间戳更新,则将运行当前目标的命令。这是Make确定是否应运行目标命令的方式。

先决条件

我们已经研究了Make操作的一些机制和Makefile的语法,但是这些如何一起工作,以及所有这些的目的是什么?如果尚未点击,那就可以了;我保证,我们要到达那里。

暂时将Makefile目标视为树,其中每个节点代表一个目标,而一个分支代表目标与其先决条件的关系。

file_3 -- file_1
       \- file_2

在这里,我们有三个目标:file_1,file_2和file_3。file_3具有先决条件file_2和file_1,并且由于它们都是叶子,因此它们没有其他先决条件。

用makefile表示,看起来可能像这样:

file_1: 
    touch file_1

file_2: 
    touch file_2

file_3: file_1 file_2
    touch file_3

因此,我们认为Makefile中的目标代表目标树,其中目标是根,节点的子代是该节点的先决条件。如果不满足节点的先决条件(我们将确切地理解这是什么意思),make将遍历图,直到找到满足条件的节点为止。一旦找到了满足的先决条件,它将反转刚要达到满足的先决条件的路径,并且对于该路径中的每个节点,它将执行目标(目标本身可能具有自己的其他先决条件)。

让我们看一个稍微复杂一点的树。

file_6 -- file_5 -- file_4
                 \- file_3 -- file_2
                           \- file_1

对应于以下Makefile

file_1:
    touch file_1

file_2: 
    touch file_2

file_3: file_2 file_1
    touch file_3

file_4:
    touch file_4

file_5: file_4 file_3
    touch file_5

file_6: file_5
    touch file_6

假设您的ls -ahl的输出如下:

-rw-r--r--   1 ben.brodie  staff     0B Aug  9 10:52 file_1
-rw-r--r--   1 ben.brodie  staff     0B Aug  9 10:52 file_2
-rw-r--r--   1 ben.brodie  staff     0B Aug  9 10:52 file_3

继续运行:

make file_6

现在make将遍历树,将每个孩子递归推入堆栈。然后,它将每个节点弹出堆栈,并检查是否满足要求。如果不满意,它将为该节点运行命令。

stack.push(file_6) -> |file_6|
stack.push(file_5) -> |file_5, file_6|
stack.push(file_4) -> |file_4, file_5, file_6|
stack.push(file_3) -> |file_3, file_4, file_5, file_6|
stack.push(file_2) -> |file_2, file_3, file_4, file_5, file_6|
stack.push(file_1) -> |file_1, file_2, file_3, file_4, file_5, file_6|

stack.pop() -> file_1 <- |file_2, file_3, file_4, file_5, file_6|

file_1 是否满足? 它是一个叶子节点并且存在, 所以满足.

stack.pop() -> file_2 <- |file_3, file_4, file_5, file_6|

file_2 是否满足? 它是一个叶子节点并且存在, 所以满足.

stack.pop() -> file_3 <- |file_4, file_5, file_6|

file_3 是否满足?? file_3 存在. 它是 file_2 and file_3孩子节点, 并且它们的时间戳早于file_3的时间戳, 所以也满足.

stack.pop() -> file_4 <- |file_5, file_6|

file_4是否满足?它不存在,所以不不满足。运行命令。

touch file_4

stack.pop() -> file_5 <- |file_6|

file_5是否满足?它不存在,所以不不满足。运行命令。

touch file_5

stack.pop -> file_6 <- | |

file_6是否满足?它不存在,所以不不满足。运行命令。

touch file_6

我们还没有提出满足的正式定义,但是现在我们可以了。我们知道,要满足目标,它必须(1)存在,并且(2)目标的时间戳必须比其先决条件的时间戳新。从该练习中我们还知道遍历了整个树,如果不满足树中的任何先决条件,那么该先决条件的目标也将不满足。因此,(3)必须满足前提条件。

满足先决条件的定义

本节仅是我们刚刚得出的结论的摘要。满足先决条件的特点。

  1. 目标存在.
  2. 目标的时间戳必须比目标先决条件的时间戳新
  3. 必须满足前提条件的目标。

Make是关于文件的

到目前为止,我们一直在谈论文件。第一个属性要求“目标存在”-目标的存在由存在的同名文件确定。时间戳要求(时间戳必须比先决条件的时间戳更新)也仅在目标名称与文件一一对应时才有意义。

在这一点上,人们意识到了一切的本质,并且没有任何进一步的建议,很容易错误地认为这可能与您的一系列问题或语言工具无关,但这是天真的误解-看法使判断蒙上阴影-我们将尽快获得优质产品。

Make是关于文件的。文件是make运作方式的原语,因为文件使我们能够跟踪状态。如果满足目标的先决条件,make将不会运行先决条件。如果满足该行中的某些先决条件,make将仅运行不满足的先决条件。

这全部归结为该时间戳属性-目标的时间戳必须比目标先决条件的时间戳新。请记住,第三个属性使其具有递归性,因此它一直应用于先决条件树。

让我们考虑一下timestamp属性的含义。如果先决条件的时间戳比目标的时间戳新,则意味着先决条件是在生成目标的时间之后的某个时间生成的。存在先决条件关系是因为有关目标的某些内容取决于有关先决条件的某些内容,因此,如果先决条件是较新的,则必须重新生成目标,否则,当前存在的目标已过时。

拿时间打个比方。我正在预订一次波兰旅行,然后从拉脱维亚里加乘飞机飞往挪威特罗姆瑟。我必须计划从特罗姆瑟到洛杉矶的返程航班,以及我预定的日期,具体取决于我从里加飞往挪威的日期。每次更新从里加飞往特罗姆瑟的日期时,我都必须更新要从特罗姆瑟飞往洛杉矶的时间,否则我可能在特罗姆瑟呆了半天,甚至遇到了不可能的事情,像是从特罗姆瑟(Tromsø)飞回洛杉矶,然后降落在特罗姆瑟(Tromsø)。

让我们将其公式化为Makefile,在其中我们需要协调四个行程。

1.从洛杉矶飞往波兰克拉科夫的航班
2.从波兰克拉科夫前往里加
3.从里加飞往特罗姆瑟
4.从Tromsø飞往洛杉矶

 schedule_flight_to_krakow: 
     echo $(krakow_date) > schedule_flight_to_krakow

 schedule_drive_to_riga: schedule_flight_to_krakow
     echo $(riga_date) > schedule_drive_to_riga

 schedule_flight_to_tromso: schedule_drive_to_riga
     echo $(tromso_date) > schedule_flight_to_tromso

 schedule_flight_to_los_angeles: schedule_flight_to_tromso
     echo $(los_angeles_date) > schedule_flight_to_los_angeles

在这里,我介绍了make文件中的新语法-参数。用$(…)包围的单词将由命令行中为该参数指定的字符串替换。例如,在以下两个make调用中,$(arg_1)将被file_1替换:arg_1 = file_1 make file或make file arg_1 = file_1。简单吧?是的。

因此,要安排飞往克拉科夫的航班,我们将make称为make schedule_flight_to_krakow krakow_date = 08/01/2019

让我们看看它产生了什么。

$ cat schedule_flight_to_krakow
10/01/2019

简单地传递所有日期并注意创建所有文件会更容易。默认情况下,make将运行文件中的第一个目标,并且习惯上我们将全部称为该目标。

all: schedule_flight_to_los_angeles

现在我们可以在有或没有全部的情况下调用它

$ make krakow_date=10/01/2019 \
       riga_date=10/15/2019 \
       tromso_date=10/20/2019 \
       los_angeles_date=10/23/2019
  echo 10/01/2019 > schedule_flight_to_krakow
  echo 10/15/2019 > schedule_drive_to_riga
  echo 10/20/2019 > schedule_flight_to_tromso
  echo 10/23/2019 > schedule_flight_to_los_angeles

错误处理

让我们添加一些错误处理,以便我们在运行make时知道需要哪个排期。我们可以使用sh本身来处理错误,在这种情况下,缺少参数。

all: schedule_flight_to_los_angeles

schedule_flight_to_krakow:
    @if [ -z "$(krakow_date)" ]; then \
        echo "You must set krakow_date"; exit 1; fi
    echo $(krakow_date) > schedule_flight_to_krakow

schedule_drive_to_riga: schedule_flight_to_krakow
    @if [ -z "$(riga_date)" ]; then \
        echo "You must set riga_date"; exit 1; fi
    echo $(riga_date) > schedule_drive_to_riga

schedule_flight_to_tromso: schedule_drive_to_riga
    @if [ -z "$(tromso_date)" ]; then \
        echo "You must set tromso_date"; exit 1; fi
    echo $(tromso_date) > schedule_flight_to_tromso

schedule_flight_to_los_angeles: schedule_flight_to_tromso
    @if [ -z "$(los_angeles_date)" ]; then \
        echo "You must set los_angeles_date"; exit 1; fi
    echo $(los_angeles_date) > schedule_flight_to_los_angeles

@只是简单地指定make不会回显该命令,对于错误检查,该命令只是噪音。

现在,如果我们从一个干净的状态开始运行,make将通知我们所有必要的必要参数,并且这些参数将失败。这里我们省略了riga_date

$ make krakow_date=10/01/2019 \
       tromso_date=10/20/2019 \
       los_angeles_date=10/23/2019
  echo 10/01/2019 > schedule_flight_to_krakow
  You must set riga_date
  make: *** [schedule_drive_to_riga] Error 1

设置 riga_date.

$ make riga_date=10/15/2019 \
        tromso_date=10/20/2019 \
        los_angeles_date=10/23/2019
  echo 10/15/2019 > schedule_drive_to_riga
  echo 10/20/2019 > schedule_flight_to_tromso
  echo 10/23/2019 > schedule_flight_to_los_angeles

假设我们现在要重新安排飞往里加,特罗姆瑟和洛杉矶的航班,因为我们已经安排了飞往克拉科夫的航班,所以我们将无法安排航班。

$ make riga_date=10/16/2019 \
       tromso_date=10/21/2019 \
       los_angeles_date=10/24/2019
  make: Nothing to be done for `all'.

先决条件

make won’t allow us to reschedule once we have scheduled something.

$ make schedule_drive_to_riga riga_date=10/16/2019 
make: `schedule_drive_to_riga' is up to date.

但是,目前,我们可以手动rm文件。

$ rm schedule_drive_to_riga

然后安排它

$ make schedule_drive_to_riga riga_date=10/16/2019 

让我们尝试以相同的方式重新安排飞往洛杉矶的航班。

$ rm schedule_flight_to_los_angeles
$ make schedule_flight_to_los_angeles los_angeles_date=10/24/2019
  You must set tromso_date
  make: *** [schedule_flight_to_tromso] Error 1

Make检测到我们的schedule_flight_to_tromso前提条件不满足,因为schedule_flight_to_riga的时间戳比schedule_flight_to_tromso的时间戳新,因此也必须对其进行更新!所以,让我们这样做。

$ make schedule_flight_to_los_angeles tromso_date=10/22/2019 los_angeles_date=10/24/2019
  echo 10/22/2019 > schedule_flight_to_tromso
  echo 10/24/2019 > schedule_flight_to_los_angeles

Make与文件无关

到目前为止,我们已经看到Makefile的目标真正代表了文件。但是,这似乎是有限的。例如,如果我们要重新安排目标该怎么办?

以前,我们必须手动rm文件来重新计划它。重新计划目标应使我们能够运行make makechedched_flight_to_los_angeles los_angeles_date = 10/24/2019之类的东西。

事实证明,我们可以做到这一点。如果考虑重新安排的意义,似乎没有任何情况可以阻止重新安排(即我们正在积极地重新安排时间,因此我们打算在已经设置日期的情况下进行安排)。

事情很简单,就像这样:

reschedule_flight_to_los_angeles: schedule_flight_to_los_angeles
    @if [ -z "$(los_angeles_date)" ]; then \
        echo "You must set los_angeles_date"; exit 1; fi
        echo $(los_angeles_date) > schedule_flight_to_los_angeles

这看起来很熟悉,因为它与schedule_flight_to_los_angeles相同。但是,它的行为有所不同。我们可以根据需要运行它多次,并且不会抱怨。

$ make reschedule_flight_to_los_angeles los_angeles_date=10/25/2019
  echo 10/25/2019 > schedule_flight_to_los_angeles
$ test$ make reschedule_flight_to_los_angeles los_angeles_date=10/26/2019
  echo 10/26/2019 > schedule_flight_to_los_angeles
$ test$ make reschedule_flight_to_los_angeles los_angeles_date=10/27/2019
  echo 10/27/2019 > schedule_flight_to_los_angeles

本质上的区别在于,生成的文件与目标名称不同。

由于未创建与目标名称相同的文件,因此从Make中可以从未将该目标视为最新目标。

之前我们实现了all目标-该目标也独立于对应的文件。

但是,如果其他过程确实导致文件名与目标冲突怎么办?最好添加一个干净的任务来重新开始我们的日程安排,就像我们从未安排过任何事情一样

clean:
    rm schedule*

这将删除名称中以“ schedule”开头的所有文件,实际上忘记了我们曾经安排过任何事情。但是,并非无法想象的是,其他一些过程可能会导致一个名为clean的文件,从而导致令人困惑的错误。

$ touch clean
$ make clean
  make: `clean' is up to date.

Make有一个名为.PHONY的特殊目标,并且该目标的所有先决条件总是被确定为过期,并且将始终运行。

.PHONY: clean

现在,当我们运行干净时,它不会检查文件是否存在。

$ touch clean
$ make clean
  rm schedule*
  rm: schedule*: No such file or directory
  make: *** [clean] Error 1

但是请注意,这将导致错误,因为我们已经清除了“计划”文件。理想情况下,这不应导致错误;我们希望在删除文件后或文件已经移动时成功,并且,如果我们可以防止导致错误,则干净目标具有幂等的有用属性。

我们可以在配方的前面加上-来告知make忽略错误。

clean:
    -rm schedule*

现在,当我们运行clean时,该错误将被忽略。

$ make clean
rm schedule*
rm: schedule*: No such file or directory
make: [clean] Error 1 (ignored)

被忽略的错误仍会报告该错误,但不会停止制作过程。

我们的最终文件(尽管高度伪造)如下所示:

.PHONY: all clean reschedule_flight_to_los_angeles

all: schedule_flight_to_los_angeles

schedule_flight_to_krakow:
    @if [ -z "$(krakow_date)" ]; then \
        echo "You must set krakow_date"; exit 1; fi
        echo $(krakow_date) > schedule_flight_to_krakow

schedule_drive_to_riga: schedule_flight_to_krakow
    @if [ -z "$(riga_date)" ]; then \
        echo "You must set riga_date"; exit 1; fi
        echo $(riga_date) > schedule_drive_to_riga

schedule_flight_to_tromso: schedule_drive_to_riga
    @if [ -z "$(tromso_date)" ]; then \
        echo "You must set tromso_date"; exit 1; fi
        echo $(tromso_date) > schedule_flight_to_tromso

schedule_flight_to_los_angeles: schedule_flight_to_tromso
    @if [ -z "$(los_angeles_date)" ]; then \
        echo "You must set los_angeles_date"; exit 1; fi
        echo $(los_angeles_date) > schedule_flight_to_los_angeles

reschedule_flight_to_los_angeles: schedule_flight_to_los_angeles
    @if [ -z "$(los_angeles_date)" ]; then \
        echo "You must set los_angeles_date"; exit 1; fi
        echo $(los_angeles_date) > schedule_flight_to_los_angeles

clean:
    -rm schedule*

使它适合Ruby(或任何其他语言)

我已经给出了Make的概述,以及一个虚构的示例。现在,我想说明一下如何将其用作开发工作流程中的实用工具。

我认为许多开发人员认为,他们的语言附带的构建工具就是他们所需要的-只需运行rspec测试,rails服务器或npm install即可完成某些任务。

但是,其中许多工具没有利用依赖关系,而且开箱即用的东西常常不知道那些依赖关系是什么。例如,我在一个Rails项目中工作,如果前端层有任何更改,则必须由Webpacker编译Javascript。我们的测试套件中的集成测试使用了Web前端,并且如果在引入更改时未运行Webpacker,则测试可能会失败,从而导致关于代码状态的错误结论,并浪费大量时间直到开发人员意识到“哦,我忘记了运行Webpacker!”。

此外,如果您是在计算机上实时运行服务器,则有一个Webpacker进程监视实时代码更改,并在发生更改时重新编译Javascript-因此,在这种情况下,如果开发人员运行了集成测试,则测试将准确反映代码的当前状态。

测试套件的成功不应依赖于另一个进程正在运行并监视代码的假设。首先,如果该进程未运行,则测试将无法通过。其次,这意味着该测试套件的先决条件未在test命令中捕获,这可能导致CI服务器出现问题,代码干净清除或建立新的开发人员。成功的测试套件永远不要依赖外部性。这导致不一致的行为。

我并不建议让流程监视代码并重新编译是错误的,这对于拥有一个自我更新的实时站点进行实验的目标来说是完全可以的-但这与运行测试套件只是一个不同的用例,因此不要混在一起。

一个人也可以简单地拥有一个脚本,该脚本可以在运行测试之前运行所有先决条件,但是该脚本没有一种系统来跟踪需要运行的内容和不需要运行的内容。如果Javascript已被编译,则每次运行测试时都将浪费时间等待其编译。Make确定是否由于更改而需要编译Javascript。

一个人也可以简单地拥有一个脚本,该脚本可以在运行测试之前运行所有先决条件,但是该脚本没有一种系统来跟踪需要运行的内容和不需要运行的内容。如果Javascript已被编译,则每次运行测试时都将浪费时间等待其编译。Make确定是否由于更改而需要编译Javascript。

Rake尽管命名为Ruby Make,但它既不能代替make,也不会与make冲突。相反,我将Rake看作是针对Ruby和Framework特定任务(例如数据库迁移)的有用工具,而Make则可以用作连接所有这些命令和依赖项的通用粘合剂。与在Rake中相比,在Make中编写命令并声明其依赖关系要简单得多,Makefile实际上可以通过在配方中调用rake some_task来利用已经存在的Rake任务。

此外,Make与语言无关,因此非常适合任何项目。开发人员可以简单地查看给定项目中的Makefile,即使他们不熟悉该语言或框架,他们也拥有执行该项目的命令列表。

我想重申,Make不会替换给定的构建工具,它只是将那些工具包起来,以使更高级的事情变得更加简单和一致。

一个实际的例子

即使是一个简单的项目,也可以从Makefile中受益匪浅。几个月前,我被要求组织一个小项目进行面试。这个项目涉及后端服务器和React中的前端组件。运行各种命令和模式时,我将它们添加到Makefile中。因为现代的构建工具有时具有未捕获为文件的副作用,例如用于设置数据库的bundle exec rake db:setup,所以我为Makefile设计了一种模式来捕获这些任务已经发生,因此它们不会如果不需要,请运行。

.PHONY: serve live-reload db-reset db-setup db-migrate init deps compile bundle yarn clean

serve: init deps compile db-setup db-migrate
    rails server

live-reload: yarn
    ./bin/webpack-dev-server --host 127.0.0.1

db-reset:
    bundle exec rake db:reset

db-setup: .make.db-setup

db-migrate: .make.db-migrate

init: .make.init

deps: bundle yarn

compile: .make.webpacker

bundle: .make.bundle

yarn: .make.yarn

clean:
    rm .make.*

.make.webpacker: $(shell find app/javascript -type f)
    ./bin/webpack
    touch .make.webpacker

.make.db-setup: .make.bundle
    bundle exec rake db:setup
    touch .make.db-setup

.make.db-migrate: .make.bundle $(shell find db/migrate -type f)
    bundle exec rails db:migrate
    touch .make.db-migrate

.make.bundle: Gemfile
    bundle
    touch .make.bundle

.make.yarn: package.json
    yarn
    touch .make.yarn

.make.init:
    gem install bundler
    touch .make.init

花几分钟看一下并理解它。此处是我们的主要入口点,默认情况下将在调用不带参数的make时运行。

init

让我们仔细研究服务目标的先决条件。首先,有一个初始化先决条件。此先决条件确保已安装捆绑程序(例如,也可用于通过自制软件安装要求)。init有一个先决条件.make.init,它首先运行gem install bundler,然后创建文件.make.init。因为.make.init是它创建的文件的名称,所以它将永远不会再次运行(除非删除该文件)。这是一种将副作用捕获为文件以指示任务已运行的简单方法。

deps

deps确保所有依赖项都已安装到项目中,并且如果指定了任何新版本,或者已从项目中添加或删除了依赖项,则将更新依赖项。deps有两个先决条件-bundleyarn.。

bundle

当且仅当Gemfile文件已更新时,bundle才会运行bundle来更新项目的Ruby依赖项。我们在.make.bundle目标中看到了这一点,该目标具有Gemfile的先决条件,而Gemfile就是该文件。捆绑包成功运行后,将创建文件.make.bundle。只要Gemfile的时间戳比.make.bundle的时间戳新,这恰好是在添加,删除和更改依赖项版本的情况下发生的,任务将再次运行!否则,它什么也不会做。

yarn

yarn与bundle目标具有相同的作用,不同之处在于此任务取决于package.json文件,并运行yarn命令。

compile

compile运行webpack来编译Javascript。重要的是,只有在更新了任何Javascript文件后,才执行此操作。通过设置所有Javascript文件的先决条件来完成此操作!如果其中任何一个的时间戳都比.make.webpacker文件新,则它将运行webpack。此目标还向我们介绍了makefile中的两个新概念:shell函数和动态生成的先决条件。如您所见,动态生成先决条件的功能非常强大。shell函数仅在shell中运行命令,然后返回函数返回的内容,在这种情况下,返回的是app / javascript目录中所有文件的列表。

db-setup

db-setup如果尚未运行,则运行包执行rake db:setup。如果未首先安装捆绑软件,它将进行安装。

db-migrate

在rails项目中,db-migrate是一个非常漂亮的目标。在我弄清楚这一点之前,通常我会切换到另一个分支或提取新代码,然后我的数据库将与迁移不同步。该目标自动解决了此问题。您再也不会看到有关需要运行迁移的消息!与编译一样,db-migrate使用shell函数动态生成先决条件列表,这些先决条件是项目中的所有迁移。如果它们中的任何一个都比.make.db-migrate更新,它将运行包执行轨道db:migrate。如果尚未安装捆绑软件,它将首先安装它。

serve

完成所有这些操作后,就该为应用程序提供服务了-Rails服务器可以解决问题。

Adding a new target

这里缺少一些东西……没有测试目标!但是,请注意添加测试目标将非常简单。我们已经定义了所有必需的目标先决条件。它们与服务目标相同。

test: init deps compile db-setup db-migrate
    rspec test

开发Makefile和开发代码是很自然的事情,一旦奠定了坚实的基础,定义新目标通常就这么简单。

gitignore

由于这会生成.make。文件,因此请务必回显.make。> .gitignore,以避免将它们检入git。

解耦的自由

因为Make是与语言和框架分离的通用工具,所以一旦Make成为工作流的一部分,它将变得难以置信的释放。

例如,我目前正在一个项目上,该项目需要加载数据库才能运行服务器。我意识到,如果我只是简单地使用docker来启动数据库,而不是依赖实际上在我的机器上作为服务运行的数据库,那么它与我自己的系统的耦合就会更少。

通常,在项目中使用docker可能会引起问题,因为没有可用的现成工具来管理docker实例。有时Docker正在运行,但一切正常,有时却未运行,要求开发人员记住那些令人困惑的docker命令之一(它是创建,运行,执行还是启动?)。如果Docker映像上的数据库被破坏了怎么办?现在,您需要记住另一组命令来重置数据库。

Makefile允许开发人员使用docker创建工作流,该工作流不会混淆,并且保持一致且可预测。

使用Makefile允许我在运行服务器时(或在需要进行测试时)将mysql作为依赖项启动。我在下面修改了它作为示例。

...
run: build mysql-start
    ./start-myapp

mysql-create: 
    @docker container ls -a | grep my_app_mysql || \
      (docker create -p 3306:3306 \
        --name my_app_mysql \
        -v $$(pwd)/mysql/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d \
        -v $$(pwd)/mysql/mysql-keyring:/var/lib/mysql-keyring \
        -e MYSQL_ROOT_PASSWORD=password \
        mysql:8.0.18 \
        --early-plugin-load=keyring_file.so \
        --lower-case-table-names=1

mysql-start: mysql-create
    docker start my_app_mysql
    while ! docker exec my_app_mysql -h 127.0.0.1 -ppassword -e \
    "\q" 2> /dev/null; do echo "Waiting for MySQL..." && sleep 1; done

mysql-stop:
    docker stop my_app_mysql

mysql-clean:
    docker rm my_app_mysql

mysql-shell:
    docker exec -it my_app_mysql -h 127.0.0.1 -ppassword

mysql-logs
    docker logs my_app_mysql
...

如您所见,一旦我们有一些要求,我们的配方就会变得非常复杂。在这里,我们有一个启用了加密的架构,因此它需要一些配置。手动运行这些命令将无济于事。mysql-create仅在不存在的情况下创建容器。我正在使用||为此。此外,在mysql-start中,docker start是幂等的,因此,如果启动容器,它将正常工作。while循环检查进程是否可以连接到mysql,如果无法连接,则等待一秒钟并尝试再次检查。这是必要的,因为mysql数据库进程并不总是立即可用,因此我们需要等待直到它连接为止。

如果没有适当的Makefile,我们将根本没有自由以一致的方式将docker纳入我们的开发工作流程。充其量,我们会有一些脚本来停止和启动容器,但是这些脚本必须手动运行,这不可避免地导致在没有数据库启动的情况下意外运行服务器,以及入职开发人员可能不知道的神秘要求。

如何开发一个Makefile

他们成功编写Makefile的关键是只写下当前需要的命令。跑了新命令?将其放在Makefile中。随着时间的流逝,Makefile将会增长,甚至包括那些您很少运行并且不记得它们是什么的罕见命令。不断出现的问题,这些将有助于指导前提条件,否则这些前提条件可能不会很明显。

重要的是不要尝试预先定义所有命令。更新和完善Makefile是开发工作流程本身的一部分。事情会改变,Makefile也会随着时间而改变。由于Makefile具有通用性和灵活性,因此这些更改通常很容易实现。另外,由于开发人员将一直在运行makefile,因此与文档不同,它们保持最新状态。如果不起作用,则需要立即修复。

PHP7 opcache缓存清理问题
shell命令大全