如何制作一个简单的 C++ Makefile
Posted
技术标签:
【中文标题】如何制作一个简单的 C++ Makefile【英文标题】:How to make a SIMPLE C++ Makefile 【发布时间】:2011-01-29 16:34:40 【问题描述】:我们需要使用 Makefile 来为我们的项目整合所有内容,但我们的教授从未向我们展示过如何操作。
我只有一个文件,a3driver.cpp
。驱动程序从"/user/cse232/Examples/example32.sequence.cpp"
位置导入一个类。
就是这样。其他所有内容都包含在 .cpp
中。
我将如何制作一个简单的 Makefile 来创建一个名为 a3a.exe
的可执行文件?
【问题讨论】:
.EXE 所以它绝对是 Windows。再三考虑……路径是 Unix 风格的。可能使用 Mingw-32。 叹息。我想你必须学习每笔交易的基础知识,即使你永远不会使用它们。只需要了解事物的工作原理。不过,很有可能您将始终在 IDE 中进行开发,例如 Eclipse。您将在这里得到简单的单行案例的答案,并且有大量的网络教程,但如果您想要深入了解,您无法击败 O'reilly 的书(大多数软件主题相同)。 amazon.com/Managing-Projects-Make-Nutshell-Handbooks/dp/… 从 amazon、half.com、betterworldbooks eBay 选择第二手副本 @Dennis 发布的链接现已失效,但在此archive.org page 中可以找到相同的材料。 我更喜欢这个人的想法。 (hiltmon.com/blog/2013/07/03/…) 可以轻松修改项目结构以适应。而且我也同意开发人员的时间应该花在 automake/autoconf 以外的其他事情上。这些工具有它们的位置,但可能不适用于内部项目。我正在构建一个将产生这样一个项目结构的脚本。 @GuilhermeSalomé 谢谢,我相信这是最简单最完整的教程。 【参考方案1】:由于这是针对 Unix 的,因此可执行文件没有任何扩展名。
需要注意的是root-config
是一个提供正确编译和链接标志的实用程序;以及用于针对 root 构建应用程序的正确库。这只是与本文档的原始受众相关的一个细节。
让我成为婴儿
或者你永远不会忘记你第一次被制造的时候
关于make的介绍性讨论,以及如何编写一个简单的makefile
什么是 Make?我为什么要关心?
名为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
呸!多么麻烦!有很多事情要记住,也有很多犯错的机会。 (顺便说一句——这里展示的命令行细节取决于我们的软件环境。这些在我的电脑上工作。)
当然,您可以每次都运行所有三个命令。这可行,但它不能很好地扩展到大量软件(例如 DOGS,它需要 15 多分钟才能在我的 MacBook 上从头开始编译)。
相反,您可以像这样编写一个名为 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)
在文本中用 npat
替换所有 opat
实例。
利用这一点给了我们:
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
的文件,其中包含看起来像制作依赖行的内容
其他读物
GNU make manual Recursive Make Considered Harmful 介绍了一种不太理想的 makefile 常用编写方式,以及如何避免这种情况。了解错误和历史记录
Make 的输入语言对空格敏感。特别是,依赖项之后的操作行必须以制表符开头。但是一系列空格看起来是一样的(确实有些编辑器会默默地将制表符转换为空格,反之亦然),这会导致 Make 文件看起来正确但仍然无法工作。这在早期被确定为一个错误,但 (the story goes) 并没有修复,因为已经有 10 个用户。
(这是从我为物理研究生写的 wiki 帖子中复制的。)
【讨论】:
这种生成依赖的方法已经过时并且实际上是有害的。见Advanced Auto-Dependency Generation。-pthread
标志导致gcc
定义必要的宏,-D_REENTRANT
是不必要的。
@jcoe 它执行了不必要的额外预处理程序以生成依赖项。做不必要的工作只会消散融化冰极的热量,在更大的范围内,它正在接近我们宇宙的热寂。
@jcoe 除了 Maxim 的高昂成本之外,还有一个非常直接的成本是让您的构建时间更长。一旦项目变得比几个开发人员大并且一些分数文件编译时间成为一个问题,并且不明智地使用make
功能,可能会生成非常缓慢且仍然不正确的构建。如果您有时间阅读“递归使认为有害”。我在这里做错了,因为(1)货物崇拜和(2)当有人问的时候我知道如何解释。
可能“有害”有点过分,但鉴于至少从 GCC 3 开始明确的依赖生成阶段或目标已经过时,我真的认为我们都应该超越它们。 bruno.defraine.net/techtips/makefile-auto-dependencies-with-gcc/…【参考方案2】:
我一直认为通过详细的示例更容易学习,所以这就是我对 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
。
【讨论】:
我尝试使用的绝对代码是:p4a.exe: p4driver.cpp g++ -o p4a p4driver.cpp 但是,它告诉我“缺少分隔符”。我正在使用 TAB,但它仍然告诉我。有什么想法吗? 据我所知,只有当您有空格时才会出现该错误消息。确保您没有任何以空格开头的行(空格 + 制表符会给出该错误)。这是我唯一能想到的.. 未来编辑请注意:*** 无法呈现选项卡,即使您将它们编辑到答案中,所以请不要尝试“修复”我对此的注释。【参考方案3】:为什么每个人都喜欢列出源文件?一个简单的 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
【讨论】:
不自动查找源文件的一个原因是,不同的构建目标可能需要不同的文件。 同意@hmijail,以及包含大量您不想编译/链接的源/标头的子模块......毫无疑问,还有许多其他不适合详尽搜索/使用的情况。跨度> 为什么要使用“shell find”而不是“wildcard”? @Nolan 在源目录树中查找源文件【参考方案4】:你有两个选择。
选项 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
【讨论】:
如果它没有预先假设关于 OP 环境细节的很多事情,这将是一个很好的答案。是的,它们在 Windows 上,但这并不意味着它们使用的是nmake
。 link
命令行看起来也非常特定于特定的编译器,并且至少应该记录哪一个。【参考方案5】:
我使用了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++ 库中链接有关的错误。这个配置解决了这个问题。
【讨论】:
【参考方案6】:我建议(注意缩进是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
允许构建 .d 文件,其中包含有关标头依赖项的 Makefile 片段。第二行只是使用它们。
当然,编写良好的 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
【讨论】:
接近尾声:规则之间不应该有空行吗? John Knoeller's answer 声称。 我知道的make
的实现(GNU Make 和 BSD Make)都不需要规则之间的空行。然而,它存在大量的make
实现,它们有自己的错误^W 特性。【参考方案7】:
您的 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
【讨论】:
以上是关于如何制作一个简单的 C++ Makefile的主要内容,如果未能解决你的问题,请参考以下文章
如何在 Linux 上为 gcc 制作一个简单的 makefile?