一个简单的通用Makefile实现

一个简单的通用Makefile实现

 

MakefileLinux下程序开发的自动化编译工具,一个好的Makefile应该准确的识别编译目标与源文件的依赖关系,并且有着高效的编译效率,即每次重新make时只需要处理那些修改过的文件即可。Makefile拥有很多复杂的功能,这里不可能也没必要一一介绍,为了简化问题的复杂性,本文仅和大家讨论针对单目录下的C/C++项目开发,如何写一个通用的Makefile

首先,我们假设当前工程目录为prj/,该目录下有6文件,分别是:main.cabc.cxyz.cabc.hxyz.hMakefile。其中main.c包含头文件abc.hxyz.habc.c包含头文件abc.hxyz.c包含头文件xyz.h,而abc.h又包含了xyz.h。它们的依赖关系如图1

1 文件依赖关系

第一次使用Makefile应该写成这个样子(假设生成目标main):

main:main.o abc.o xyz.o

    gcc main.o abc.o xyz.o -o main

main.o:main.c abc.h xyz.h

    gcc -c main.c –o main.o -g

abc.o:abc.c abc.h xyz.h

    gcc -c abc.c –o abc.o -g

xyz.o:xyz.c xyz.h

    gcc -c xyz.c -o xyz.o -g

clean:

    rm main main.o abc.o xyz.o -f

虽然这样Makefile完全符合Makefile的书写规则,但是当代码文件增加几倍后,再管理这些命令将会是一个噩梦!!!因此Makefile提供了认规则和自动推导帮我们完成一些常用功能。然后,我们将Makefile修改如下:

EXE=main

CC=gcc

OBJ=main.o abc.o xyz.o

CFLAGS=-g

$(EXE):$(OBJ)

    $(CC) $^ -o $@

clean:

    rm $(EXE) $(OBJ) -f

变量EXECCOBJ分别代指目标程序名,编译器名,目标文件名。CFLAGSMakefile的预定义变量,它会附加在每条编译命令(gcc -c)之后。

$(EXE)是对变量的引用,$^代指所有的依赖项——即$(OBJ)$@代指目标项——即$(EXE)。该命令等价于:$(CC) $(OBJ) -o $(EXE)

这个Makefile只有目标文件链接的命令,源文件的编译命令都被忽略了!这正是Makefile自动推导功能——它可以将目标文件自动依赖于同名的源文件,即:

main.o:main.c

    gcc -c main.c -o main.o

abc.o:abc.c

    gcc -c abc.c -o abc.o

xyz.o:xyz.c

    gcc -c xyz.c -o xyz.o

按照上述方式,只要工程下增加了源文件后,只需要在OBJ初始化处增加一个*.o即可。但是这种方式是有问题的,Makefile自动推导功能只会推导出目标文件对源文件的依赖关系,而不会增加文件的依赖关系!!!这导致的直接问题就是修改项目的头文件,不会导致make自动更新!除非修改文件后运行一次make clean,再运行make…… :-)

为了能让make自动包含头文件的依赖关系,我们需要做一点额外的工作。幸运的是gcc为我们提供了一个编译选项(gcc -M,对于g++-MM),能输出目标文件的依赖关系!比如:

$gcc -M main.c

main.o:main.c abc.h xyz.h

如果将每个源文件的依赖关系包含到Makefile里,就可以使得目标文件自动依赖于头文件了!再次修改原先的Makefile

EXE=main

CC=gcc

SRC=$(wildcard *.c)

OBJ=$(SRC:.c=.o)

CFLAGS=-g

all:depend $(EXE)

depend:

@$(CC) -MM $(SRC) > .depend

-include .depend

$(EXE):$(OBJ)

$(CC) $(OBJ) -o $(EXE)

clean:

@rm $(EXE) $(OBJ) .depend -f

我们虚设了一个目标all,它依赖于depend和实际的目标EXE。而depend正式将所有的源文件对应的目标文件的依赖关系输入到.depend文件,并包含在Makefile内!这里有几个细节需要说明:

1.depend文件是隐藏文件,避免和工程的文件混淆。

2include命令之前增加符号‘-’,避免第一次make时由于.depend文件不存在报告错误信息。

