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

特别是在类Unix系统下的软件编译,会不会写makefile,从一个侧面说明了一个人是否具备完成大型工程的能力。因为,makefile关系到了整个工程的编译规则。一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi的make,Visual C++的nmake,Linux下的GNU make。

Makefile基本格式如下

target ... : prerequisites ...
    command
    ......
    ......
###target        - 目标文件, 可以是 Object File, 也可以是可执行文件
###prerequisites - 生成 target 所需要的文件或者目标
###command       - make需要执行的命令 (任意的shell命令), 必须以[tab]开头

Makefile基本规则

显示规则 :: 说明如何生成一个或多个目标文件(包括 生成的文件, 文件的依赖文件, 生成的命令)
隐晦规则 :: make的自动推导功能所执行的规则
变量定义 :: Makefile中定义的变量
文件指示 :: Makefile中引用其他Makefile; 指定Makefile中有效部分; 定义一个多行命令
注释 :: Makefile只有行注释 "#", 如果要使用或者输出"#"字符, 需要进行转义, "#"

make基本的工作方式

1.读入主Makefile (主Makefile中可以引用其他Makefile)
2.读入被include的其他Makefile
3.初始化文件中的变量
4.推导隐晦规则, 并分析所有规则
5.为所有的目标文件创建依赖关系链
6.根据依赖关系, 决定哪些目标要重新生成
7.执行生成命令
示例

[free@hurd]$ vim foo1.c
#include <stdio.h>
void foo1()
{
     printf("This is foo1!\n");
}
[free@hurd]$ vim foo2.c
#include <stdio.h>
void foo2()
{
    printf("This is foo2!\n");
}
[free@hurd]$ vim main.c
#include <stdio.h>
int main(void)
{
    foo1();
    foo2();
    printf("This is main!\n");
    return 0;
}
[free@hurd]$ vim Makefile
main: foo1.c foo2.c main.c
	gcc foo1.c foo2.c main.c -o main
###这是一个最简单的Makefile
###生成目标main依赖foo1.c foo2.c main.c
###执行命令gcc foo1.c foo2.c main.c -o main;完成目标
[free@hurd]$ make
gcc foo1.c foo2.c main.c -o main
[free@hurd]$ ls
foo1.c  foo2.c  main  main.c  Makefile
[free@hurd]$ ./main
This is foo1!
This is foo2!
This is main!
###成功执行,现在改进Makefile
[free@hurd]$ vim Makefile
main: main.o foo1.o foo2.o
	gcc main.o foo1.o foo2.o -o main
main.o: main.c
	gcc -c main.c
foo1.o: foo1.c
	gcc -c foo1.c
foo2.o: foo2.c
	gcc -c foo2.c
###改写后各种依赖更清楚
###只要有一项更改,重新make只重新编译更新的部分,其他不变
[free@hurd]$ make
gcc -c main.c
gcc -c foo1.c
gcc -c foo2.c
gcc main.o foo1.o foo2.o -o main
[free@hurd]$ ./main
This is foo1!
This is foo2!
This is main!
###成功执行,继续改进Makefile
[free@hurd]$ vim Makefile
target= main
src= $(shell find . -name "*.c")
obj= $(src:%.c=%.o)

$(target): $(obj)
    gcc $^ -o $@

%.o: %.c
    gcc -c $< -o $@
###这里引入了变量,其实和C语言中的宏类似
###$(shell ****)使用了shell命令,将搜索到的结果传给src
###$(src:%.c=%.o)是进行字符替换,将所有的*.c字符串替换成*.o字符串,传给obj
###$^:所有依赖目标的集合
###$@:目标集合
###$<:第一个依赖目标. 如果依赖目标是多个, 逐个表示依赖目标
###改进后,更加精炼清晰,而且方便以后拓展构建
[free@hurd]$ make
gcc -c foo2.c -o foo2.o
gcc -c main.c -o main.o
gcc -c foo1.c -o foo1.o
gcc foo2.o main.o foo1.o -o main
[free@hurd]$ ./main
This is foo1!
This is foo2!
This is main!
###成功执行,进一步改进Makefile
[free@hurd]$ vim Makefile
CC = gcc
CFLAGS = -Wall
CXXFLAGS =
CPPFLAGS =
LDFLAGS =
target = main
src = $(shell find . -name "*.c")
obj = $(src:%.c=%.o)

$(target): $(obj)
    $(CC) $^  $(CFLAGS) -o $@

%.o : %.c
    $(CC) -c $< $(CFLAGS) $(CPPFALGS) -o $@

.PHONY:clean
clean:
    -rm -rf $(obj) $(target)

install:
    mkdir target
    mv main target

test:
    @echo $(src)
    @echo $(obj)
