Makefile简易教程

本文部分内容引用:

Makefile简介

在软件开发中,make通常被视为一种软件构建工具。该工具主要经由读取一种名为“makefile”或“Makefile”的文件来实现软件的自动化建构。它会通过一种被称之为“target”概念来检查相关文件之间的依赖关系,这种依赖关系的检查系统非常简单,主要通过对比文件的修改时间来实现。在大多数情况下,我们主要用它来编译源代码,生成结果代码,然后把结果代码连接起来生成可执行文件或者库文件。

优点与缺点

与大多数古老的Unix工具一样,make也分别有着人数众多的拥护者和反对者。它在适应现代大型软件项目方面有着许许多多的问题。但是,依然有很多人坚定地认为(包括我)它能应付绝大多数常见的情况,而且使用非常的简单,功能强大,表达清楚。无论如何,make如今仍然被用来编译很多完整的操作系统,而且它的那些“更为现代”的替代品们在基本操作上与它没有太大差别。

当然,随着现代的集成开发环境(IDE)的诞生,特别是非Unix的平台上,很多程序员不再手动管理依靠关系检查,甚至不用去管哪些文件是这个项目的一部分,而是把这些任务交给了他们的开发环境去做。类似的,很多现代的编程语言有自己专属的、能高效配置依赖关系的方法(譬如Ant)。

主要版本

make程序经历过各方多次的改写与重写,各方都依据自己的需要做了一些特定的改良。目前市面上主要流行有以下几种版本:

  • GNU make:
    GNU make对make的标准功能(通过clean-room工程)进行了重新改写,并加入作者自认为值得加入的新功能,常和GNU编译系统一起被使用,是大多数GNU Linux默认安装的工具。

  • BSD make:
    该版本是从Adam de Boor制作的版本上发展起来的。它在编译目标的时有并发计算的能力。主要应用于FreeBSD,NetBSD和OpenBSD这些系统。

  • Microsoft nmake:
    该版本主要用于微软的Windows系统中,需要注意的是,微软的nmake与Unix项目中的nmake是两种不同的东西,千万不要混淆。

从一个简单的例子开始

我们可以用K&R C中4.5那个例子来做个说明。在这个例子中,我们会看到一份主程序代码(main.c)、三份函数代码(getop.c、stack.c、getch.c)以及一个头文件(calc.h)。通常情况下,我们需要这样编译它:

    gcc -o calc main.c getch.c getop.c stack.c 

如果没有makefile,在开发+调试程序的过程中,我们就需要不断地重复输入上面这条编译命令,要不就是通过终端的历史功能不停地按上下键来寻找最近执行过的命令。这样做两个缺陷:

  1. 一旦终端历史记录被丢失,我们就不得不从头开始;

  2. 任何时候只要我们修改了其中一个文件,上述编译命令就会重新编译所有的文件,当文件足够多时这样的编译会非常耗时。

那么Makefile又能做什么呢?我们先来看一个最简单的makefile文件:

    calc: main.c getch.c getop.c stack.c
        gcc -o calc main.c getch.c getop.c stack.c 

现在你看到的就是一个最基本的Makefile语句,它主要分成了三个部分,第一行冒号之前的calc,我们称之为目标(target),被认为是这条语句所要处理的对象,具体到这里就是我们所要编译的这个程序calc。冒号后面的部分(main.c getch.c getop.c stack.c),我们称之为依赖关系表,也就是编译calc所需要的文件,这些文件只要有一个发生了变化,就会触发该语句的第三部分,我们称其为命令部分,相信你也看得出这就是一条编译命令。现在我们只要将上面这两行语句写入一个名为Makefile或者makefile的文件,然后在终端中输入make命令,就会看到它按照我们的设定去编译程序了。

请注意,在第二行的“gcc”命令之前必须要有一个tab缩进。语法规定Makefile中的任何命令之前都必须要有一个tab缩进,否则make就会报错。

接下来,让我们来解决一下效率方面的问题,先初步修改一下上面的代码:

    cc = gcc
    prom = calc
    src = main.c getch.c getop.c stack.c
     
    $(prom): $(src)
        $(cc) -o $(prom) $(src)

如你所见,我们在上述代码中定义了三个常量cc、prom以及src。它们分别告诉了make我们要使用的编译器、要编译的目标以及源文件。这样一来,今后我们要修改这三者中的任何一项,只需要修改常量的定义即可,而不用再去管后面的代码部分了。