3SRC初始化为wildcard *.c表示当前目录下的所有.c文件,这就省去了我们手动输入新增的源文件

4OBJ初始化为SRC:.c=.o,表示将SRC中所有.c结尾的文件名替换为.o结尾的,这样就自动生成了源文件的目标文件序列。

5cleanrm命令钱@符号表示执行该命令时不输出任何信息。

这样,每次执行make时都会重新计算目标文件的依赖关系,并输出.depend文件,然后包含到Makefile后进行编译工作,这样目标文件的依赖关系就不会出错了!而我们得到了一个自动包含源文件和识别头文件依赖关系的Makefile,将该文件应用于任何单目录的C/C++工程(C++需要修改部分细节,不作赘述)都能正常工作。

但是,这种方式也有一定的不足,当头文件的依赖关系不发生变化时,每次make也会重新生成.depend文件。如果这样使得工程的编译变得不尽人意,那么我们可以尝试将依赖文件拆分,使得每个源文件独立拥有一个依赖文件,这样每次make时变化的只是一小部分文件的依赖关系。

EXE=main

CC=gcc

SRC=$(wildcard *.c)

OBJ=$(SRC:.c=.o)

DEP=$(patsubst %.c,.%.d,$(SRC))

CFLAGS=-g

$(EXE):$(OBJ)

$(CC) $^ -o $@

$(DEP):.%.d:%.c

@set -e;\

rm -f $@;\

$(CC) -M $< > $@.$$$$;\

sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@;\

rm -f $@.$$$$

-include $(DEP)

clean:

@rm $(EXE) $(OBJ) $(DEP) -f

Makefile增加一个变量DEP,初始化为patsubst %.c,.%.d,$(SRC),表示将SRC中的以*.c结尾的源文件名替换为.*.d的形式,比如main.c对应着文件.main.d,这就是main.c的依赖关系文件,且是隐藏的。

为了生成每个源文件的依赖文件,建立了目标依赖关系$(DEP):.%.d:%.c,该关系表示,对于目标DEP,通过$@可以访问一个依赖文件,通过$>则访问对应的同名源文件。命令部分使用\连接,表示当前命令作为一个整体在一个进程内执行。该组命令的含义是:将gcc -M生成的信息输出一个临时文件,然后在:之前加上当前的文件输出到依赖文件。比如对于main.c生成的临时文件信息为:

main.o:main.c abc.h xyz.h

处理后依赖文件信息是:

main.o .main.d:main.c abc.h xyz.h

这样的依赖关系表示main.o和它的依赖关系文件的依赖项是一致的,只要相关的源文件或头文件发生了改变,才会重新生成目标文件和依赖关系文件,也就达到了依赖关系文件单独更新的目的了。

虽然如此,但是这样的Makefile也不是完美的。现假设工程目录内新增一个文件lmn.c,按照Makefile的指令make后会产生.lmn.d依赖关系文件。而如果我们再删除lmn.c文件后,重新make.lmn.d依然存在!尤其是当重复增删很多源文件后,工程目录下可能会存在很多无用的依赖文件,当然这些问题可以通过make clean解决

通过前边的讨论,我们得到一个能在单目录工程下工作的通用Makefile,至于是实现为单独一个依赖文件的形式,还是每个源文件产生一个独立的依赖文件,要根据程序作者自己的喜恶来选择。虽然每种方法都有一些细微的瑕疵,但是不影响这个通用的Makefile的实用性,试想一下在工程目录下拷贝一份当前的Makefile,稍加修改便可以正确的编译开发,一定会令人心情大好。希望本文对你学习Linux写的程序开发有所帮助!

原文地址:https://www.cnblogs.com/fanzhidongyzby/p/3141041.html

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