###改进后基本看不到源文件的影子,慢慢走向通用化更易于拓展
###此处还添加了伪目标
###.PHONY意思表示clean是一个"伪目标";而在rm命令前面加了一个小减号的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。当然,clean的规则不要放在文件的开头,不然,这就会变成make的默认目标,相信谁也不愿意这样。不成文的规矩是——"clean从来都是放在文件的最后"
###"伪目标"并不是一个文件,只是一个标签,由于“伪目标”不是文件,所以make无法生成它的依赖关系和决定它是否要执行。只有通过显示地指明这个“目标”才能让其生效。当然,“伪目标”的取名不能和文件名重名,不然其就失去了“伪目标”的意义了
###为了避免和文件重名的这种情况,可以使用一个特殊的标记".PHONY"来显示地指明一个目标是“伪目标”,向make说明,不管是否有这个文件,这个目标就是"伪目标"

Makefile 命令前缀

不用前缀 :: 输出执行的命令以及命令执行的结果, 出错的话停止执行
前缀 @ :: 只输出命令执行的结果, 出错的话停止执行
前缀 - :: 命令执行有错的话, 忽略错误, 继续执行
例如上面的-rm、@echo
make 退出码
0 :: 表示成功执行
1 :: 表示make命令出现了错误
2 :: 使用了 "-q" 选项, 并且make使得一些目标不需要更新

make 常用参数

参数 含义
–debug[=] 输出make的调试信息, options 可以是 a, b, v
-j –jobs 同时运行的命令的个数, 也就是多线程执行 Makefile,比如常用的make -j8
-r –no-builtin-rules 禁止使用任何隐含规则
-R –no-builtin-variabes 禁止使用任何作用于变量上的隐含规则
-B –always-make 假设所有目标都有更新, 即强制重编译

命令变量 和 命令参数变量
变量名 含义
RM rm -f
AR ar
CC cc
CXX g++
ARFLAGS AR命令的参数
CFLAGS C语言编译器的参数
CXXFLAGS C++语言编译器的参数
CPPFLAGS 预处理阶段的参数
LDFLAGS 传递给连接器的参数
LIBS 链接库的参数

[free@hurd]$ vim Makefile
all:
    @echo $(RM)
    @echo $(AR)
    @echo $(CC)
    @echo $(CXX)
[free@hurd]$ make
rm -f
ar
cc
g++

自动变量
自动变量 含义
@@ 目标集合 % 当目标是函数库文件时, 表示其中的目标文件名
<.,< 第一个依赖目标. 如果依赖目标是多个, 逐个表示依赖目标 ? 比目标新的依赖目标的集合
,^ 所有依赖目标的集合, 会去除重复的依赖目标 + 所有依赖目标的集合, 不会去除重复的依赖目标
$* 这个是GNU make特有的, 其它的make不一定支持
  Makefile中一些约定俗成的伪目标
伪目标 含义
all 所有目标的目标,其功能一般是编译所有的目标
clean 删除所有被make创建的文件
install 安装已编译好的程序,其实就是把目标可执行文件拷贝到指定的目录中去
print 列出改变过的源文件
tar 把源程序打包备份. 也就是一个tar文件
dist 创建一个压缩文件, 一般是把tar文件压成Z文件. 或是gz文件
TAGS 更新所有的目标, 以备完整地重编译使用
check 或 test 一般用来测试makefile的流程
  以上只是简单介绍了 Makefile,足以应付一些小项目,但当遇到一些工程级的大项目手写 Makefile 变得十分困难繁琐,而且不难以跨平台,我们需要其他的工具来生成 Makefile。

CMake是一个比make更高级的编译配置工具,它可以根据不同平台、不同的编译器,生成相应的Makefile或者vcproj项目。通过编写CMakeLists.txt,可以控制生成的Makefile,从而控制编译过程。CMake自动生成的Makefile不仅可以通过make命令构建项目生成目标文件,还支持安装(make install)、测试安装的程序是否能正确执行(make test,或者ctest)、生成当前平台的安装包(make package)、生成源码包(make package_source)、产生Dashboard显示数据并上传等高级功能,只要在CMakeLists.txt中简单配置,就可以完成很多复杂的功能,包括写测试用例。如果有嵌套目录,子目录下可以有自己的CMakeLists.txt。

在 linux 平台下使用 CMake 生成 Makefile 并编译的流程如下:
编写 CMake 配置文件 CMakeLists.txt
执行命令 cmake PATH 或者 ccmake PATH 生成 Makefile 。其中, PATH 是 CMakeLists.txt 所在的目录
使用 make 命令进行编译。

示例

###这是一个简单的C快速排序程序
[free@hurd]$ vim test.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void swap(int *x, int *y)
{
    int tmp;
    tmp = *x;
    *x  = *y;
    *y  = tmp;

    return ;
}

void display_arr(int *arr, int len)
{
    int i;
    for(i = 0; i < len; i++)
    {
        printf("%d\t", arr[i]);
        if((i + 1) % 10 == 0)
            printf("\n");
    }
    printf("\n");
    return ;
}

