我们需要使用 Makefile 为我们的项目整合所有内容,但我们的教授从未向我们展示过如何去做。
我只有 一个 文件,a3driver.cpp
。驱动程序从位置 "/user/cse232/Examples/example32.sequence.cpp"
导入一个类。
而已。其他所有内容都包含在 .cpp
中。
我将如何制作一个简单的 Makefile 来创建一个名为 a3a.exe
的可执行文件?
由于这是针对 Unix 的,因此可执行文件没有任何扩展名。
需要注意的一点是 root-config
是一个提供正确编译和链接标志的实用程序;以及用于针对 root 构建应用程序的正确库。这只是与本文档的原始受众相关的一个细节。
让我成为宝贝
或者你永远不会忘记你第一次被制造
一个关于make的介绍性讨论,以及如何编写一个简单的makefile
什么是制作?我为什么要关心?
名为 Make 的工具是一个构建依赖项管理器。也就是说,它负责了解需要以什么顺序执行哪些命令,以便从源文件、目标文件、库、头文件等的集合中获取您的软件项目——其中一些可能已经改变最近——并将它们变成正确的最新版本的程序。
实际上,您也可以将 Make 用于其他事情,但我不打算谈论这个。
一个简单的 Makefile
假设您有一个目录,其中包含:tool
tool.cc
tool.o
support.cc
support.hh
和 support.o
,它们依赖于 root
并且应该被编译成一个名为 tool
的程序,并假设您一直在破解源文件(这意味着现有的 tool
现在已经过时)并且想要编译程序。
要自己做,你可以
检查 support.cc 或 support.hh 是否比 support.o 新,如果是,则运行 g++ -g -c -pthread -I/sw/include/root support.cc 之类的命令 检查 support.hh 或 tool .cc 比 tool.o 新,如果是,则运行类似 g++ -g -c -pthread -I/sw/include/root tool.cc 的命令 检查 tool.o 是否比 tool 新,如果是,则运行命令像 g++ -g tool.o support.o -L/sw/lib/root -lCore -lCint -lRIO -lNet -lHist -lGraf -lGraf3d -lGpad -lTree -lRint \ -lPostscript -lMatrix -lPhysics -lMathCore -lThread - lz -L/sw/lib -lfreetype -lz -Wl,-framework,CoreServices \ -Wl,-framework,ApplicationServices -pthread -Wl,-rpath,/sw/lib/root -lm -ldl
呸!多么麻烦!有很多事情要记住,也有很多犯错的机会。 (顺便说一句——这里展示的命令行的细节取决于我们的软件环境。这些在我的电脑上工作。)
当然,您可以每次都运行所有三个命令。这会起作用,但它不能很好地扩展到大量软件(比如在我的 MacBook 上从头开始编译需要 15 分钟以上的 DOGS)。
相反,您可以像这样编写一个名为 makefile
的文件:
tool: tool.o support.o
g++ -g -o tool tool.o support.o -L/sw/lib/root -lCore -lCint -lRIO -lNet -lHist -lGraf -lGraf3d -lGpad -lTree -lRint \
-lPostscript -lMatrix -lPhysics -lMathCore -lThread -lz -L/sw/lib -lfreetype -lz -Wl,-framework,CoreServices \
-Wl,-framework,ApplicationServices -pthread -Wl,-rpath,/sw/lib/root -lm -ldl
tool.o: tool.cc support.hh
g++ -g -c -pthread -I/sw/include/root tool.cc
support.o: support.hh support.cc
g++ -g -c -pthread -I/sw/include/root support.cc
只需在命令行输入 make
。它将自动执行上面显示的三个步骤。
此处未缩进的行具有 "target: dependencies" 形式,并告诉 Make,如果任何依赖项比目标更新,则应运行关联的命令(缩进行)。也就是说,依赖行描述了需要重建以适应各种文件中的更改的逻辑。如果 support.cc
发生更改,则意味着必须重建 support.o
,但可以不理会 tool.o
。当 support.o
更改时,必须重建 tool
。
与每个依赖行关联的命令都用一个选项卡(见下文)设置,应该修改目标(或至少触摸它以更新修改时间)。
变量、内置规则和其他好东西
在这一点上,我们的 makefile 只是记住了需要做的工作,但我们仍然必须弄清楚并完整地键入每个需要的命令。它不一定是这样的:Make 是一种强大的语言,它具有变量、文本操作函数和大量内置规则,可以使我们更容易做到这一点。
制作变量
访问 make 变量的语法是 $(VAR)
。
分配给 Make 变量的语法是:VAR = A text value of some kind
(或 VAR := A different text value but ignore this for the moment
)。
您可以在规则中使用变量,例如我们的 makefile 的改进版本:
CPPFLAGS=-g -pthread -I/sw/include/root
LDFLAGS=-g
LDLIBS=-L/sw/lib/root -lCore -lCint -lRIO -lNet -lHist -lGraf -lGraf3d -lGpad -lTree -lRint \
-lPostscript -lMatrix -lPhysics -lMathCore -lThread -lz -L/sw/lib -lfreetype -lz \
-Wl,-framework,CoreServices -Wl,-framework,ApplicationServices -pthread -Wl,-rpath,/sw/lib/root \
-lm -ldl
tool: tool.o support.o
g++ $(LDFLAGS) -o tool tool.o support.o $(LDLIBS)
tool.o: tool.cc support.hh
g++ $(CPPFLAGS) -c tool.cc
support.o: support.hh support.cc
g++ $(CPPFLAGS) -c support.cc
这更具可读性,但仍然需要大量输入
制作函数
GNU make 支持从文件系统或系统上的其他命令访问信息的各种功能。在这种情况下,我们对 $(shell ...)
感兴趣,它扩展为参数的输出,而 $(subst opat,npat,text)
将文本中的 opat
的所有实例替换为 npat
。
利用这一点,我们可以:
CPPFLAGS=-g $(shell root-config --cflags)
LDFLAGS=-g $(shell root-config --ldflags)
LDLIBS=$(shell root-config --libs)
SRCS=tool.cc support.cc
OBJS=$(subst .cc,.o,$(SRCS))
tool: $(OBJS)
g++ $(LDFLAGS) -o tool $(OBJS) $(LDLIBS)
tool.o: tool.cc support.hh
g++ $(CPPFLAGS) -c tool.cc
support.o: support.hh support.cc
g++ $(CPPFLAGS) -c support.cc
这更容易输入并且更具可读性。
请注意
我们仍然明确说明每个目标文件和最终可执行文件的依赖关系我们必须明确键入两个源文件的编译规则
隐式规则和模式规则
我们通常希望所有 C++ 源文件都应该以同样的方式处理,Make 提供了三种方式来说明这一点:
后缀规则(在 GNU make 中被认为是过时的,但为了向后兼容而保留)隐式规则模式规则
隐式规则是内置的,下面将讨论一些规则。模式规则以如下形式指定
%.o: %.c
$(CC) $(CFLAGS) $(CPPFLAGS) -c $<
这意味着目标文件是通过运行显示的命令从 C 源文件生成的,其中“自动”变量 $<
扩展为第一个依赖项的名称。
内置规则
Make 有一大堆内置规则,这意味着很多时候,一个项目可以通过一个非常简单的 makefile 来编译,确实如此。
C 源文件的 GNU make 内置规则就是上面展示的规则。同样,我们使用 $(CXX) -c $(CPPFLAGS) $(CFLAGS)
之类的规则从 C++ 源文件创建目标文件。
单个对象文件使用 $(LD) $(LDFLAGS) n.o $(LOADLIBES) $(LDLIBS)
链接,但这在我们的例子中不起作用,因为我们想要链接多个对象文件。
内置规则使用的变量
内置规则使用一组标准变量,允许您指定本地环境信息(例如在哪里可以找到 ROOT 包含文件),而无需重写所有规则。我们最感兴趣的是:
CC -- 要使用的 C 编译器
CXX -- 要使用的 C++ 编译器
LD——要使用的链接器
CFLAGS -- C 源文件的编译标志
CXXFLAGS -- C++ 源文件的编译标志
CPPFLAGS -- c 预处理器的标志(通常包括在命令行上定义的文件路径和符号),由 C 和 C++ 使用
LDFLAGS -- 链接器标志
LDLIBS——要链接的库
一个基本的 Makefile
通过利用内置规则,我们可以将 makefile 简化为:
CC=gcc
CXX=g++
RM=rm -f
CPPFLAGS=-g $(shell root-config --cflags)
LDFLAGS=-g $(shell root-config --ldflags)
LDLIBS=$(shell root-config --libs)
SRCS=tool.cc support.cc
OBJS=$(subst .cc,.o,$(SRCS))
all: tool
tool: $(OBJS)
$(CXX) $(LDFLAGS) -o tool $(OBJS) $(LDLIBS)
tool.o: tool.cc support.hh
support.o: support.hh support.cc
clean:
$(RM) $(OBJS)
distclean: clean
$(RM) tool
我们还添加了几个执行特殊操作(如清理源目录)的标准目标。
请注意,当不带参数调用 make 时,它使用在文件中找到的第一个目标(在本例中为全部),但您也可以将目标命名为 get,在这种情况下 make clean
删除目标文件。
我们仍然对所有依赖项进行了硬编码。
一些神秘的改进
CC=gcc
CXX=g++
RM=rm -f
CPPFLAGS=-g $(shell root-config --cflags)
LDFLAGS=-g $(shell root-config --ldflags)
LDLIBS=$(shell root-config --libs)
SRCS=tool.cc support.cc
OBJS=$(subst .cc,.o,$(SRCS))
all: tool
tool: $(OBJS)
$(CXX) $(LDFLAGS) -o tool $(OBJS) $(LDLIBS)
depend: .depend
.depend: $(SRCS)
$(RM) ./.depend
$(CXX) $(CPPFLAGS) -MM $^>>./.depend;
clean:
$(RM) $(OBJS)
distclean: clean
$(RM) *~ .depend
include .depend
请注意
源文件不再有任何依赖行!?! .depend 和depend 有一些奇怪的魔法 如果你确实 make 然后 ls -A 你会看到一个名为 .depend 的文件,其中包含看起来像 make 依赖行的东西
其他阅读
GNU 制作手册
Recursive Make Considered Harmful on a common way of writing makefiles that less than best,以及如何避免它。
了解错误和历史记录
Make 的输入语言对空格敏感。特别是,依赖项之后的操作行必须以制表符开头。但是一系列空格看起来是一样的(确实有些编辑器会默默地将制表符转换为空格,反之亦然),这会导致 Make 文件看起来正确但仍然无法工作。这在早期被确定为一个错误,但 (the story goes) 它没有修复,因为已经有 10 个用户。
(这是从我为物理研究生写的 wiki 帖子中复制的。)
我一直认为通过详细的示例更容易学习,所以这就是我对 makefile 的看法。对于每个部分,您都有一个不缩进的行,它显示该部分的名称,后跟依赖项。依赖项可以是其他部分(将在当前部分之前运行)或文件(如果更新将导致当前部分在您下次运行 make
时再次运行)。
这是一个简单的示例(请记住,我在应该使用制表符的地方使用了 4 个空格,堆栈溢出不会让我使用制表符):
a3driver: a3driver.o
g++ -o a3driver a3driver.o
a3driver.o: a3driver.cpp
g++ -c a3driver.cpp
当您键入 make
时,它将选择第一部分 (a3driver)。 a3driver 依赖于 a3driver.o,所以它会转到那个部分。 a3driver.o 依赖于 a3driver.cpp,因此仅当 a3driver.cpp 自上次运行后发生更改时才会运行。假设它已经(或从未运行过),它会将 a3driver.cpp 编译为 .o 文件,然后返回到 a3driver 并编译最终的可执行文件。
由于只有一个文件,它甚至可以简化为:
a3driver: a3driver.cpp
g++ -o a3driver a3driver.cpp
我展示第一个示例的原因是它展示了 makefile 的强大功能。如果您需要编译另一个文件,您可以添加另一个部分。这是一个带有 secondFile.cpp 的示例(加载到名为 secondFile.h 的标头中):
a3driver: a3driver.o secondFile.o
g++ -o a3driver a3driver.o secondFile.o
a3driver.o: a3driver.cpp
g++ -c a3driver.cpp
secondFile.o: secondFile.cpp secondFile.h
g++ -c secondFile.cpp
这样,如果您更改 secondFile.cpp 或 secondFile.h 中的某些内容并重新编译,它只会重新编译 secondFile.cpp(而不是 a3driver.cpp)。或者,如果您更改 a3driver.cpp 中的某些内容,它不会重新编译 secondFile.cpp。
如果您对此有任何疑问,请告诉我。
包括一个名为“all”的部分和一个名为“clean”的部分也是传统的。 “all”通常会构建所有可执行文件,“clean”会删除“构建工件”,如 .o 文件和可执行文件:
all: a3driver ;
clean:
# -f so this will succeed even if the files don't exist
rm -f a3driver a3driver.o
编辑:我没有注意到你在 Windows 上。我认为唯一的区别是将 -o a3driver
更改为 -o a3driver.exe
。
为什么大家都喜欢列出源文件?一个简单的 find 命令可以轻松解决这个问题。
这是一个简单的 C++ Makefile 示例。只需将其放在包含 .C
个文件的目录中,然后键入 make
...
appname := myapp
CXX := clang++
CXXFLAGS := -std=c++11
srcfiles := $(shell find . -name "*.C")
objects := $(patsubst %.C, %.o, $(srcfiles))
all: $(appname)
$(appname): $(objects)
$(CXX) $(CXXFLAGS) $(LDFLAGS) -o $(appname) $(objects) $(LDLIBS)
depend: .depend
.depend: $(srcfiles)
rm -f ./.depend
$(CXX) $(CXXFLAGS) -MM $^>>./.depend;
clean:
rm -f $(objects)
dist-clean: clean
rm -f *~ .depend
include .depend
你有两个选择。
选项 1:最简单的 makefile = NO MAKEFILE。
将“a3driver.cpp”重命名为“a3a.cpp”,然后在命令行中写入:
nmake a3a.exe
就是这样。如果您使用 GNU Make,请使用“make”或“gmake”等。
选项 2:一个 2 行的 makefile。
a3a.exe: a3driver.obj
link /out:a3a.exe a3driver.obj
nmake
。 link
命令行看起来也非常特定于特定的编译器,并且至少应该记录哪个编译器。
我使用了 friedmud's answer。我研究了一段时间,这似乎是一个很好的开始方式。此解决方案还具有添加编译器标志的明确定义的方法。我再次回答,因为我进行了更改以使其在我的环境 Ubuntu 和 g++ 中工作。有时,更多的工作示例是最好的老师。
appname := myapp
CXX := g++
CXXFLAGS := -Wall -g
srcfiles := $(shell find . -maxdepth 1 -name "*.cpp")
objects := $(patsubst %.cpp, %.o, $(srcfiles))
all: $(appname)
$(appname): $(objects)
$(CXX) $(CXXFLAGS) $(LDFLAGS) -o $(appname) $(objects) $(LDLIBS)
depend: .depend
.depend: $(srcfiles)
rm -f ./.depend
$(CXX) $(CXXFLAGS) -MM $^>>./.depend;
clean:
rm -f $(objects)
dist-clean: clean
rm -f *~ .depend
include .depend
Makefile 似乎很复杂。我正在使用一个,但它产生了一个与未在 g++ 库中链接有关的错误。这个配置解决了这个问题。
我建议(注意缩进是一个 TAB):
tool: tool.o file1.o file2.o
$(CXX) $(LDFLAGS) $^ $(LDLIBS) -o $@
或者
LINK.o = $(CXX) $(LDFLAGS) $(TARGET_ARCH)
tool: tool.o file1.o file2.o
后一个建议稍微好一点,因为它重用了 GNU Make 隐式规则。但是,为了工作,源文件必须与最终可执行文件具有相同的名称(即:tool.c
和 tool
)。
注意,没有必要声明来源。中间对象文件是使用隐式规则生成的。因此,此 Makefile
适用于 C 和 C++(也适用于 Fortran 等)。
另请注意,默认情况下,Makefile 使用 $(CC)
作为链接器。 $(CC)
不适用于链接 C++ 目标文件。我们仅因此而修改 LINK.o
。如果要编译 C 代码,则不必强制 LINK.o
值。
当然,您也可以使用变量 CFLAGS
添加编译标志,并在 LDLIBS
中添加您的库。例如:
CFLAGS = -Wall
LDLIBS = -lm
附注:如果您必须使用外部库,我建议 use pkg-config 以正确设置 CFLAGS
和 LDLIBS
:
CFLAGS += $(shell pkg-config --cflags libssl)
LDLIBS += $(shell pkg-config --libs libssl)
细心的读者会注意到,如果更改了一个标头,则此 Makefile
不会正确重建。添加这些行来解决问题:
override CPPFLAGS += -MMD
include $(wildcard *.d)
-MMD
允许构建包含有关标头依赖项的 Makefile 片段的 .d 文件。第二行只是使用它们。
当然,编写良好的 Makefile 还应该包含 clean
和 distclean
规则:
clean:
$(RM) *.o *.d
distclean: clean
$(RM) tool
请注意,$(RM)
等效于 rm -f
,但最好不要直接调用 rm
。
all
规则也很受欢迎。为了工作,它应该是你文件的第一条规则:
all: tool
您还可以添加 install
规则:
PREFIX = /usr/local
install:
install -m 755 tool $(DESTDIR)$(PREFIX)/bin
DESTDIR
默认为空。用户可以将其设置为在替代系统上安装您的程序(对于交叉编译过程是必需的)。多次分发的包维护者也可以更改 PREFIX
,以便在 /usr
中安装您的包。
最后一句话:不要将源文件放在子目录中。如果您真的想这样做,请将此 Makefile
保留在根目录中并使用完整路径来标识您的文件(即 subdir/file.o
)。
总而言之,您的完整 Makefile 应如下所示:
LINK.o = $(CXX) $(LDFLAGS) $(TARGET_ARCH)
PREFIX = /usr/local
override CPPFLAGS += -MMD
include $(wildcard *.d)
all: tool
tool: tool.o file1.o file2.o
clean:
$(RM) *.o *.d
distclean: clean
$(RM) tool
install:
install -m 755 tool $(DESTDIR)$(PREFIX)/bin
make
的实现(GNU Make 和 BSD Make)都不需要规则之间的空行。但是,它存在大量具有自身错误^W 特性的 make
实现。
您的 Make 文件将有一个或两个依赖关系规则,具体取决于您是使用单个命令编译和链接,还是使用一个用于编译的命令和一个用于链接的命令。
依赖是一棵规则树,看起来像这样(注意缩进必须是 TAB):
main_target : source1 source2 etc
command to build main_target from sources
source1 : dependents for source1
command to build source1
目标的命令后面必须有一个空行,并且命令之前不能有一个空行。 makefile 中的第一个目标是总体目标,只有当第一个目标依赖于它们时才会构建其他目标。
所以你的 makefile 看起来像这样。
a3a.exe : a3driver.obj
link /out:a3a.exe a3driver.obj
a3driver.obj : a3driver.cpp
cc a3driver.cpp
-pthread
标志导致gcc
定义必要的宏,-D_REENTRANT
是不必要的。make
功能,可能会生成非常缓慢且仍然不正确的构建。如果您有时间阅读“递归使认为有害”。我在这里做错了,因为(1)货物崇拜和(2)当有人问的时候我知道如何解释。