一、前言
Make工具最主要也是最基本的功能就是通过makefile文件来描述源程序之间的相互关系并自动维护编译工作。而makefile 文件需要按照某种语法进行编写,文件中需要说明如何编译各个源文件并连接生成可执行文件,并要求定义源文件之间的依赖关系。
然而make的命令"博客精深",对于初学者来说,真是望而生畏,这篇文章不是make详解,只是讲解实用makefile的编写和使用。
在linux上,如果用gcc一个个编译源码,实在很繁琐,尤其是随着源代码的增加,这种繁琐更是明显,很多人,包括我,其实要求很简单,只要把头文件和cpp或者c文件放在当前目录或者其他目录,直接一个命令就可以编译了,但是make的命令太多了,相当打击初学者的信心。
常用的make指令其实不多,够用就行了,接下来开始讲解实用makefile的编写,看完之后大家可以直接下载make工程直接一个make命令完成编译。
本文相关环境:redhat 5.5 + g++ version 4.1.2 + GNU Make 3.81
g++可以支持cpp文件编译,gcc编译cpp文件在链接时会出现点问题,所以这里统一使用g++。
二、Makefile的规则
target ... : prerequisites ...
command
...
...
comman如果和target不是同一行,需要在第二行键入\t再键入command.
target也就是一个目标文件,可以是Object File,也可以是执行文件。还可以是一个标签(Label)
这是一个文件的依赖关系,也就是说,target这一个或多个的目标文件依赖于prerequisites中的文件,其生成规则定义在command中。
prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。这就是Makefile的规则。也就是Makefile中最核心的内容。
三、使用make一个个编译源代码
假设我们写一个简单的程序,源码位于/usr/make/main.cpp,代码如下:
//main.cpp
#include <stdio.h>
int main(int argc, char** argv) {
printf("app startup\n");
printf("app stop\n");
return 0;
}
make的最大好处是自动化编译,于是我们新建/usr/make/makefile,makefile文件内容如下:
main: main.o
g++ main.o -o main
main.o: main.cpp
g++ -c main.cpp -o main.o
clean:
rm -rf *.o main
保存之后,直接执行make,就可以生成main.o目标文件,main可执行文件了。
然而我们这个程序实在单薄了点,于是我们需要加入一个App类,控制整个程序的启动和关闭周期。
新建/usr/make/app.h文件,代码如下:
#ifndef APP_H
#define APP_H
class App{
public:
static App& getInstance();
bool start();
bool shutdown();
private:
App();
App(const App&);
App& operator=(const App&);
bool m_stopped;
};
#endif
新建/usr/make/app.cpp文件,代码如下:
#include "app.h"
#include <stdio.h>
#include <unistd.h>
App& App::getInstance() {
static App app;
return app;
}
App::App() {
m_stopped = false;
}
bool App::start() {
printf("app startup\n");
while (!m_stopped) {
printf("app run\n");
sleep(5);
}
return true;
}
bool App::shutdown() {
if (m_stopped == false) {
m_stopped = true;
}
return true;
}
修改/usr/make/main.cpp文件,代码如下:
//main.cpp
#include <stdio.h>
#include "app.h"
int main(int argc, char** argv) {
App& app = App::getInstance();
if(!app.start()) {
printf("app start fail\n");
}
app.shutdown();
return 0;
}
修改/usr/make/makefile,内容如下:
main: main.o app.o
g++ main.o app.o -o main
main.o: main.cpp
g++ -c main.cpp -o main.o
app.o: app.cpp
g++ -c app.cpp -o app.o
clean:
rm -rf *.o main
执行make,./main,发现后台会每隔5秒打印出"app run",只能ctrl+c结束.
以后我们修改了main.cpp,app.cpp,app.h文件后,我们只要输入make,就可以自动编译了,不必每次一个个g++命令去进行繁杂的编译过程。
这种方式,以后每添加一个源码文件,我们就要手工修改makefile的内容,添加依赖和命令,仍然显得不自动。
四、使用make自动推导编译
GNU的make很强大,它可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个[.o]文件后都写上类似的命令,因为,我们的make会自动识别,并自己推导命令。
我们的是新的makefile又出炉了:
CPP_SOURCES = $(wildcard *.cpp)
CPP_OBJS = $(patsubst %.cpp, %.o, $(CPP_SOURCES))
default:compile
$(CPP_OBJS):%.o:%.cpp
g++ -c $< -o $@
compile: $(CPP_OBJS)
g++ $^ -o main
clean:
rm -f $(CPP_OBJS)
rm -f main
下面来一步步解析这个makefie的语法.
初始化:
wildcard函数功能是展开成一列所有符合由其参数描述的文件名,文件间以空格间隔,本例是产生一个所有以 '.cpp' 结尾的文件的列表,然后存入变量 CPP_SOURCES.
patsubst函数是匹配替换的函数,有三个参数,第一个是一个需要匹配的式样,第二个表示用什么来替换它,第三个是一个需要被处理的由空格分隔的字列,本例是把
CPP_SOURCES的后缀为cpp文件列表,替换成后缀为o的文件列表。
生成目标文件
$(CPP_OBJS):%.o:%.cpp
g++ -c $< -o $@
上面的例子中,指明了我们的目标从$(CPP_OBJS)中获取,"%.o"表明要所有以".o"结尾的目标,也就是"main.o app.o",也就是变量$(CPP_OBJS)集合的模式.
而依赖模式"%.cpp"则取模式"%.o"的"%",也就是"main app",再加上后缀"cpp".于是,我们的依赖目标就是"main.cpp app.cpp"。
而命令中的"$<"和"$@"则是自动化变量,"$<"表示所有的依赖目标集(也就是"main.cpp app.cpp"),"$@"表示目标集(也就是"main.o cpp.o")。
展开之后是:
main.o: main.cpp
g++ -c main.cpp -o main.o
main.o: app.cpp
g++ -c app.cpp -o app.o
链接
上述的default目标是是makefile的第一个目标,依赖compile,compile文件不存在,也不会出现,所以会执行compile目标。
default和compile只是makefile一个普通的规则而已,没有特殊之处。
compile: $(CPP_OBJS)
g++ $^ -o main
"$<"表示所有的依赖目标集,表示main.o app.o
展开之后是
compile: main.o app.o
g++ main.o app.o -o main
这种方式,当我们在当前目录添加cpp文件,不用再改makefile,只要键入命令make就可以完成整个编译过程,这个时候我们可以感受到make的自动化编译的方便。
五、实用makefile的完善
虽然我们已经使用了makefile的自动推导,但是这个makefile还有有些不足:
1.只能支持cpp文件,不支持c文件
2.生成的目标文件全放在当前目录,造成当前目录混乱
3.h文件和cpp文件或者c文件没有分离,造成目录里的源码文件2倍爆炸,而且也不支持多目录
4.不支持第三方的库文件,包括include文件和lib文件。
于是,有一个完善的makefile如下:
TARGET = main
OBJ_PATH = objs
CC = g++
CFLAGS = -Wall -Werror -g
LINKFLAGS =
#INCLUDES = -I include/myinclude -I include/otherinclude1 -I include/otherinclude2
INCLUDES = -I include
#SRCDIR =src/mysrcdir src/othersrc1 src/othersrc2
SRCDIR = src
#LIBS = -Llib -lcurl -Llib -lmysqlclient -Llib -llog4cpp
LIBS =
C_SRCDIR = $(SRCDIR)
C_SOURCES = $(foreach d,$(C_SRCDIR),$(wildcard $(d)/*.c) )
C_OBJS = $(patsubst %.c, $(OBJ_PATH)/%.o, $(C_SOURCES))
CPP_SRCDIR = $(SRCDIR)
CPP_SOURCES = $(foreach d,$(CPP_SRCDIR),$(wildcard $(d)/*.cpp) )
CPP_OBJS = $(patsubst %.cpp, $(OBJ_PATH)/%.o, $(CPP_SOURCES))
default:init compile
$(C_OBJS):$(OBJ_PATH)/%.o:%.c
$(CC) -c $(CFLAGS) $(INCLUDES) $< -o $@
$(CPP_OBJS):$(OBJ_PATH)/%.o:%.cpp
$(CC) -c $(CFLAGS) $(INCLUDES) $< -o $@
init:
$(foreach d,$(SRCDIR), mkdir -p $(OBJ_PATH)/$(d);)
compile:$(C_OBJS) $(CPP_OBJS)
$(CC) $^ -o $(TARGET) $(LINKFLAGS) $(LIBS)
clean:
rm -rf $(OBJ_PATH)
rm -f $(TARGET)
install: $(TARGET)
cp $(TARGET) $(PREFIX_BIN)
uninstall:
rm -f $(PREFIX_BIN)/$(TARGET)
rebuild: clean compile
这个makefile的功能是扫描src文件夹的c文件和cpp文件,进行编译,编译的命令是g++,编译选项是-Wall -Werror -g,表示尽可能打印出报错信息,有错误和警告就停止编译,并且生成调试信息。如果是生产环境,请把-g去掉。最后连接所有目标文件为main。如果所有的c文件和cpp文件没有main函数入口,链接会报错。
使用这个makefile,需要注意的几个变量是:
TARGET 表示最后生成的可执行文件的名字,默认生成的main可执行文件在当前工作目录。
OBJ_PATH 表示编译过程中产生的目标文件放在那个目录,默认生成放在在当前目录的objs目录下,无需手工mkdir,自动生成。
CC 默认是g++,如果是使用gcc的,请修改,但是gcc在编译cpp文件后,链接时会出现问题,请自行解决。
INCLUDES 配置头文件的目录,默认是include目录,如果放在当前目录,可以改为"INCLUDES = .",如果要配置多个include目录,可以按照注释,自行配置。
SRCDIR 配置c文件和cpp文件的目录,默认是src目录,如果喜欢放在当前目录,可以改为"SRCDIR= .",如果要配置多个源码目录,可以按照注释,自行配置。
LIBS 配置第三方的动态链接库和静态链接库,当程序使用了第三方库时,我们编译是要配置第三方的头文件目录,链接时就要配置链接的库文件,LIBS配置可以参考注释,如果不清楚,请看本人博客里的“gcc常用命令”文章