本文转自http://blog.chinaunix.net/uid-20608849-id-360294.html

 笔者在写程序的时候会遇到这样的烦恼:一个项目中可能会有很多个应用程序,而新建一个应用程序则所有的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,所以不是简单的树形结构,应该是一个图,另外依赖是有方向性的,所以应该是一个有向图结构的实现,不知道对不对?

--------------------------------------------------------------------------------分割线--------------------------------------------------------------------------------------

文件testCmakefile和testCPPmakefile分别是真的c和c++的两个实例,两者makefile内容主要的区别就是编译器gcc和g++,以及后缀.c和.cpp。其他的地方都不用修改。

Ubuntu下比较通用的makefile实例的更多相关文章

  1. Ubuntu下通过makefile生成静态库和动态库简单实例

    本文转自http://blog.csdn.net/fengbingchun/article/details/17994489 Ubuntu环境:14.04 首先创建一个test_makefile_gc ...

  2. Ubuntu 14.04下 Java通用安装方法

    参考: 解决Floodlight1.2+Mininet问题及使用安装 Ubuntu下安装JDK1.7图文详解 Ubuntu 14.04下 Java通用安装方法 1.到oracle官网下下载对应jdk包 ...

  3. 在Ubuntu下安装imx6linux系统的交叉编译环境遇到的问题总结

    这段时间一直忙于手上的嵌入式项目,可以说自己从嵌入式的菜鸟一点点的入门了,关于嵌入式和imx6核心板的开发有了一点的了解,尤其是对于板子环境的搭建.硬件的开发,搭建环境,是一个很大的工程量,也是很重要 ...

  4. 搭建Ubuntu下c/c++编译环境【转】

    1.       安装Ubuntu. 2.       安装gcc 方法一: sudo apt-get  install  build-essential 安装完了可以执行 gcc--version的 ...

  5. ubuntu下常用命令

    目录 一.查找命令 二.打开相应文件 三.查看系统资源占用 四.Ubantu解压文件 五.虚拟机ubuntu server 14.0 根目录扩容 七.ubuntu 关机,重启,注销命令 1 关机命令 ...

  6. 编写一个通用的Makefile文件

    1.1在这之前,我们需要了解程序的编译过程 a.预处理:检查语法错误,展开宏,包含头文件等 b.编译:*.c-->*.S c.汇编:*.S-->*.o d.链接:.o +库文件=*.exe ...

  7. Linux C编程学习之开发工具3---多文件项目管理、Makefile、一个通用的Makefile

    GNU Make简介 大型项目的开发过程中,往往会划分出若干个功能模块,这样可以保证软件的易维护性. 作为项目的组成部分,各个模块不可避免的存在各种联系,如果其中某个模块发生改动,那么其他的模块需要相 ...

  8. 来杯Caffe——在ubuntu下安装Caffe框架并测试

    Caffe是一种深度学习框架...blablabla...... Caffe要在ubuntu下安装 1. 安装依赖 sudo apt-get install libatlas-base-dev sud ...

  9. windows、ubuntu下eclipse搭建java、Python环境问题总结

    前两篇博文分别讲述了如何在windows.ubuntu下用eclipse搭建java.python环境,下面就针对本人遇到的问题做一个总结. 一.windows下关于java环境变量JAVA_HOME ...

随机推荐

  1. Java对象大小:size和retained size

    最近看到网上很多文章讲如何计算java对象的大小(size),很多观点不敢苟同. 这是其中一篇比较靠前的文章,写的也比较全面: http://blog.csdn.net/iter_zc/article ...

  2. GMF常见问题

    1.问题:连接线旁边没有文字标签和箭头 文字标签:在gmfmap里的Connection Mappping下增加Label Mapping元素:箭头:在gmfgraph里为Polyline Conne ...

  3. oracle pl/sql 包

    包用于在逻辑上组合过程和函数,它由包规范和包体两部分组成.1).我们可以使用create package命令来创建包,如:i.创建一个包sp_packageii.声明该包有一个过程update_sal ...

  4. JS设计模式(二) 惰性模式

    惰性模式:减少代码每次执行时的重复性判断,通过重新定义对象来避免原对象中的分支判断,提高网站性能. 例如针对不同浏览器的事件注册方法: var AddEvent = function(dom, typ ...

  5. .NET第四章总结

    .NET第四章简单总结 1.简单的获取文件路径:                    2.回车跳转控件焦点    3.*************无标题窗体拖动!!************* 1): ...

  6. HIVE---基于Hadoop的数据仓库工具讲解

    Hadoop: Hadoop是一个由Apache基金会所开发的分布式系统基础架构.用来开发分布式程序.充分利用集群的威力进行高速运算和存储.Hadoop实现了一个分布式文件系统(Hadoop Dist ...

  7. Spring读书笔记——bean加载

    我们的日常开发几乎离不开Spring,他为我们的开发带来了很大的便捷,那么Spring框架是如何做到方便他人的呢.今天就来说说bean如何被加载加载. 我们在xml文件中写过太多类似这样的bean声明 ...

  8. C#下的两种加密方式MD5和DEC

    md5加密 /// <summary>    /// MD5加密    /// </summary>    /// <param name="toCryStri ...

  9. JavaScript设计模式--简单工厂模式例子---XHR工厂

    第一步,Ajax操作接口(目的是起一个接口检测作用) (1)引入接口文件 //定义一个静态方法来实现接口与实现类的直接检验 //静态方法不要写出Interface.prototype ,因为这是写到接 ...

  10. Java继承--子类的实例化过程

    一个对象的实例化过程: Person p = new Person(); 1,JVM会读取指定的路径下的Person.class文件,并加载进内存,并会先加载Person的父类(如果有直接的父类的情况 ...