一个通用的Makefile (转)

时间:2022-07-10 21:50:26
http://bbs.chinaunix.net/thread-2300778-1-1.html的讨论,发现还是有很多人在问通用Makefile的问题,这里做一个总结。也作为以后的参考。
 
    笔者在写程序的时候会遇到这样的烦恼:一个项目中可能会有很多个应用程序,而新建一个应用程序则所有的Makefile都要重写一遍,虽然可以部分的粘帖复制,但还是感觉应该找到更好的解决途径;另外当一个应用程序中包含多个文件夹时通常要在每个目录下创建一个Makefile,当有数十个文件夹时,要创建如此多的Makefile也是不胜其烦。那么为什么不用automake呢,诚然,对于一个很大的工程来说使用automake是一个很好的选择,但是对于一个新手或者一个小的应用程序来说使用它有两个弊端:或多或少阻碍对于Makefile的理解和书写;没有必要,编译出了差错难以从全局把握。
 
    在某次项目中,发现了一个比较好的Makefile,所有生成的.o .d文件均位于build下,而源码位于src下,条理清晰,无论添加多少文件夹,只需要一个Makefile就可以搞定。但是很快发现一个问题,当src下不同文件夹下有重名的文件.c时,事情很糟糕,后者会覆盖前者的.o文件。那么开始考虑以下问题:
 
    只是用一个Makefile如何动态改变.o生成路径呢?比如一个源码目录为src, 其中包含两个目录a和b,分别又包含a.c 和b.c,在src目录下有一Makefile,a和b中没有单独的Makefile。
  1. src/a/a.c
  2. src/b/b.c
  3. src/main.c

如何写Makefile使临时文件.o .d(之所以需要生成.d,是因为头文件依赖,后面会提到)等自动生成到

  1. build/a/a.o 和a.d
  2. build/b/b.o 和b.d
  3. build/main.o 和main.d