在Linux上编写运行C语言程序,经常会遇到程序崩溃、卡死等异常的情况。程序崩溃时最常见的就是程序运行终止,报告 Segmentation fault (core dumped) 错误。而程序卡死一般来源于代码逻辑的缺陷,导致了死循环、死锁等问题。总的来看,常见的程序异常问题一般可以分为 非法内存访
git使用小结很多人可能和我一样,起初对git是一无所知的。我也是因为一次偶然的机会接触到git,并被它强大的功能所蛰伏。git其实就是一种版本控制工具,就像svn一样,但是git是分布式的。我不想给git打广告,我们直入正题——git能帮我们做什么?1)源码版本控制。平常写一写demo程序可能和g
1. 操作系统环境、安装包准备 宿主机:Max OSX 10.10.5 虚拟机:Parallel Desktop 10.1.1 虚拟机操作系统:CentOS 7 x86_64 DVD 1511.iso Oracle:linux.x64_11gR2_database_1of2.zip linux.x6
因为业务系统需求,需要对web服务作nginx代理,在不断的尝试过程中,简单总结了一下常见的nginx代理配置。 1. 最简反向代理配置 在http节点下,使用upstream配置服务地址,使用server的location配置代理映射。 upstream my_server { server 10
Linux模块机制浅析 Linux允许用户通过插入模块,实现干预内核的目的。一直以来,对linux的模块机制都不够清晰,因此本文对内核模块的加载机制进行简单地分析。 模块的Hello World! 我们通过创建一个简单的模块进行测试。首先是源文件main.c和Makefile。 f...
一、Hadoop HA的Web页面访问 Hadoop开启HA后,会同时存在两个Master组件提供服务,其中正在使用的组件称为Active,另一个作为备份称为Standby,例如HDFS的NameNode、YARN 的ResourceManager。HDFS的web页面只有通过Active的Name
一个简单的通用Makefile实现Makefile是Linux下程序开发的自动化编译工具,一个好的Makefile应该准确的识别编译目标与源文件的依赖关系,并且有着高效的编译效率,即每次重新make时只需要处理那些修改过的文件即可。Makefile拥有很多复杂的功能,这里不可能也没必要一一介绍,为了
Linux内核源码分析方法一、内核源码之我见Linux内核代码的庞大令不少人“望而生畏”,也正因为如此,使得人们对Linux的了解仅处于泛泛的层次。如果想透析Linux,深入操作系统的本质,阅读内核源码是最有效的途径。我们都知道,想成为优秀的程序员,需要大量的实践和代码的编写。编程固然重要,但是往往
题记:自从接触到“跳板机”的概念后,一直就被烦不胜烦的机器名,ip地址,用户名,密码折腾的死去活来,心说能有个小精灵随时帮我输入那些重复的登录信息就好了。我见过最挫的方式就是用记事本把一堆机器的ip、登录用户、密码记录下来,每次登录机器就像是一场战斗:打开笔记本 勾选复制 写ssh命令 登录 再打开
统计一下你写过多少代码最近整理了一下自己从开始学习编程以来写过的程序和代码,林林总总,花了不少的时间,最后把一些自认为还算不错的代码提交到github上做一个简单的分类和备份。当然我并不奢求它们能成为多好的开源代码,只是希望通过这种方式分享自己的劳动成果罢了。如果大家有兴趣可以访问我的github,
一直以来被Linux的hostname和fqdn(Fully Qualified Domain Name)困惑了好久,今天专门抽时间把它们的使用细节弄清了。 一、设置hostname/fqdn&#xD;在Linux系统内设置hostname很简单,如: $ hostname florian 如果...
Linux的原子操作与同步机制 并发问题 现代操作系统支持多任务的并发,并发在提高计算资源利用率的同时也带来了资源竞争的问题。例如C语言语句“count++”在未经编译器优化时生成的汇编代码为。 当操作系统内存在多个进程同时执行这段代码时,就可能带来并发问题。 假设count变量初始值为0。进程1
最简git Server配置如何保持多台计算机的项目代码的同步更新呢?通过在一个公用计算机上开启git服务,任何能与该计算机互联的终端都可以同步最新的项目代码。每个终端所负责的就是下载代码更新,修改代码,提交代码更新,这些工作产生的变化能全部反应到git服务器上。同时,这么做也能避免使用github
建议收藏!!!Linux 服务器必备的安全设置~
QQ 用 Electron 重构后,终实现 Linux、macOS、Windows 三端架构统一!
Shell 分析日志文件高效命令,超级好用!
Linux下的Docker容器网络:如何设置容器间的网络连接和通信?
Linux 服务器必备的安全设置,建议收藏!!!
以为很熟悉 Linux,万万没想到在生产环境翻车了.....
Linux 或 Windows 上实现端口映射