Makefile,Linux程序员必须要会的一项技能;当然,并不是说要“知根知底”,懂得一些常用用法即可!
目录
三、Makefile编译子目录的Makefile(多路径编译)
3. src中的Makefile(也就是主Makefile)
一、Makefile简介
Makefile的注释:
# 这是一个注释
使用 “ # ” 即可;
Makefile的简单用法:
目标文件 : 依赖文件
[TAB]命令
- [TAB]:是你键盘左边tab键;
- 目标文件:要生成的文件;
- 依赖文件:生成目标文件要依赖的文件;
- 命令:执行的命令,生成目标文件。
准备一份代码,例如:
main.c
#include <stdio.h>
int main(int argc, char **argv) {
printf("hello world\n");
return 0;
}
我们正常在Linux中编译,一般都是这样去编译:
gcc main.c -o main
也可以新建一个文件,命名为Makefile,并写如下命令:
main: main.o
gcc main.o -o main
main.o: main.c
gcc -c main.c -o main.o
# -o mian.o 可以省略
-c 是生成 .o 文件的必要参数!
写好后,直接make就可以编译了,且生成main可执行文件!
Makfile执行顺序:
首先是mian目标文件,要生成目标文件得依赖main.o文件;此时还没有main.o文件,Makefile会往下找,找到了main.o目标文件,其依赖main.c文件;main.c文件刚好有,然后就会调用下面那条命令( gcc -c main.c -o main.o )去生成main.o文件;有了main.o文件,也就可以生成main目标文件了;Makefile紧接着会执行命令( gcc main.o -o main )去生成main目标文件!
也就是说,是先执行gcc -c main.c -o main.o,然后在执行gcc main.o -o main
二、高难度点的Makfile用法
1. 准备以下项目文件
circle.h
#include <stdio.h>
void test2();
circle.cpp
#include "circle.h"
void test2() {
printf("test2()\n");
}
cube.h
#include <iostream>
void test22();
cube.cpp
#include "cube.h"
void test22() {
std::cout << "test22()" << std::endl;
}
main.cpp
#include <iostream>
#include "cube.h"
#include "circle.h"
int main(int argc, char **argv) {
std::cout << "hello world" << std::endl;
test2();
test22();
return 0;
}
他们都是在同一个路径下!
2. 显示规则
跟上面举的例子差不多,只是这个例子多了几个文件,所以要多些几条命令。
什么是显示规则,就是命令都得正确明了的写在Makefile上。
test2: main.o circle.o cube.o
g++ main.o circle.o cube.o -o test2
main.o: circle.h cube.h main.cpp
g++ -c main.cpp -o main.o
circle.o: circle.h circle.cpp
g++ -c circle.cpp -o circle.o
cube.o: cube.h cube.cpp
g++ -c cube.cpp -o cube.o
.PHONY: clean
clean:
rm -fr main.o circle.o cube.o
cleanall:
rm -fr main.o circle.o cube.o test2
test2是要生成的目标可执行文件,它依赖main.o、circle.o、cube.o;
所以main.o、circle.o、cube.o都得为其写上命令去生成,他们各自都有各自的依赖文件,依据依赖文件去生成;例如cube.o依赖cube.h和cube.cpp;
那么下面那些是什么?
伪目标
先不看 .PHONY: clean
先看 clean:
他也可以是一个目标,不过是伪目标;当触发clean后,会执行其下指定的命令:rm -fr main.o circle.o cube.o
一般常用来删除上面生成的.o文件和可执行文件!cleanall也是如此。
用法:
make clean
make cleanall
.PHONY: clean 是什么?
可以说是用来屏蔽同名clean的文件夹或文件;如果相同路径下有一个clean的文件夹或者文件,那么Makefile执行make clean会失败!
3. 变量
= :变量(可修改)
+= :追加
:= :常量(不可修改)
例如:
TARGET = test
CXX := g++
使用:
$(TARGET) # 相当于C语言的宏定义
使用英文的美元$符号为开头,再用括号括起来即可!
那么现在可以使用变量对上面的Makefile做完善一下:
CXX := g++
TARGET = test2
OBJ = main.o cube.o circle.o
$(TARGET): $(OBJ)
$(CXX) $(OBJ) -o $(TARGET)
main.o: cube.h circle.h main.cpp
$(CXX) -c main.cpp -o main.o
cube.o: cube.h cube.cpp
$(CXX) -c cube.cpp -o cube.o
circle.o: circle.h circle.c
$(CXX) -c circle.c -o circle.o
.PHONY: clean
clean:
rm -fr $(OBJ)
cleanall:
rm -fr $(OBJ) $(TARGET)
文件开头定义了三个变量,后续写的命令都是用这三个变量去替换掉了,执行make时,会自动识别替换回来,相当于C语言的宏替换!
4. 隐含规则
%c、%cpp、%o : 任意的.c 或 任意的.cpp 或 任意的.o
*.c、*.cpp、*.o :所有的.c 或 所有的.cpp 或 所有的.o
使用%c 或 %cpp 或 %o 或 %h 时,优先加点'.'使用,如:%.c、%.cpp、%.o、%.h;
如果使用某个编译时报错了,再把点'.'去掉即可!
知道了这些隐含规则,就可以使用他们再对Makefile做优化了:
CXX := g++
TARGET = test2
OBJ = main.o cube.o circle.o
$(TARGET): $(OBJ)
$(CXX) $(OBJ) -o $(TARGET)
%.o: %.h %cpp
$(CXX) -c %.cpp -o %.o
.PHONY: clean
clean:
rm -fr *.o
cleanall:
rm -fr *.o $(TARGET)
5. 通配符
$^ | 所有的依赖文件 |
$< | 所有依赖文件的第一个文件(应该也是最匹配的一个文件) |
$@ | 所有的目标文件 |
依赖文件:我要做这个操作,依赖哪些东西;
目标文件:我要做这个操作,要生成的东西;
使用通配符再对Makefile做最后的完善:
CXX := g++
TARGET = test2
OBJ = main.o cube.o circle.o
RMRF := rm -rf
$(TARGET): $(OBJ)
$(CXX) $^ -o $@
%.o: %.h %cpp
$(CXX) -c $< -o $@
.PHONY: clean
clean:
$(RMRF) *.o
cleanall:
$(RMRF) *.o $(TARGET)
具体怎么使用,要放在哪个位置上呢?
如果还不懂,那就这样去想,一条命令,哪个位置是写依赖文件的,就放$^;哪个位置是写生成的目标文件的,就放$@;至于$<,可以-c后面去放!
6. 更加高级的用法
CXX := g++
TARGET = test2
#OBJ = main.o cube.o circle.o
RMRF := rm -rf
SRC = $(wildcard *.cpp) # 获取当前路径下的所有.cpp文件
OBJ = $(patsubst %.cpp, %.o, $(SRC)) # 把$(SRC)中符合.cpp的所有文件转换成.o文件
CXXFLAGS = -c -Wall
$(TARGET): $(OBJ)
$(CXX) $^ -o $@
%.o: %cpp
$(CXX) $(CXXFLAGS) $< -o $@
.PHONY: clean
clean:
$(RMRF) *.o
cleanall:
$(RMRF) *.o $(TARGET)
SRC = $(wildcard *.cpp) : 获取项目路径下的所有.cpp源文件;
OBJ = $(patsubst %.cpp, %.o, $(SRC)) : 根据源文件链接成 .o 文件;
wildcard 和 patsubst 是Makefile函数的用法,具体我也不是很懂,反正就是这么使用就对了!
注释是我加上的,我想应该是这样解释的!
7. 可以简写命令
也就是不用写命令都可以,Makefile会自动识别生成命令!
CXX := g++
TARGET = test2
OBJ = main.o cube.o circle.o
RMRF := rm -rf
$(TARGET): $(OBJ)
#$(CXX) $^ -o $@
%.o: %.h %cpp
#$(CXX) -c $< -o $@
.PHONY: clean
clean:
$(RMRF) *.o
cleanall:
$(RMRF) *.o $(TARGET)
三、Makefile编译子目录的Makefile(多路径编译)
在原项目的基础上,新建文件夹circle和cube和src;将circle.h和circle.cpp拷贝到circle;将cube.h和cube.cpp拷贝到cueb;将main.cpp拷贝src;并在每个文件夹内都创建一个Makefile。
我们现在的需求是,在src中的Makefile,执行make命令后,会自动编译circle和cube中的Makefile,并生成各自的.a静态库;然后再进行编译链接,最后生成可执行文件!
1. circle中的Makefile
libcircle.a: circle.o
ar crv libcircle.a circle.o # 编译链接成.a静态库
circle.o: circle.cpp
gcc -c circle.cpp
.PHONY: clean
clean:
rm libcircle.a circle.o
2. cube中的Makefile
libcube.a: cube.o
ar crv libcube.a cube.o # 编译链接成.a静态库
cube.o: cube.cpp
gcc -c cube.cpp
.PHONY: clean
clean:
rm libcube.a cube.o
3. src中的Makefile(也就是主Makefile)
GXX = g++
TARGET = test2
ALL_C = $(wildcard ./*.cpp) # 获取当前路径下的所有.cpp文件
C_OBJ = $(notdir $(ALL_C)) # 去掉文件的绝对路径,只保留文件名
O_OBJ = $(patsubst %.cpp,%.o,$(C_OBJ)) # 把$(C_OBJ)中符合.cpp的所有文件转换成.o文件
CIRCLE := ../circle/
CUBE := ../cube/
.PHONE: all # 将all 设置成伪目标,all会第一个执行 ,但不会生成目标,依赖的目标会依次执行
all: libcircle.a libcube.a $(TARGET) #依赖的目标 libcircle.a 、libcube.a 、 test2
libcircle.a:
$(MAKE) -C $(CIRCLE) # 进入指定路径,执行路径中的Makefile,然后回到当前目录。 $(MAKE) -C (路径)
libcube.a:
$(MAKE) -C $(CUBE)
$(TARGET):$(C_OBJ) #编译当前目录下的.cpp 生成目标程序
$(GXX) $^ -L $(CIRCLE) -L $(CUBE) -I $(CIRCLE) -I $(CUBE) -lcircle -lcube -o $@
.PHONY: clean cleanall
clean:
rm -fr $(TARGET) $(CIRCLE)*.o $(CIRCLE)*.a $(CUBE)*.o $(CUBE)*.a #清理子目录下的编译后产生的文件 ,当前目录下的目标文件
cleanall:
rm -fr $(TARGET) $(CIRCLE)*.o $(CIRCLE)*.a $(CUBE)*.o $(CUBE)*.a $(TARGET)
MAKE是Makefile内部定义的变量,获取当前Makfile的路径;具体可以使用命令 make -p 去查看;
具体就不解释了,拷贝下来,再根据自己的项目去修改就好了!注释那里也介绍的很清楚了!
最终会生成一条命令去执行:
g++ main.cpp -L ../circle/ -L ../cube/ -I ../circle/ -I ../cube/ -lcircle -lcube -o test2
四、Makefile链接第三方库编译
在上面第三步的基础上,新建include文件夹和lib文件夹;将自己需要链接的第三方库的头文件拷贝到include;将需要链接的第三方库文件拷贝到lib文件夹;
例如,之前学习了log4cpp库的用法,我这里就以这个库为例:(具体根据自己需要的库来使用)
然后修改main.cpp代码,增加日志的头文件等:(具体根据自己的库来使用)
#include <iostream>
#include "cube.h"
#include "circle.h"
#include "log4cpp/Category.hh"
#include "log4cpp/Appender.hh"
#include "log4cpp/FileAppender.hh"
#include "log4cpp/OstreamAppender.hh"
#include "log4cpp/Layout.hh"
#include "log4cpp/BasicLayout.hh"
#include "log4cpp/Priority.hh"
#include "log4cpp/PatternLayout.hh"
int main(int argc, char **argv) {
std::cout << "hello world" << std::endl;
test2();
test22();
/* 1.日志输出到控制台 */
{
log4cpp::Appender *appender1 = new log4cpp::OstreamAppender("console", &std::cout);
appender1->setLayout(new log4cpp::BasicLayout()); // 默认配置
log4cpp::Category& root = log4cpp::Category::getRoot();
root.setPriority(log4cpp::Priority::DEBUG);
root.addAppender(appender1);
/* 执行后,会在终端输出这句话*/
root.debug("This is log4cpp output log message!\n");
}
// 关闭日志
log4cpp::Category::shutdown();
return 0;
}
修改主Makefile:
GXX = g++
TARGET = test2
ALL_C = $(wildcard ./*.cpp) # 获取当前路径下的所有.cpp文件
C_OBJ = $(notdir $(ALL_C)) # 去掉文件的绝对路径,只保留文件名
O_OBJ = $(patsubst %.cpp,%.o,$(C_OBJ)) # 把$(C_OBJ)中符合.cpp的所有文件转换成.o文件
CIRCLE := ../circle/
CUBE := ../cube/
LOG4CPP_INCLUDE := ../include/ # 第三方库的头文件路径
LOG4CPP_LIB := ../lib/log4cpp # 第三方库的库路径
LIBS = -lpthread
LOG4CPP = -llog4cpp # 链接第三方库
.PHONE: all # 将all 设置成伪目标,all会第一个执行 ,但不会生成目标,依赖的目标会依次执行
all: libcircle.a libcube.a $(TARGET) #依赖的目标 libcircle.a 、libcube.a 、 test2
libcircle.a:
$(MAKE) -C $(CIRCLE) # 进入指定路径,执行路径中的Makefile,然后回到当前目录。 $(MAKE) -C (路径)
libcube.a:
$(MAKE) -C $(CUBE)
$(TARGET):$(C_OBJ) #编译当前目录下的.cpp 生成目标程序
$(GXX) $^ -L $(CIRCLE) -L $(CUBE) -L $(LOG4CPP_LIB) -I $(CIRCLE) -I $(CUBE) -I $(LOG4CPP_INCLUDE) -lcircle -lcube $(LOG4CPP) $(LIBS) -o $@
.PHONY: clean cleanall
clean:
rm -fr $(TARGET) $(CIRCLE)*.o $(CIRCLE)*.a $(CUBE)*.o $(CUBE)*.a #清理子目录下的编译后产生的文件 ,当前目录下的目标文件
cleanall:
rm -fr $(TARGET) $(CIRCLE)*.o $(CIRCLE)*.a $(CUBE)*.o $(CUBE)*.a $(TARGET)
最终会生成一条命令去执行:
g++ main.cpp -L ../circle/ -L ../cube/ -L ../lib/log4cpp -I ../circle/ -I ../cube/ -I ../include/ -lcircle -lcube -llog4cpp -lpthread -o test2
五、Makefile企业项目用法编译
个人觉得,企业项目中,会像下方这样去安排文件和编写Makefile去编译!
首先准备代码,编译成.so库
hello.h
#ifndef _HELLO_H_
#define _HELLO_H
void sayHello();
#endif
hello.cpp
#include "hello.h"
#include <iostream>
void sayHello() {
std::cout << "hello world!" << std::endl;
}
通过以下命令编译成动态库.so
g++ -c hello.cpp -o hello.o
ar -rsv libhello.so hello.o
将hello.h头文件拷贝到include文件夹中;将libhello.so拷贝到lib文件夹中;
文件目录如下:
.
├── include
│ ├── hello.h
│ └── log4cpp
│ ├── AbortAppender.hh
│ ├── Appender.hh
| .
| . # 文件太多,省略了。。。
| .
│ ├── TriggeringEventEvaluator.hh
│ └── Win32DebugAppender.hh
├── lib
│ ├── libhello.so
│ └── log4cpp
│ ├── liblog4cpp1.so
│ ├── liblog4cpp.a
│ ├── liblog4cpp.la
│ ├── liblog4cpp.so.5
│ └── liblog4cpp.so.5.0.6
└── src
├── circle
│ ├── circle.cpp
│ ├── circle.h
│ └── Makefile
├── cube
│ ├── cube.cpp
│ ├── cube.h
│ └── Makefile
├── main.cpp
└── Makefile
8 directories, 68 files
circle.h和circle.cpp调用了libhello.so库,然后他们自己再编译成.so库!
include/hello.h
#ifndef _HELLO_H_
#define _HELLO_H
void sayHello();
#endif
src/circle/circle.h
#include <stdio.h>
#include "hello.h"
void test2();
src/circle/circle.cpp (这里调用了libhello.so库中的sayHello()方法)
#include "circle.h"
void test2() {
printf("test2()\n");
// 调用libhello.so库中的方法
sayHello();
}
src/circle/Makefile
GXX := g++
HELLO_INCLUDE := ../../include/ # libhello.so库的头文件路径
HELLO_LIB := ../../lib/ # libhello.so库的路径
libcircle.so: circle.o
ar crv libcircle.so circle.o # 编译链接成.so动态库
circle.o: circle.cpp
$(GXX) -I $(HELLO_INCLUDE) -L $(HELLO_LIB) -lhello -c circle.cpp
.PHONY: clean
clean:
rm libcircle.so circle.o
crc/cube/cube.h
#include <iostream>
void test22();
crc/cube/cube.cpp
#include "cube.h"
void test22() {
std::cout << "test22()" << std::endl;
}
crc/cube/Makefile
GXX := g++
libcube.a: cube.o
ar crv libcube.a cube.o # 链接成.a静态库
cube.o: cube.cpp
$(GXX) -c cube.cpp
.PHONY: clean
clean:
rm libcube.a cube.o
src/main.cpp (这里调用了libhello.so库中的sayHello()方法)
#include <iostream>
#include "cube.h"
#include "circle.h"
#include "log4cpp/Category.hh"
#include "log4cpp/Appender.hh"
#include "log4cpp/FileAppender.hh"
#include "log4cpp/OstreamAppender.hh"
#include "log4cpp/Layout.hh"
#include "log4cpp/BasicLayout.hh"
#include "log4cpp/Priority.hh"
#include "log4cpp/PatternLayout.hh"
int main(int argc, char **argv) {
std::cout << "hello world" << std::endl;
test2();
test22();
// 注意,这里调用了libhello.so库的sayHello方法
sayHello();
/* 1.日志输出到控制台 */
{
log4cpp::Appender *appender1 = new log4cpp::OstreamAppender("console", &std::cout);
appender1->setLayout(new log4cpp::BasicLayout()); // 默认配置
log4cpp::Category& root = log4cpp::Category::getRoot();
root.setPriority(log4cpp::Priority::DEBUG);
root.addAppender(appender1);
root.debug("This is log4cpp output log message!\n");
}
// 关闭日志
log4cpp::Category::shutdown();
return 0;
}
src/Makefile
GXX = g++
TARGET = test3
ALL_C = $(wildcard ./*.cpp) # 获取当前路径下的所有.cpp文件
C_OBJ = $(notdir $(ALL_C)) # 去掉文件的绝对路径,只保留文件名
O_OBJ = $(patsubst %.cpp,%.o,$(C_OBJ)) # 把$(C_OBJ)中符合.cpp的所有文件转换成.o文件
CIRCLE := ./circle/
CUBE := ./cube/
HELLO_LIB := ../lib/ # 自定义库 libhello.so库的路径
LOG4CPP_INCLUDE := ../include/ # 第三方库的头文件路径
LOG4CPP_LIB := ../lib/log4cpp # 第三方库的库路径
LIBS = -lpthread
LOG4CPP = -llog4cpp # 链接第三方库
.PHONE: all # 将all 设置成伪目标,all会第一个执行 ,但不会生成目标,依赖的目标会依次执行
all: libcircle.so libcube.a $(TARGET) #依赖的目标 libcircle.so 、libcube.a 、 test2
libcircle.so:
$(MAKE) -C $(CIRCLE) # 进入指定路径,执行路径中的Makefile,然后回到当前目录。 $(MAKE) -C (路径)
libcube.a:
$(MAKE) -C $(CUBE) # 进入指定路径,执行路径中的Makefile,然后回到当前目录。 $(MAKE) -C (路径)
$(TARGET):$(C_OBJ) #编译当前目录下的.cpp 生成目标程序
$(GXX) $^ -L $(CIRCLE) -L $(CUBE) -L $(LOG4CPP_LIB) -L $(HELLO_LIB) -I $(CIRCLE) -I $(CUBE) -I $(LOG4CPP_INCLUDE) -lcircle -lcube -lhello $(LOG4CPP) $(LIBS) -o $@
.PHONY: clean cleanall
clean:
rm -fr $(TARGET) $(CIRCLE)*.o $(CIRCLE)*.so $(CUBE)*.o $(CUBE)*.a #清理子目录下的编译后产生的文件 ,当前目录下的目标文件
cleanall:
rm -fr $(TARGET) $(CIRCLE)*.o $(CIRCLE)*.so $(CUBE)*.o $(CUBE)*.a $(TARGET)
最终会生成一条命令去执行:
g++ main.cpp -L ./circle/ -L ./cube/ -L ../lib/log4cpp -L ../lib/ -I ./circle/ -I ./cube/ -I ../include/ -lcircle -lcube -lhello -llog4cpp -lpthread -o test3
六、总结
Makefile的基本用法已经介绍完了,我想的是,如果作为一名Linux程序员,懂得上面所介绍的用法也差不多了;如果是架构师或者项目经理级别的人物,当然得去更深入的去学习!