通常可以做到所有临时文件都生成到build下,如下规则:

  1. ......
  2. TARGETMAIN = testmk
  3. OBJECTDIR        = build
  4. VPATH                = $(shell ls -AxR ./src|grep ":"|grep -v "\.svn"|tr -d ':')
  5. SOURCEDIRS = $(VPATH)
  6. # search source file in the current dirs
  7. SOURCES = $(foreach subdir,$(SOURCEDIRS),$(wildcard $(subdir)/*.c))
  8. SRCOBJS = $(patsubst %.c,%.o,$(SOURCES))
  9. BASE_FILES = $(notdir $(TMSRCOBJS))
  10. BUILDOBJS = $(BASE_FILES:%=$(OBJECTDIR)/%)
  11. all:$(TARGETMAIN)
  12. $(TARGETMAIN) :$(BUILDOBJS)
  13. $(CC) $(CFLAGS) -o $@ -c $<
  14. @$(STRIP) --strip-unneeded $(TARGETMAIN)
  15. ......
  16. $(OBJECTDIR)/%.o: %.c $(DEPS_DIR)
  17. @$(CC) $(CPPFLAGS) $(CFLAGS) -o $@ -c $<
  
   根据上面的问题分析,SOURCEDIRS中为src中所有的目录(包括./src),SOURCES中保存了所有的相对当前路径的.c文件,比如./src/a/a.c。SRCOBJS使用函数patsubst将SOURCES中所有.c文件的后缀替换为.o。$(TARGETMAIN)也即testmk依赖于$(BUILDOBJS),BUILDOBJS变量由SRCOBJS生成,只是所有的src路径被subst函数替换为了build,比如./src/a/a.o,则替换为了./build/a.o,我们看到在匹配$(OBJECTDIR)/%.o: %.c规则的时候,%将代表文件名main, a,b等,也即./build/a.o依赖于a.c。
  
    $@变量代表了生成的目标文件,由目标./build/a/a.o决定,$<为对应于依赖,由%.c决定,也即a.c,那么Makefile如何知道a.c对应src/a/a.c呢,这和VPATH变量有关。如果定义了VPATH这个变量,那么make就会在当当前目录找不到的情况下,到VPATH指定的目录中去查找文件了。
 
    最终build下会生成a.o,b.o和main.o,并且链接为testmk可执行文件。
 
    这是我们最终通用makefile的雏形,但是会当src下不同文件夹有重名文件的.c时,生成.o就会冲突,那么如何解决呢,我们看到$@和$<分别代表了目标集和依赖集,而目标集由%决定,.c由VPATH决定,那么我们只要改变%或者$@和$<变量就可以,$@和$<被称为自动变量,但是并不意味着我们不能动态改变它。这里给出最终的解决方案:
  1. #声明伪目标,防止Makefile去生成all等
  2. .PHONY : all install clean
  3. #定义路径变量,所有.c文件和所有非公开的.h应该放在src下,所有需要的.a文件放在lib
  4. #下,所有公开的.h(比如生成库文件的时候)或者多个.c公用的.h放在include文件夹下
  5. #global directory defined
  6. TOPDIR = $(shell pwd)
  7. SRCDIR         = $(TOPDIR)/src
  8. LIBDIR = $(TOPDIR)/lib
  9. OBJECTDIR = $(TOPDIR)/build
  10. INCLUDEDIR = $(TOPDIR)/include
  11. #定义交叉编译环境变量,当需要编译arm/mips等平台应用程序/库的时候修改它
  12. #cross compile tools defined
  13. CROSS_COMPILE =
  14. AS = $(CROSS_COMPILE)as
  15. LD = $(CROSS_COMPILE)ld
  16. CC = $(CROSS_COMPILE)gcc
  17. CPP = $(CC) -E
  18. AR = $(CROSS_COMPILE)ar
  19. NM = $(CROSS_COMPILE)nm
  20. STRIP = $(CROSS_COMPILE)strip
  21. RANLIB     = $(CROSS_COMPILE)ranlib
  22. #本机相关的命令,一般无需修改
  23. #local host tools defined
  24. CP        := cp
  25. RM        := rm
  26. MKDIR    := mkdir
  27. SED        := sed
  28. FIND    := find
  29. MKDIR    := mkdir
  30. XARGS    := xargs
  31. #目标名称,这里我们给出了三种常用的目标格式:目标文件,静态库和共享库
  32. #target name
  33. TARGETMAIN     = testmk
  34. TARGETLIBS     = libmk.a
  35. TARGETSLIBS    = libmk.so
  36. #所有源码文件的路径被放入SOURCEDIRS,所有.c源码文件(含路径)放入SOURCES
  37. #.c .o and .d files defined
  38. VPATH             = $(shell ls -AxR $(SRCDIR)|grep ":"|grep -v "\.svn"|tr -d ':')
  39. SOURCEDIRS    = $(VPATH)
  40. SOURCES     = $(foreach subdir,$(SOURCEDIRS),$(wildcard $(subdir)/*.c))
  41. #所有目标文件.o(含路径)放入BUILDOBJS,注意它们的路径已经是build了。
  42. SRCOBJS             = $(patsubst %.c,%.o,$(SOURCES))
  43. BUILDOBJS = $(subst $(SRCDIR),$(OBJECTDIR),$(SRCOBJS))
  44. #所有.d依赖文件放入DEPS
  45. DEPS            = $(patsubst %.o,%.d,$(BUILDOBJS))
  46. #注意-MD,是为了生成.d文件后,构造对.h的依赖
  47. #external include file define
  48. CFLAGS    = -O2 -Wall -MD $(foreach dir,$(INCLUDEDIR),-I$(dir))
  49. ARFLAGS = rc
  50. #special parameters for app
  51. CFLAGS    +=
  52. #LDFLAGS指明所有-llibxx,libxx.a应该放到lib下,当然也可以添加.so。Xlinker是为了
  53. #在提供多个.a时,未知它们之间的依赖顺序时,自动查找依赖顺序
  54. #c file compile parameters and linked libraries
  55. CPPFLAGS =
  56. LDFLAGS     =
  57. XLDFLAGS = -Xlinker "-(" $(LDFLAGS) -Xlinker "-)"
  58. LDLIBS         += -L$(LIBDIR)
  59. #如果要生成.a或者.so,那么不要将main函数所在的.c放入src。另外添加$(TARGETLIBS)
  60. #或$(TARGETSLIBS)到all中
  61. #defaut target:compile the currrent dir file and sub dir
  62. all: $(TARGETMAIN)
  63. #for .h header files dependence
  64. -include $(DEPS)
  65. $(TARGETMAIN) :$(BUILDOBJS)
  66. @$(CC) $(subst $(SRCDIR),$(OBJECTDIR),$^) $(CPPFLAGS) $(CFLAGS) $(XLDFLAGS) -o $@ $(LDLIBS)
  67. @$(STRIP) --strip-unneeded $(TARGETMAIN)
  68. $(TARGETLIBS) :$(BUILDOBJS)
  69. @$(AR) $(ARFLAGS) $@ $(BUILDOBJS)
  70. @$(RANLIB) $@
  71. $(TARGETSLIBS) :$(BUILDOBJS)
  72. @$(CC) -shared $(subst $(SRCDIR),$(OBJECTDIR),$^) $(CPPFLAGS) $(CFLAGS) $(XLDFLAGS) -o $@ $(LDLIBS)
  73. #这里是Makefile的核心,根据%中的内容,查找src路径下对应的.c,注意到$@和$<自动
  74. #变量的取值,首先查看路径build/xx是否存在,不存在则创建,然后我们尝试将$@中的src
  75. #替换为build,这样所有的.o和.d都将被创建到对应的build下了。
  76. $(OBJECTDIR)%.o: $(SRCDIR)%.c
  77. @[ ! -d $(dir $(subst $(SRCDIR),$(OBJECTDIR),$@)) ] & $(MKDIR) -p $(dir $(subst $(SRCDIR),$(OBJECTDIR),$@))
  78. @$(CC) $(CPPFLAGS) $(CFLAGS) -o $(subst $(SRCDIR),$(OBJECTDIR),$@) -c $<
  79. #添加安装的路径
  80. intall:
  81. clean:
  82. @$(FIND) $(OBJECTDIR) -name "*.o" -o -name "*.d" | $(XARGS) $(RM) -f
  83. @$(RM) -f $(TARGETMAIN) $(TARGETLIBS) $(TARGETSLIBS)

经测试,完全满足了要求,虽然不是完美无缺,但是对于通常的工程项目,是绰绰有余了,欢迎大家指正,以便改善。

另外对于Makefile的理解,个人认为可以分成几个部分:依赖关系,自定义变量和自动变量以及Makefile提供的相关函数。理解了它们,对写出结构良好,通用性强的Makefile会有大的帮助,虽然现在有了automake,但是研究一下Makefile的写法还是有收获的。

附件testmk.rar中提供了一个完整的测试程序,目录如下:

  1. |-- build
  2. |-- include
  3. | `-- hello.h
  4. |-- lib
  5. |-- Makefile
  6. `-- src
  7. |-- a
  8. | `-- a.c
  9. |-- b
  10. | `-- b.c
  11. `-- main.c
  12. 6 directories, 5 files

在linux下解压开后运行命令一下命令,可以根据需要定制Makefile。

  1. #make            // 当前目录下生成testmk
  2. #make libmk.a    // 当前目录下生成libmk.a
  3. #make libmk.so    // 当前目录下生成libmk.so

参考资料:

  1. 跟我一起写Makefile(通俗易懂,易于理解)
  2. GNU make中文手册(内容很全,是对GNU makefile手册的完全翻译,做手册参考)

思考:

根据Makefile的依赖原理,似乎应该使用一个树类型的结构,a依赖b,c等,b又依赖其他的文件,但是不能反方向依赖(否则为死循环),但是d可以同时依赖b,c,所以不是简单的树形结构,应该是一个图,另外依赖是有方向性的,所以应该是一个有向图结构的实现,不知道对不对?

# A generic template Makefile
# Author Red_Liu lli_njupt@.com v0.
#
# This file is a(part of) free software; you can redistribute it
# and/or modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version , or
# (at your option) any later version. .PHONY : all install clean #global directory defined
TOPDIR = $(shell pwd)
SRCDIR = $(TOPDIR)/src
LIBDIR = $(TOPDIR)/lib
OBJECTDIR = $(TOPDIR)/build
INCLUDEDIR = $(TOPDIR)/include #cross compile tools defined
CROSS_COMPILE ?=
AS = $(CROSS_COMPILE)as
LD = $(CROSS_COMPILE)ld
CC = $(CROSS_COMPILE)gcc
CPP = $(CC) -E
AR = $(CROSS_COMPILE)ar
NM = $(CROSS_COMPILE)nm
STRIP = $(CROSS_COMPILE)strip
RANLIB = $(CROSS_COMPILE)ranlib #local host tools defined
CP := cp
RM := rm
MKDIR := mkdir
SED := sed
FIND := find
MKDIR := mkdir
XARGS := xargs #target name
TARGETMAIN = testmk
TARGETLIBS = libmk.a
TARGETSLIBS = libmk.so #FILE' INFOMATION COLLECT
VPATH = $(shell ls -AxR $(SRCDIR)|grep ":"|grep -v "\.svn"|tr -d ':')
SOURCEDIRS = $(VPATH) #search source file in the current dir
SOURCES = $(foreach subdir,$(SOURCEDIRS),$(wildcard $(subdir)/*.c))
SRCOBJS = $(patsubst %.c,%.o,$(SOURCES))
BUILDOBJS = $(subst $(SRCDIR),$(OBJECTDIR),$(SRCOBJS))
DEPS = $(patsubst %.o,%.d,$(BUILDOBJS)) #external include file define
CFLAGS = -O2 -Wall -MD $(foreach dir,$(INCLUDEDIR),-I$(dir))
ARFLAGS = rc #special parameters for apps
CFLAGS += #c file compile parameters and linked libraries
CPPFLAGS =
LDFLAGS =
XLDFLAGS = -Xlinker "-(" $(LDFLAGS) -Xlinker "-)"
LDLIBS += -L$(LIBDIR) #defaut target:compile the currrent dir file and sub dir
all: $(TARGETMAIN) #for .h header files dependence
-include $(DEPS) $(TARGETMAIN) :$(BUILDOBJS)
@$(CC) $(subst $(SRCDIR),$(OBJECTDIR),$^) $(CPPFLAGS) $(CFLAGS) $(XLDFLAGS) -o $@ $(LDLIBS)
@$(STRIP) --strip-unneeded $(TARGETMAIN) $(TARGETLIBS) :$(BUILDOBJS)
@$(AR) $(ARFLAGS) $@ $(BUILDOBJS)
@$(RANLIB) $@ $(TARGETSLIBS) :$(BUILDOBJS)
@$(CC) -shared $(subst $(SRCDIR),$(OBJECTDIR),$^) $(CPPFLAGS) $(CFLAGS) $(XLDFLAGS) -o $@ $(LDLIBS) $(OBJECTDIR)%.o: $(SRCDIR)%.c
@[ ! -d $(dir $(subst $(SRCDIR),$(OBJECTDIR),$@)) ] & $(MKDIR) -p $(dir $(subst $(SRCDIR),$(OBJECTDIR),$@))
@$(CC) $(CPPFLAGS) $(CFLAGS) -o $(subst $(SRCDIR),$(OBJECTDIR),$@) -c $< intall: clean:
@$(FIND) $(OBJECTDIR) -name "*.o" -o -name "*.d" | $(XARGS) $(RM) -f
@$(RM) -f $(TARGETMAIN) $(TARGETLIBS) $(TARGETSLIBS)

Linux Makefile 生成 *.d 依赖文件及 gcc -M -MF -MP 等相关选项说明

1. 为什么要使用后缀名为 .d 的依赖文件?

在 Makefile 中, 我们的依赖关系可能需要包含一系列的头文件。
比如 main.c 源文件内容如下:

#include "stdio.h"
#include "defs.h" int main(int argc, char *argv[])
{
printf("Hello, %s!\n", NAME);
return 0;
}

defs.h 头文件如下:

#ifndef _DEFS_H_
#define _DEFS_H_ #define NAME "makefile" #endif _DEFS_H_
那么依赖关系应该如下:
main.o : main.c stdio.h defs.h ...

但如果是一个比较大型的工程,你必需清楚每一个 C 源文件包含了哪些头文件,并且在加入或删除头文件时,也需要小心地修改 Makefile,这是一个很没有维护性的工作。为了避免这种繁重而又容易出错的事情,我们可以使用 C/C++ 编译的一个功能。大多数的 C/C++ 编译器都支持一个 “-M” 的选项,即自动找寻源文件中包含的头文件,并生成一个依赖关系。例如,执行下面的命令:

gcc -M main.c
其输出如下:
main.o : main.c defs.h

由编译器自动生成依赖关系,这样做的好处有以下几点:

  • 不必手动书写若干文件的依赖关系,由编译器自动生成
  • 不管是 .c 文件还是 .h 文件有更新,目标文件都会重新编译

2. 使用说明:

参数介绍:

-M
    生成文件的依赖关系,同时也把一些标准库的头文件也包含了进来。本质是告诉预处理器输出一个适合 make 的规则,用于描述各目标文件的依赖关系。对于每个源文件,预处理器输出 一个 make 规则,该规则的目标项 (target) 是源文件对应的目标文件名,依赖项 (dependency) 是源文件中 ‘#include’ 引用的所有文件,生成的规则可以是单行,但如果太长,就用’\’换行符续成多行。规则 显示在标准输出,不产生预处理过的C程序。
    注意:该选项默认打开了 -E 选项, -E 参数的用处是使得编译器在预处理结束时就停止编译

例如: gcc -M main.c
则在终端上输出如下:
main.o: main.c defs.h \
/usr/include/stdio.h \
/usr/include/features.h \
/usr/include/sys/cdefs.h /usr/include/gnu/stubs.h \
/usr/lib/gcc-lib/i486-suse-linux/2.95.3/include/stddef.h \
/usr/include/bits/types.h \
/usr/include/bits/pthreadtypes.h \
/usr/include/_G_config.h /usr/include/wchar.h \
/usr/include/bits/wchar.h /usr/include/gconv.h \
/usr/lib/gcc-lib/i486-suse-linux/2.95.3/include/stdarg.h \
/usr/include/bits/stdio_lim.h

-MM
生成文件的依赖关系,和 -M 类似,但不包含标准库的头文件

例如:gcc -MM main.c
则在终端上输出如下:
main.o: main.c defs.h

-MG
要求把缺失的头文件按存在对待,并且假定他们和源程序文件在同一目录下.必须和 ‘-M’ 选项一起用.

-MF File
当使用了 ‘-M’ 或者 ‘-MM’ 选项时,则把依赖关系写入名为 ‘File’ 的文件中。若同时也使用了 ‘-MD’ 或 ‘-MMD’,’-MF’ 将覆写输出的依赖文件的名称

例如:gcc -M -MF main.d main.c
则 '—M' 输出的内容就存在于 main.d 文件中了

-MD
等同于 ‘-M -MF File’,但是默认关闭了 ‘-E’ 选项. 其输出的文件名是基于 ‘-o’ 选项,若给定了 ‘-o’ 选项,则输出的文件名是 ‘-o’ 指定的文件名,并添加 .d 后缀,若没有给定,则输入的文件名作为输出的文件名,并添加.d后缀,同时继续指定的编译工作
注意:’-MD’ 不会像 ‘-M’ 那样阻止正常的编译任务. 因为它默认关闭了 ‘-E’ 选项, 比如命令中使用了 -c 选项,其结果要生成 .o 文件,若使用了 ‘-M’ 选项,则不会生成 .o 文件,若使用的是 ‘-MD’ 选项,则会生成 .o 文件

例如1:

gcc -E -MD main.c
本目录下生成了以下文件:
main.d
同时在终端上输出了 main.c文件的预处理结果
经实测发现,不使用 '-o' 指定输出文件名,以下情况有细微的差别:
gcc -E main.c
//不使用 '-o',则把结果输出在终端上
gcc -S main.c
//不使用 '-o',则把结果默认输出到以输入文件名为名称的 .s 文件中,即 main.s
gcc -c main.c
//同上 gcc main.o //不使用 '-o',则把结果默认输出到 a.out 可执行文件中
例如2:gcc -E -o tmp.i -MD main.c
本目录下生成了以下文件:
tmp.d tmp.i
例如3:gcc -c -MD main.c
本目录下生成了以下文件:
main.d main.o
例如4:gcc -c -o tmp.o -MD main.c
本目录下生成了以下文件:
tmp.d tmp.o
例如5: gcc -MD main.c
本目录下生成了以下文件:
a.out main.d
例如6: gcc -M -MD main.c
本目录下生成了以下文件:
main.d //并不会生成a.out可执行文件,因为 '-M' 默认打开了 '-E' 选项,使得编译器在预处理结束后就停止编译

-MMD
类似于 ‘-MD’,但是输出的依赖文件中,不包含标准头文件

-MP
生成的依赖文件里面,依赖规则中的所有.h依赖项都会在该文件中生成一个伪目标,其不依赖任何其他依赖项。该伪规则将避免删除了对应的头文件而没有更新 “Makefile” 去匹配新的依赖关系而导致make出错的情况出现。
(英文描述:This option instructs CPP to add a phony target for each dependency
other than the main file, causing each to depend on nothing. These
dummy rules work around errors ‘make’ gives if you remove header
files without updating the ‘Makefile’ to match.)

例如1:   gcc -c -MM -MD main.c
生成的 main.d 文件内容如下:
main.o: main.c defs.h
例如2:   gcc -c -MM -MD main.c -MP
生成的 main.d 文件内容如下:
main.o: main.c defs.h
defs.h:   //该选项会生成该伪目标,其没有任何依赖项,若不使用 '-MP' 选项,则不会生成该伪目标规则

-MT Target
在生成的依赖文件中,指定依赖规则中的目标

例如:  gcc -MF main.d -MG -MM -MP -MT main.d -MT main.o main.c

$ cat main.d     #查看生成的依赖文件的内容,输出以下内容

main.d main.o: main.c

注:依赖规则中main.d 和 main.o 目标都是通过'-MT'选项指定的

3. 使用参考:

以上简单介绍了 gcc -M 相关的选项,旨在让 make 自动推导并生成文件的依赖关系.
以下提供一个比较好的 gcc -M 选项的参考示例, 它将自动生成依赖文件,并保存在指定目录下的 ‘.d’ 文件中。

makefile如下所示:

SRCS=$(wildcard *.c)
OBJS=$(SRCS:.c=.o)
DEPS=$(SRCS:.c=.d) .PHONY: all clean all: main -include $(DEPS) #注释:'-'号的作用:加载错误时,会继续执行 make,主要是考虑到首次 make 时,目录中若不存在 '*.d' 文件时,加载便会产生错误而停止 make 的执行 %.o:%.c
gcc -c -g -Wall $< -o $@ -MD -MF $*.d -MP main: $(OBJS)
gcc $^ -o $@ #注释:$^:表示所有的依赖文件 $@:表示目标文件 clean:
rm -f *.d *.o main

仍旧以本篇文章开头的源文件进行 make,将生成如下文件:
main : 可执行文件
main.o : 编译的二进制目标文件
main.d:保存了 main.o 依赖关系的文件

注释: $* 表示目标模式中 '%' 及其之前的部分.如果目标是 'dir/a.foo.b', 
并且目标的模式为 'a.%.b',那么 '$*' 的值就是 'dir/a.foo'.
如果目标中没有模式的定义,那么 '$*' 就不能被推导出.
但是,如果目标文件是 make 所识别的,那么 '$*' 就是除了后缀的那一部分, 例如:目标是 'foo.c',因为 '.c' 是 make 所能识别的后缀名,
所以 '$*' 的值就是 'foo'.这个特性是 GNU make 的.