void genereate(int *arr, int len, int maxnum)
{
    int i;
    srand(time(NULL));
    
    for(i = 0; i < len; i++)
    {
        arr[i] = rand()%maxnum;
    }

    return ;
}

void sort(int *arr, int left, int right)
{
    if(left < right)
    {
        int i, j;
        i = left + 1;
        j = right;
        while(i < j)
        {
            if(arr[i] > arr[left])
            {
                swap(&arr[i], &arr[j]);
                j--;
            }
            else
                i++;
        }

        if(arr[i] >= arr[left])
            i--;
        swap(&arr[left], &arr[i]);

        sort(arr, left, i);
        sort(arr, j, right);
    }
}

int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        fprintf(stderr,"\e[1;31mUsage: ./a.out len maxnum\e[0m");
        exit(1);
    }

    int len, maxnum;
    len = atoi(argv[1]);
    maxnum = atoi(argv[2]);
    if(len <= 0)
    {
        fprintf(stderr,"\e[1;31mThe array of length error\e[0m\n");
        exit(1);
    }
    if(maxnum <= 0)
    {
        fprintf(stderr, "\e[1;31mThe maxnum is error\e[0m\n");
        exit(1);
    }
    int arr[len];
    printf("\e[1;32m排序前的随机数组: \e[0m\n");
    genereate(arr, len, maxnum);
    display_arr(arr, len);
    printf("\e[1;32m排序后的有序数组: \e[0m\n");
    sort(arr, 0, len - 1);
    display_arr(arr, len);
    exit(0);
}
###编写CMakeLists.txt
[free@hurd]$ vim CMakeLists.txt
# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8)
# 项目信息
project (cmake_test)
# 指定生成目标
add_executable(test test.c)

###CMakeLists.txt 的语法比较简单,由命令、注释和空格组成,其中命令是不区分大小写的。符号 # 后面的内容被认为是注释。命令由命令名称、小括号和参数组成,参数之间使用空格进行间隔
###cmake_minimum_required:指定运行此配置文件所需的 CMake 的最低版本;
###project:参数值是 cmake_test,该命令表示项目的名称是 cmake_test
###add_executable:将名为 test.c 的源文件编译成一个名称为 test 的可执行文件
[free@hurd]$ cmake .
-- The C compiler identification is GNU 4.8.4
-- The CXX compiler identification is GNU 4.8.4
-- 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: /home/free/cmake

###得到生成的Makefile,并执行make
[free@hurd]$ make
Scanning dependencies of target test
[100%] Building C object CMakeFiles/test.dir/test.c.o
Linking C executable test
[100%] Built target test

[free@hurd]$ ./test 20 255
排序前的随机数组:
71	130	200	244	31	149	206	241	4	34	
127	11	251	167	233	68	250	231	247	14	

排序后的有序数组:
4	11	14	31	34	68	71	127	130	149	
167	200	206	231	233	241	244	247	250	251	
###下面对test.c进行拆分,将单独的函数抽取出来
[free@hurd]$ ls
CMakeCache.txt  CMakeFiles  cmake_install.cmake  CMakeLists.txt  display_arr.c  genereate.c  Makefile  sort.c  swap.c  test  test.c
[free@hurd]$ vim CMakeLists.txt
# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8)
# 项目信息
project (cmake_test)
# 指定生成目标
add_executable(test test.c display_arr.c  genereate.c sort.c  swap.c)
###唯一的改动只是在 add_executable 命令中增加了其他的 .c源文件
###如果源文件很多,把所有源文件的名字都加进去将是一件烦人的工作;更省事的方法是使用 aux_source_directory 命令,该命令会查找指定目录下的所有源文件,然后将结果存进指定变量名
###可以做如下修改
[free@hurd]$ vim CMakeLists.txt
# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8)
# 项目信息
project (cmake_test)
# 查找当前目录下的所有源文件
# 并将名称保存到 DIR_SRCS 变量
aux_source_directory(. DIR_SRCS)
# 指定生成目标
add_executable(test ${DIR_SRCS})

###CMake 会将当前目录所有源文件的文件名赋值给变量 DIR_SRCS ,再指示变量 DIR_SRCS 中的源文件需要编译成一个名称为 test 的可执行文件
[free@hurd]$ cmake .
-- Configuring done
-- Generating done
-- Build files have been written to: /home/free/cmake
[free@hurd]$ make
Scanning dependencies of target test
[ 20%] Building C object CMakeFiles/test.dir/display_arr.c.o
[ 40%] Building C object CMakeFiles/test.dir/test.c.o
[ 60%] Building C object CMakeFiles/test.dir/swap.c.o
[ 80%] Building C object CMakeFiles/test.dir/genereate.c.o
[100%] Building C object CMakeFiles/test.dir/sort.c.o
Linking C executable test
[100%] Built target test