C实战:项目构建Make,Automake,CMake

在本系列文章《C实战:强大的程序调试工具GDB》中我们简要学习了流行的调试工具GDB的使用方法。本文继续“C实战”的主题,对同样非常流行的构建工具Make的用法和原理一探究竟,并顺便看一下一些高级衍生产品。


1.Make基础

首先我们编写一个简单的C项目,以此项目在实战中学习Make的相关知识。更全面的介绍请参考官方手册

cdai@vm /syspace/2-ccpp/24-pragmatic/build-tool/make $ tree
.
├── hello.c
├── hello.h
├── main.c
└── Makefile 0 directories, 4 files

整个程序的逻辑非常简单:main.c中包含一个main方法,调用了hello.c中的sayHello()函数,打印了一句话到控制台上。

// cat main.c hello.h hello.c
// ----- main.c -----
#include "hello.h" int main(void)
{
sayHello("Make");
return 1;
} // ----- hello.h -----
#ifndef _HELLO_H_
#define _HELLO_H_ void sayHello(char *name); #endif // ----- hello.c -----
#include "hello.h"
#include <stdio.h> void sayHello(char *name)
{
printf("Hello, %s!\n", name);
}

1.1 基本语法

一个简单的Makefile包含很多规则(Rule),每一条规则的语法结构由目标(Target)、先决条件(Prerequisite)、动作(Recipe)三部分组成:

  • 目标:通常有两种命名方法,一是与要生成的可执行文件或目标文件同名,二是说明动作的目的,例如最常见的clean清理规则。对于第二种规则命名,为了避免与同名文件冲突,可以将目标名加入到.PHONY伪目标列表中。默认情况下,make执行Makefile中的第一个规则,此规则被称为最终目标
  • 先决条件:先决条件是用来创建目标的输入文件,一个目标可以依赖多个先决条件
  • 动作:动作由Make命令负责执行,可以包含多个命令,每个命令可以另起一行。一定要注意的是:命令必须以TAB开头
target: prerequisite
recipe

下面就看一下示例项目的Makefile是什么样子的。在Makefile中有3个规则,其中目标main依赖于main.o和hello.o,利用gcc执行链接,这与我们的代码结构是相对应的。而main.o和hello.o则分别依赖于各自的源代码.c文件和hello.h头文件,并利用gcc -c执行编译。

main: main.o hello.o
gcc -o main main.o hello.o main.o: main.c hello.h
gcc -c main.c -o main.o hello.o: hello.c hello.h
gcc -c hello.c -o hello.o

1.2 实现原理

Make看似非常智能,其实它的原理就像其语法规则一样简单。

  1. 确定目标:如果没有指明,则执行最终目标,即第一个规则的目标
  2. 处理变量和规则:替换变量,推导隐式规则(下一节会学习)
  3. 生成依赖关系链:为所有目标生成依赖关系链
  4. 递归构建:从依赖链的底部向上,根据先决条件会有三种情况:

    4.1 先决条件不存在,则执行规则中的命令

    4.2 先决条件存在,且至少一个比目标“更新”,则执行规则中的命令重新生成

    4.3 先决条件存在,且都比目标“更旧”, 则什么都不做

了解了Make的原理,就看一下我们的示例项目Make的执行过程。可以看到,Make以第一个目标main作为构建目标,从关系链底部的main.o和hello.o开始构建,最终生成了可执行文件main。接下来就执行main,可以看到控制台输出了”Hello, Make!”,证明我们构建成功了!

cdai@vm /syspace/2-ccpp/24-pragmatic/build-tool/make $ make
gcc -c main.c -o main.o
gcc -c hello.c -o hello.o
gcc -o main main.o hello.o cdai@vm /syspace/2-ccpp/24-pragmatic/build-tool/make $ ./main
Hello, Make!

再次执行make会看到“‘main’ is up to date.”的提示,说明Make检测到了没有发生任何修改。如果我们做一点改动,例如修改以下sayHello()函数中的输出,再执行Make就能看到hello.o和main被重新构建,而main.o规则的命令没有被执行。

cdai@vm /syspace/2-ccpp/24-pragmatic/build-tool/make $ make
make: 'main' is up to date. cdai@vm /syspace/2-ccpp/24-pragmatic/build-tool/make $ make
gcc -c hello.c -o hello.o
gcc -o main main.o hello.o cdai@vm /syspace/2-ccpp/24-pragmatic/build-tool/make $ ./main
Hello111, Make!

2.Make进阶

2.1 变量