请注意,很多教程将这里的cc、prom和src称之为变量,个人认为这是不妥当的,因为它们在整个文件的执行过程中并不是可更改的,作用也仅仅是字符串替换而已,非常类似于C语言中的宏定义。或者说,事实上它就是一个宏。

但我们现在依然还是没能解决当我们只修改一个文件时就要全部重新编译的问题。而且如果我们修改的是calc.h文件,make就无法察觉到变化了(所以有必要为头文件专门设置一个常量,并将其加入到依赖关系表中)。下面,我们来想一想如何解决这个问题。考虑到在标准的编译过程中,源文件往往是先被编译成目标文件,然后再由目标文件连接成可执行文件的。我们可以利用这一点来调整一下这些文件之间的依赖关系:

    cc = gcc
    prom = calc
    deps = calc.h
    obj = main.o getch.o getop.o stack.o
     
    $(prom): $(obj)
        $(cc) -o $(prom) $(obj)

    main.o: main.c $(deps)
        $(cc) -c main.c

    getch.o: getch.c $(deps)
        $(cc) -c getch.c

    getop.o: getop.c $(deps)
        $(cc) -c getop.c

    stack.o: stack.c $(deps)
        $(cc) -c stack.c                

这样一来,上面的问题显然是解决了,但同时我们又让代码变得非常啰嗦,啰嗦往往伴随着低效率,是不祥之兆。经过再度观察,我们发现所有.c都会被编译成相同名称的.o文件。我们可以根据该特点再对其做进一步的简化:

    cc = gcc
    prom = calc
    deps = calc.h
    obj = main.o getch.o getop.o stack.o
    
    $(prom): $(obj)
        $(cc) -o $(prom) $(obj)

    %.o: %.c $(deps)
        $(cc) -c $< -o $@

在这里,我们用到了几个特殊的宏。首先是%.o:%.c,这是一个模式规则,表示所有的.o目标都依赖于与它同名的.c文件(当然还有deps中列出的头文件)。再来就是命令部分的<和@,其中<代表的是依赖关系表中的第一项(如果我们想引用的是整个关系表,那么就应该使用^),具体到我们这里就是%.c。而$@代表的是当前语句的目标,即%.o。这样一来,make命令就会自动将所有的.c源文件编译成同名的.o文件。不用我们一项一项去指定了。整个代码自然简洁了许多。

到目前为止,我们已经有了一个不错的makefile,至少用来维护这个小型工程是没有什么问题了。当然,如果要进一步增加上面这个项目的可扩展性,我们就会需要用到一些Makefile中的伪目标和函数规则了。例如,如果我们想增加自动清理编译结果的功能就可以为其定义一个带伪目标的规则;

    cc = gcc
    prom = calc
    deps = calc.h
    obj = main.o getch.o getop.o stack.o
    
    $(prom): $(obj)
        $(cc) -o $(prom) $(obj)

    %.o: %.c $(deps)
        $(cc) -c $< -o $@

    clean:
        rm -rf $(obj) $(prom)

有了上面最后两行代码,当我们在终端中执行make clean命令时,它就会去删除该工程生成的所有编译文件。

另外,如果我们需要往工程中添加一个.c或.h,可能同时就要再手动为obj常量再添加第一个.o文件,如果这列表很长,代码会非常难看,为此,我们需要用到Makefile中的函数,这里我们演示两个:

    cc = gcc
    prom = calc
    deps = $(shell find ./ -name "*.h")
    src = $(shell find ./ -name "*.c")
    obj = $(src:%.c=%.o) 
    
    $(prom): $(obj)
        $(cc) -o $(prom) $(obj)

    %.o: %.c $(deps)
        $(cc) -c $< -o $@

    clean:
        rm -rf $(obj) $(prom)

其中,shell函数主要用于执行shell命令,具体到这里就是找出当前目录下所有的.c和.h文件。而$(src:%.c=%.o)则是一个字符替换函数,它会将src所有的.c字串替换成.o,实际上就等于列出了所有.c文件要编译的结果。有了这两个设定,无论我们今后在该工程加入多少.c和.h文件,Makefile都能自动将其纳入到工程中来。

到这里,我们就基本上将日常会用到的Makefile写法介绍了一遍。如果你想了解更多关于makefile和make的知识,请参考GNU Make Manual

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容