设计师网站pin,分销系统小程序开发,湖南营销型网站建设报价,搭建专业网站服务器makefile案例学习
很多时候#xff0c; 我们在git clone完一个project之后#xff0c;就会让我们使用make命令进行项目的构建。这个make命令的背后就是按照了Makefile文件定义的格式去完成项目构建。
因此Makefile的作用就是帮助程序员进行项目的构建#xff0c;它按照项目…makefile案例学习
很多时候 我们在git clone完一个project之后就会让我们使用make命令进行项目的构建。这个make命令的背后就是按照了Makefile文件定义的格式去完成项目构建。
因此Makefile的作用就是帮助程序员进行项目的构建它按照项目的需求个性化的定义自己的构建过程。Makefile并不限定编程语言但是在c/c项目中使用相对较多。其他的一些构建工具例如qmake也是将*.pro文件转化为Makefile再进行构建。
可以看出Makefile的应用面还是非常广泛的 下面将一步一步的讲解Makefile最常使用的语法 并通过案例进行实践 一步一步深入Makefile 本文的案例主要使用了C语言。
makefile的基本规则
Makefile的格式通常有如下两种
格式一
targets : prerequisitescommand...格式二
targets : prerequisites ; commandcommand...其中targets为目标文件 prerequisites为依赖文件 command为如果使用依赖文件构建出目标文件的命令。
格式一中command不与target:prerequisites在一行必须以Tab键开头。
格式二中, command和prerequisites在一行那么可以用分号做为分隔。
通常情况下 一般使用格式一 命令和依赖分开 比较清晰。
构建的逻辑如下所示
(1)如果发现目标文件不存在但是依赖文件存在就会执行命令集构建生成目标文件。
(2)如果发现目标文件不存在但是依赖文件也不存在那么就会寻找依赖文件的构建模块 尝试构建依赖文件 然后再构建目标文件。
(3)如果发现目标文件已经存在依赖文件也存在make指令会自动去比较两者的修改时间 依赖文件的最后修改时间晚于目标文件就会执行指令集合。 依赖文件的最后修改时间早于目标文件就不会执行指令集合。同时会提示目标文件已经是最新的。
(4)如果发现目标文件已经存在 依赖文件不存在那么makefile将会寻找依赖文件的构建模块并尝试构建依赖模块 由于依赖模块生成时间晚于目标文件 因此目标文件将会重新构建。
下面我们用过一些demo一步一步的深入Makefile。
下面是demo1 通过demo1来熟悉makefile的基本语法。
demo1第一个Makefile
demo1的目录结构如下所示
.
├── main.cpp
└── Makefile其中main.cpp如下
#include iostream
using namespace std;
int main ()
{cout Hello World endl;
}为其编写的Makefile如下
main:main.og main.o -o main
main.o:main.cppg -c main.cpp main.o
.PHONY : clean
clean:rm -rf *.o main首先看构建main对象main对象依赖于main.o对象main:main.o 因此需要完成main.o对象的构建。
接着看到main.o对象依赖于main.cpp文件 该文件存在与当前目录中 因此执行相应的命令g -c main.cpp。
main.o:main.cppg -c main.cpp如此之后 main.o对象构建成功这样就可以构建main对象于是执行了main对象的命令g main.o -o main。
至此main对象的构建完毕。
makefile的最后一部分是一个clean对象 用于清理生成的文件 使用make clean即可构建clean对象。 关于关键字.PHONY将在下面的例子中讲解。
.PHONY : clean
clean:rm -rf *.o main使用$ $ $^符号简化编写
在Makefile中可以使用一些预定好的符号来简化书写 例如$ $ $^其含义如下所示
$ 表示目标文件
$^ 表示所有的依赖文件
$ 表示第一个依赖文件
例如 main: main.cpp add.cpp
$指的就是main$指的就是main.cpp, $^指的就是main.cpp add.cpp
下面我们就使用它们来改动demo1中的makefile。
demo2使用$ $ $^ 简化书写
demo2的文件结构如下所示
.
├── main.cpp
└── Makefilemain.cpp如下
#include iostream
using namespace std;
int main ()
{cout Hello World endl;
}Makefile内容如下:
main:main.og $ -o $
main.o:main.cppg -c $ -o $
.PHONY : clean
clean:rm -rf *.o main首先看main目标的commandg $ -o $, $代表第一个依赖项即main.o $代表构建目标即main 因此该语句可以翻译成g main.o -o main。
main.o可以以此类推。
vpath和VPATH
vpath和VPATH主要作用是通过指定文件的搜索路径自动寻找源文件但是这种自动推导需要你将vpath/VPATH与$,$^结合使用。
VPATH是一个变量 其格式如下所示
VPATH PATH1:PATH2:PATH3将需要搜索的目录按照冒号分割。
vpath是一个关键字 有三种格式
1、vpath pattern directories
为符合模式pattern的文件指定搜索目录directories。
vpath %.c path1:path2其表示搜索.c结尾的文件先在path1目录搜索接着在path2目录搜索。
2、vpath pattern
清除符合模式pattern的文件的搜索目录。
3、vpath
清除所有已被设置好了的文件搜索目录。
第一个格式用于添加搜索路径 后两个格式用于清除搜索路径。
下面需要注意vpath/VPATH的一个使用误区 即vpath没有和$,$^结合使用 会有什么结果。
看下面的一个目录结构,
.
├── Makefile
└── src└── main.cpp所要编译的文件main.cpp在src目录下 我们使用VPATH指定了搜索路径src 我们在command直接指定了文件名 没有使用$,$^ 试问这样编写Makefile能正确编译吗
VPATHsrc
main.o:main.cppg -c main.cpp -o main.o答案是否定的 执行结果如下
g -c main.cpp -o main.o
cc1plus: fatal error: main.cpp: No such file or directory
compilation terminated因为此时已经手动指定了文件名称 Makefile没有能力去为这种场景做适配。
因此VPATH想要生效需要与$,$^配合 当搜索相应的目录找到对应的文件时 Makefile就会将,,,^替换为文件的相对路径。
下面通过demo3 来实践一下vpath/VPATH。
demo3使用vpath和VPATH指定依赖文件搜索路径
demo3的文件目录结构如下所示
.
├── inc
│ └── add.hpp
├── Makefile
└── src├── add.cpp└── main.cppmain.cpp
#include add.hppint main()
{int a 1;int b 2;int c my_add(a, b);return 0;
}add.cpp
#include add.hpp
int my_add(int a, int b)
{return a b;
}add.hpp
int my_add(int a, int b);Makefile
VPATH src:incmain : main.o add.og -o $ $^main.o : main.cpp add.hppg -c $ -I inc/add.o : add.cpp add.hppg -c $ -I inc/.PHONY: clean
clean:rm -rf *.o main组合使用VPATH和$之后 makefile自动推导出了语句在main.cpp前加上了src前缀。
g -c src/main.cpp -I inc/
g -c src/add.cpp -I inc/
g -o main main.o add.o使用内置函数wildcardpatsubst foreach notdir等函数帮助我们构建
makefile提供了一些内置函数帮助我们的构建过程更加自动化。
wilecard:
使用格式:
$(wildcard PATTERN...)它被展开为已经存在的、使用空格分开的、匹配此模式的所有文件列表。
例如下面的语句可以获取工作目录下的所有.c文件列表。
objects $(wildcard *.c) patsubst:
$(patsubst pattern,replacement,text ) 查找text中的单词单词以空格、“Tab或回车”换行分隔是否符合模式pattern如果匹配的话则以replacement替换。
下面的例子可以快速的生成一个目录下.c文件生成的.o文件。
首先使用wildcard函数获取工作目录下的.c文件列表之后将列表中所有文件名的后缀.c替换为.o。这样我们就可以得到在当前目录可生成的.o文件列表。
$(patsubst %.c,%.o,$(wildcard *.c))notdir:
用于去掉文件的绝对路径只保留文件名。
下面的例子用于去除sub目录的前缀。
file$(notdir $(wildcard ./sub/*.c)),foreach:
$(foreach ITEM, LIST, TEXT)实际上是一种循环 常用于遍历文件夹下的所有文件。
foreach函数的工作过程是把LIST中使用空格分割的单词依次取出并赋值给变量ITEM然后执行TEXT表达式。重复这个过程直到遍历完LIST中的最后一个单词。函数的返回值是TEXT多次计算的结果。
例如
dirs src src/math
srcs $(foreach dir, $(dirs), $(wildcard $(dir)/*.cpp))这段makefile就取出了src和src/math目录下所有的cpp文件
静态模式
静态模式可以更加容易地定义多目标的规则可以让我们的规则变得更加的有弹性和灵活。我们还是先来看一下语法
targets ... : target-pattern : prereq-patterns ...commands...targets定义了一系列的目标文件可以有通配符。是目标的一个集合。
target-pattern是指明了targets的模式也就是的目标集模式。
prereq-patterns是目标的依赖模式它对target-pattern形成的模式再进行一次依赖目标的定义。
下面的例子 将多个构建目标合并在了一个静态模式中
objects main.o add.oall: $(objects)$(objects): %.o: %.cpp$(CXX) -c $(CFLAGS) $ -o $其中
$(objects): %.o: %.cpp$(CXX) -c $(CFLAGS) $ -o $相当于
main.o : main.c$(CXX) -c $(CFLAGS) main.cpp -o main.o
add.o : add.cpp$(CXX) -c $(CFLAGS) add.cpp -o add.omakefile自动生成依赖
在讲解makefile自动生成依赖之前,先给出本节中例子的目录结构和文件内容:
首先给出本节中例子的目录结构:
.
├── add.hpp
├── main.cpp
├── Makefilemain.cpp内容如下:
#include iostream
#include add.hppint main()
{int a 1;int b 2;int c my_add(a, b);return 0;
}add.hpp内容如下:
int my_add(int a, int b)
{return a b;
}在Makefile中我们的依赖关系可能会需要包含一系列的头文件比如如果我们的main.cpp中有一句#include add.hpp 那么我们的依赖关系应该是
main.o : main.cpp add.hpp但是使用一些模式匹配的方法是不能够自动将这些依赖的头文件也包含进去的 例如下面的语句:
%.o: %.cppg -c $ -o $这就意味着, 如果add.hpp文件添加了内容并不会使得main.o重新构建。这不是我们所期望的。
这里就需要我们借助gcc/g的 -MM参数自动生成依赖 例如g -MM main.cpp的输出则是:
main.o: main.c add.hppGNU组织建议把编译器为每一个源文件的自动生成的依赖关系放到一个文件中为每一个.cpp的文件都生成一个.d的Makefile文件然后再使用include将.d的依赖关系添加进来 例如
main: main.o main.dg $ -o $
%.o: %.cppg -c $ -o $
%.d: %.cppg -MM $include main.dmain.d的内容如下
main.d
main.o: main.cpp add.hpp将include的内容展开 等效的makefile文件如下所示:
main: main.o main.dg $ -o $
main.o: main.cppg -c $ -o $
main.d: main.cppg -MM $main.o: main.cpp add.hpp可以看出展开后在该makefile中,出现了两个main.o的目标。
对于这种多规则同目标文件是有描述的
在Makefile中一个文件可以作为多个规则的目标出现。这种情况时此目标文件的所有依赖文件将会被合并成此目标一个依赖文件列表其中任何一个依赖文件比目标更新比较目标文件和依赖文件的时间戳时 make将会执行特定的命令来重建这个目标。
对于一个多规则的目标重建此目标的命令只能出现在一个规则中可以是多条命令。如果多个规则同时给出重建此目标的命令make将使用最后一个规则所以的命令同时提示错误信息一个特殊的例外是使用.开头的多规则目标文件可以在多个规则中给出多个重建命令。这种方式只是为了和其他版本make进行兼容一般在GNU make中应该避免使用这个功能。
因此上述含有多目标的makefile可以转化为如下的makefile:
main: main.o main.dg $ -o $
main.o: main.cpp add.hppg -c $ -o $
main.d: main.cppg -MM $到此, 当我们修改add.hpp时 main.o会重新构建。 这似乎已经很完美了但是真的如此吗
我们试想此时在add.hpp中添加新的依赖sub.hpp 因为main.cpp依赖于add.hpp因此main.o会重新编译。 但是由于main.d文件只依赖于main.cpp, 因此main.d不会重新生成, 因此当我们这个时候修改sub.hpp时, main.cpp并不会更新。
因此这就要求main.d 也要添加对头文件的依赖。
修改上面的makefile
main: main.o main.dg $ -o $
%.o: %.cppg -c $ -o $
%.d: %.cppset -e; rm -f $; \g -MM $ $.$$$$; \sed s,\($*\)\.o[ :]*,\1.o $ : ,g $.$$$$ $; \rm -f $.$$$$include main.d这里使用了sed去修改了gcc默认生成的依赖关系即将main.o: main.cpp add.hpp转换成main.o main.d : main.cpp add.hpp。
也就是在main.d的依赖中增加了add.hpp的依赖。
等效的makefile如下所示
main: main.o main.dg $ -o $
main.o: main.cpp add.hppg -c $ -o $
main.d: main.cpp add.hppg -MM $这样, 即使在add.hpp中增加其他头文件依赖, 然后再修改其他的头文件也会触发main.o的更新。
伪目标
在demo1中我们提到过一个clean的目标这是一个伪目标。
伪目标并不是一个文件只是一个标签。
假设我们不使用.PHONY:
clean:rm *.o temp当本地也有一个文件叫做clean时 那么rm *.o temp的操作将不会被执行。
因此为了避免和文件重名的这种情况我们可以使用一个特殊的标记.PHONY来显式地指明一个目标是伪目标向make说明不管是否有这个文件这个目标就是伪目标。
.PHONY : clean只要有这个声明不管是否有clean文件要运行clean这个目标只有make clean这样。于是整个过程可以这样写
.PHONY : clean
clean :rm *.o tempdemo4: 一个综合案列使用内置函数静态模式自动生成依赖
在最后的这个例子中 我们将综合运用上述的一些技巧去完成模块的构建。
该目录中有inc和src两个子目录 其中inc目录中包含了add.hpp和sub.hpp两个头文件。
在src目录中 包含一个main.cpp的入口函数所在的文件 还包含一个math子目录math子目录中包含了add.cpp和sub.cpp两个文件。
.
├── inc
│ ├── add.hpp
│ └── sub.hpp
├── Makefile
└── src├── main.cpp└── math├── add.cpp└── sub.cpp以下是这些文件中的内容 案例主要关注Makefile的编写 因此源文件的代码都较为简单。
add.hpp
int my_add(int a, int b);sub.hpp
int my_sub(int a, int b);main.cpp
#include iostream
#include add.hpp
#include sub.hpp
using namespace std;int main()
{int a 1;int b 2;int c my_add(a, b);cout a b c endl;int d my_sub(a, b);cout a - b d endl;return 0;
}add.cpp
#include add.hppint my_add(int a, int b)
{return a b;
}sub.cpp
#include sub.hppint my_sub(int a, int b)
{return a - b;
}以下是Makefile中的内容:
VPATH src:src/math:incCXXg
MODULEdemo4
SOURCE_PATH ./src \./src/math
TEMP_PATH./tmp
TARGET$(TEMP_PATH)/$(MODULE)CPP_SOURCES $(foreach d,$(SOURCE_PATH),$(wildcard $(d)/*.cpp) )
CPP_OBJS $(patsubst %.cpp, $(TEMP_PATH)/%.o, $(notdir $(CPP_SOURCES)))
CPP_DEPS $(patsubst %.cpp, $(TEMP_PATH)/%.d, $(notdir $(CPP_SOURCES)))
OBJS $(CPP_OBJS)CXXFLAGS-I./inc$(TARGET): $(OBJS) $(CPP_DEPS)echo generate final target$(CXX) -o $ $(OBJS)$(CPP_OBJS): $(TEMP_PATH)/%.o : %.cpp-mkdir -p $(TEMP_PATH)echo [$(MODULE) CXX] $$(CXX) -c $(CXXFLAGS) $ -o $$(CPP_DEPS): $(TEMP_PATH)/%.d: %.cpp-mkdir -p $(TEMP_PATH)-echo -n $(TEMP_PATH)/ $set -e; rm -f $; \$(CXX) -MM $(CXXFLAGS) $ $.$$$$; \sed s,\($*\)\.o[ :]*,tmp/\1.o $ : ,g $.$$$$ $; \rm -f $.$$$$include $(CPP_DEPS).PHONY: clean
clean:rm -rf $(TEMP_PATH)/*.o $(TEMP_PATH)/*.d $(TEMP_PATH)/$(MODULE)
下面一一分析。
VPATH src:src/math:inc首先使用VPATH依赖文件存在的路径 以便下面自动推导文件的相对路径。
CXXg
MODULEdemo4
SOURCE_PATH ./src \./src/math
TEMP_PATH./tmp
TARGET$(TEMP_PATH)/$(MODULE)CXXFLAGS-I./inc这里定义了一些变量包括源文件的路径 目标文件存放的位置 编译参数等等。
CPP_SOURCES $(foreach d,$(SOURCE_PATH),$(wildcard $(d)/*.cpp) )
CPP_OBJS $(patsubst %.cpp, $(TEMP_PATH)/%.o, $(notdir $(CPP_SOURCES)))
CPP_DEPS $(patsubst %.cpp, $(TEMP_PATH)/%.d, $(notdir $(CPP_SOURCES)))
OBJS $(CPP_OBJS)这里首先使用foreach去遍历SOURCE_PATH路径下的所有的.cpp文件
然后将.cpp文件做字符串替换 替换为.o 同时增加了存放的路径 这里使用了patsubst做字符串替换 并使用了notdir去获取文件名。
$(TARGET): $(OBJS) $(CPP_DEPS)echo generate final target$(CXX) -o $ $(OBJS)这里展开便是:
tmp/demo4: tmp/main.o tmp/sub.o tmp/add.o tmp/main.d tmp/sub.d tmp/main.dg -o tmp/demo4 tmp/main.o tmp/sub.o tmp/add.o$(CPP_OBJS): $(TEMP_PATH)/%.o : %.cpp-mkdir -p $(TEMP_PATH)echo [$(MODULE) CXX] $$(CXX) -c $(COMPILEFLAGS) $ -o $这里展开便是:
tmp/main.o: src/main.cppg -c src/main.cpp -o tmp/main.o -I./inc
tmp/add.o: src/math/add.cppg -c src/math/add.cpp -o tmp/add.o -I./inc
tmp/sub.o: src/math/sub.cppg -c src/math/sub.cpp -o tmp/sub.o -I./inc该步骤就是生成了.o文件:
$(CPP_DEPS): $(TEMP_PATH)/%.d: %.cpp-mkdir -p $(TEMP_PATH)-echo -n $(TEMP_PATH)/ $set -e; rm -f $; \$(CXX) -MM $(CXXFLAGS) $ $.$$$$; \sed s,\($*\)\.o[ :]*,tmp/\1.o $ : ,g $.$$$$ $; \rm -f $.$$$$该步骤使用了g -MM参数用于自动生成依赖文件 以便于当头文件修改时也可以自动编译/
.PHONY: clean
clean:rm -rf $(TEMP_PATH)/*.o $(TEMP_PATH)/main最后这个模块用于清除生成的文件。
至此 demo4结束。