在Makefile中,我们可以用变量来替换重复出现在先决条件或动作中的字符串。例如,对于前面我们的示例Makefile,最明显的问题就是gcc和main目标依赖的main.o和hello.o出现了多次,我们可以用变量将它们提取出来。同样地,我们也经常将链接和编译选项做成变量。

LD      = gcc
CC = gcc
CFLAGS = -Wall
OBJECTS = main.o hello.o all: main main: $(OBJECTS)
$(LD) -o main $(OBJECTS) main.o: main.c hello.h
$(CC) $(CFLAGS) -c $< -o $@ hello.o: hello.c hello.h
$(CC) $(CFLAGS) -c hello.c -o hello.o

执行一下make可以看到效果,我们提取出来的变量在执行之前都被替换到了正确的位置。

cdaih@vm /syspace/2-ccpp/24-pragmatic/build-tool/make $ make
gcc -Wall -c main.c -o main.o
gcc -Wall -c hello.c -o hello.o
gcc -o main main.o hello.o

2.2 隐式规则

使用Make编译.c源文件时,规则的命令和先决条件都可以简化,对于命令,我们不用明确指出,Make能够自动将.c编译成.o;对于先决条件,Make还会自动寻找.o对应的.c源文件,我们只需给出头文件即可。

LD      = gcc
OBJECTS = main.o hello.o all: main main: $(OBJECTS)
$(LD) -o main $(OBJECTS) main.o: hello.h
hello.o: hello.h

我们将main.o和hell.o的规则都做了简化,执行一下可以看到Make自动执行了cc -c,并根据目标找到了对应的源文件main.c和hello.c。

cdaih@vm /syspace/2-ccpp/24-pragmatic/build-tool/make $ make
cc -c -o main.o main.c
cc -c -o hello.o hello.c
gcc -o main main.o hello.o

2.3 模式规则

隐式规则虽然很方便,但有时我们还想自己控制规则,这时我们可以使用模式规则。老Make支持.c.o这种规则定义,而新Make一般推荐使用模式规则,因为它支持模式匹配,更灵活、更强大!例如,我们定义目标名匹配%.o和先决条件匹配%.c的话,就执行编译命令。这样main.o和hello.o被简化的同时,我们还对其进行了精确的控制。

LD      = gcc
CC = gcc
CFLAGS = -Wall
OBJECTS = main.o hello.o %.o: %.c
$(CC) $(CFLAGS) -c $< -o $@ all: main main: $(OBJECTS)
$(LD) -o main $(OBJECTS) main.o: main.c hello.h
hello.o: hello.c hello.h

执行一下看看效果。

cdaih@vm /syspace/2-ccpp/24-pragmatic/build-tool/make $ make
gcc -Wall -c main.c -o main.o
gcc -Wall -c hello.c -o hello.o
gcc -o main main.o hello.o

3.Makefile生成工具

Make的流行也带动起一批自动生成Makefile的工具,目的就是进一步减轻项目构建中的工作量,让我们程序员全身心投入到开发之中。在这些工具中,不得不提Automake和CMake。

3.1 Automake

Automake其实是一系列工具集Autotools中的一员,要想发挥Automake的威力,需要配合使用Autotools中的其他工具,例如autoscan、aclocal、autoconf和autoheader。在下面的Automake构建流程中,能看到这些工具的身影。

  1. autoscan:生成configure.scan
  2. configure.in:将configure.scan重命名为configure.in后,修改内容。重点是AM_INIT_AUTOMAKE和AC_CONFIG_FILES两项,如果没配置的话,下一步的aclocal是无法产生aclocal.m4的
  3. aclocal:生成aclocal.m4
  4. autoconf:生成configure
  5. autoheader:生成config.h.in,使程序可移植
  6. Makefile.am:手动编写Makefile.am。bin_PROGRAMS指定最终生成可执行文件的名称,helloworld_SOURCES指定所有源文件
  7. NEWS AUTHORS README ChangeLog:手动创建
  8. automake:执行automake -a生成Makefile.in
  9. configure:执行./configure生成Makefile
# Step 1:
[root@vm automaketest]# autoscan # Step 2:
[root@vm automaketest]# mv configure.scan configure.in
[root@vm automaketest]# cat configure.in
# -*- Autoconf -*-
# Process this file with autoconf to produce a configure script. AC_PREREQ([2.63])
AC_INIT(main, 1.0)
AM_INIT_AUTOMAKE(main, 1.0)
AC_CONFIG_SRCDIR([main.c])
AC_CONFIG_HEADERS([config.h]) # Checks for programs.
AC_PROG_CC # Checks for libraries. # Checks for header files. # Checks for typedefs, structures, and compiler characteristics. # Checks for library functions. AC_CONFIG_FILES([Makefile])
AC_OUTPUT # Step 3:
[root@vm automaketest]# aclocal # Step 4:
[root@vm automaketest]# autoconf # Step 5:
[root@vm automaketest]# autoheader # Step 6:
[root@vm automaketest]# cat Makefile.am
bin_PROGRAMS=main
main_SOURCES=main.c hello.c # Step 7:
[root@vm automaketest]# touch NEWS README AUTHORS ChangeLog # Step 8:
[root@vm automaketest]# automake -a
configure.in:6: installing './install-sh'
configure.in:6: installing './missing'
Makefile.am: installing './INSTALL'
Makefile.am: installing './COPYING' using GNU General Public License v3 file
Makefile.am: Consider adding the COPYING file to the version control system
Makefile.am: for your code, to avoid questions about which license your project uses.
Makefile.am: installing './depcomp' # Step 9:
[root@BC-VM-edce4ac67d304079868c0bb265337bd4 automaketest]# ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... /bin/mkdir -p
checking for gawk... gawk
checking whether make sets $(MAKE)... yes
checking for gcc... gcc
checking for C compiler default output file name... a.out
checking whether the C compiler works... yes
checking whether we are cross compiling... no
checking for suffix of executables...
checking for suffix of object files... o
checking whether we are using the GNU C compiler... yes
checking whether gcc accepts -g... yes
checking for gcc option to accept ISO C89... none needed
checking for style of include used by make... GNU
checking dependency style of gcc... gcc3
configure: creating ./config.status
config.status: creating Makefile
config.status: creating config.h
config.status: executing depfiles commands

这样Makefile就生成好了,看一下当前目录发现已经这么多文件了!如果想清理一下怎么办呢?其实Automake早为我们想好了,它生成的Makefile功能很多:

+ make:编译源代码,生成目标文件

+ make clean:清理之前make产生的临时文件

+ make install:将编译好的可执行文件安装到系统目录,一般为/usr/local/bin

+ make dist:生成软件发布包,将可执行文件及相关文件打包成”PACKAGE-VERSION.tar.gz”的tarball。其中PACKAGE和VERSION可以在configure.in中通过AM_INIT_AUTOMAKE(PACKAGE, VERSION)定义。对于我们的例子,执行后会生成main-1.0.tar.gz

+ make distcheck:查看发布包是否正确,解压开执行configure和make来确认

+ make distclean:不仅将make产生的文件,同时将configure生成的文件也都删除,包括Makefile

[root@vm automaketest]# make dist
{ test ! -d "main-1.0" || { find "main-1.0" -type d ! -perm -200 -exec chmod u+w {} ';' && rm -fr "main-1.0"; }; }
test -d "main-1.0" || mkdir "main-1.0"
test -n "" \
|| find "main-1.0" -type d ! -perm -755 \
-exec chmod u+rwx,go+rx {} \; -o \
! -type d ! -perm -444 -links 1 -exec chmod a+r {} \; -o \
! -type d ! -perm -400 -exec chmod a+r {} \; -o \
! -type d ! -perm -444 -exec /bin/sh /root/Temp/automaketest/install-sh -c -m a+r {} {} \; \
|| chmod -R a+r "main-1.0"
tardir=main-1.0 && /bin/sh /root/Temp/automaketest/missing --run tar chof - "$tardir" | GZIP=--best gzip -c >main-1.0.tar.gz
{ test ! -d "main-1.0" || { find "main-1.0" -type d ! -perm -200 -exec chmod u+w {} ';' && rm -fr "main-1.0"; }; } [root@vm automaketest]# tree -L 1
.
├── aclocal.m4
├── AUTHORS
├── autom4te.cache
├── autoscan.log
├── ChangeLog
├── config.h
├── config.h.in
├── config.log
├── config.status
├── configure
├── configure.in
├── COPYING -> /usr/share/automake-1.11/COPYING
├── depcomp -> /usr/share/automake-1.11/depcomp
├── hello.c
├── hello.h
├── INSTALL -> /usr/share/automake-1.11/INSTALL
├── install-sh -> /usr/share/automake-1.11/install-sh
├── main.c
├── main-1.0.tar.gz
├── Makefile
├── Makefile.am
├── Makefile.in
├── missing -> /usr/share/automake-1.11/missing
├── NEWS
├── README
└── stamp-h1 1 directory, 24 files [root@vm automaketest]# make distclean
test -z "main" || rm -f main
rm -f *.o
rm -f *.tab.c
test -z "" || rm -f
test . = "." || test -z "" || rm -f
rm -f config.h stamp-h1
rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
rm -f config.status config.cache config.log configure.lineno config.status.lineno
rm -rf ./.deps
rm -f Makefile

测试一下,看看Automake生成的Makefile是否能正常工作。

[root@vm automaketest]# make
make all-am
make[1]: Entering directory '/root/Temp/automaketest'
gcc -DHAVE_CONFIG_H -I. -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
mv -f .deps/main.Tpo .deps/main.Po
gcc -DHAVE_CONFIG_H -I. -g -O2 -MT hello.o -MD -MP -MF .deps/hello.Tpo -c -o hello.o hello.c
mv -f .deps/hello.Tpo .deps/hello.Po
gcc -g -O2 -o main main.o hello.o
make[1]: Leaving directory '/root/Temp/automaketest' [root@vm automaketest]# ./main
Hello, Make!

3.2 CMake

前面我们已经见识了Automake的强大和复杂。现在我们重新用CMake生成Makefile,Automake中的9步被压缩到了只需要2步

  1. 编写CMakeLists.txt
  2. 执行cmake .

3.2.1 CMakeLists.txt

对于我们示例中这种简单的项目,CMakeLists.txt简单得不能再简单了。指定好项目名称和最终生成的可执行文件名称后,就完成了!

# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8) # 项目信息
project (main) # 查找当前目录下的所有源文件
# 并将名称保存到 DIR_SRCS 变量
aux_source_directory(. DIR_SRCS) # 指定生成目标
add_executable(main ${DIR_SRCS})

3.2.2 cmake

现在执行cmake .就能得到一个CMake为我们自动生成的Makefile。这个Makefile比我们手写的要复杂得多,这里就不深入分析了。除了Makefile外,CMake还产生了一些缓存文件和临时文件,目前还不清楚具体是做什么的。

[root@vm cmaketest]# cmake .
-- The C compiler identification is GNU 4.4.7
-- The CXX compiler identification is GNU 4.4.7
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Configuring done
-- Generating done
-- Build files have been written to: /root/Temp/cmaketest [root@vm cmaketest]# tree -L 1
.
├── CMakeCache.txt
├── CMakeFiles
├── cmake_install.cmake
├── CMakeLists.txt
├── hello.c
├── hello.h
├── main.c
└── Makefile 1 directory, 7 files [root@vm cmaketest]# make
Scanning dependencies of target main
[ 50%] Building C object CMakeFiles/main.dir/main.c.o
[100%] Building C object CMakeFiles/main.dir/hello.c.o
Linking C executable main
[100%] Built target main

附:参考资料

  1. make官方手册
  2. make学习记录
  3. CMake入门实战
  4. 使用autotools生成makefile文件入门
  5. automake/autoconf入门

最新文章

  1. 总结iOS开发中的断点续传那些事儿
  2. 上传文件(单文件)(FormData)(前端代码+.NET服务器端)
  3. javascript优化--03高质量编码
  4. instancetype
  5. 【Stage3D学习笔记续】真正的3D世界(四):空间大战雏形
  6. [leetcode] 399. Evaluate Division
  7. Apple Swift学习资料汇总
  8. 三种客户端访问wcf服务端的方法 C#
  9. 再探CRC(转)
  10. Mac OS X 绑定80端口,不装nginx的小技巧
  11. 虚拟机linux系统明明已经安装了ubuntu,但是每次重新进入就又是选择安装界面
  12. 多版本python安装第三方库
  13. docker从初识到深入
  14. groovy Date 格式化
  15. 【html5】html5 本地存储
  16. shell 1基础
  17. Jvm内存工具
  18. 【贪心】hdu4803 Poor Warehouse Keeper
  19. 用Java Swing实现Freecell(空当接龙)
  20. zookeeper程序员指南

热门文章

  1. C语言的一些输出格式
  2. 使用MFC创建C++程序
  3. flask开发表单
  4. jacascript CSS样式的脚本化操作
  5. 确定稳定的 Spring Cloud 相关环境版本
  6. MVC系列 引入MVC
  7. [LeetCode] Non-decreasing Array 非递减数列
  8. 学习flexible.js
  9. Git的安装和使用(托管至GitHub的方法)
  10. [SHOI2016]黑暗前的幻想乡