ChatGPT解决这个技术问题 Extra ChatGPT

如何制作一个简单的 C++ Makefile

我们需要使用 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é 谢谢,我相信这是最简单最完整的教程。

2
23 revs, 11 users 84%

由于这是针对 Unix 的,因此可执行文件没有任何扩展名。

需要注意的一点是 root-config 是一个提供正确编译和链接标志的实用程序;以及用于针对 root 构建应用程序的正确库。这只是与本文档的原始受众相关的一个细节。

让我成为宝贝

或者你永远不会忘记你第一次被制造

一个关于make的介绍性讨论,以及如何编写一个简单的makefile

什么是制作?我为什么要关心?

名为 Make 的工具是一个构建依赖项管理器。也就是说,它负责了解需要以什么顺序执行哪些命令,以便从源文件、目标文件、库、头文件等的集合中获取您的软件项目——其中一些可能已经改变最近——并将它们变成正确的最新版本的程序。

实际上,您也可以将 Make 用于其他事情,但我不打算谈论这个。

一个简单的 Makefile

假设您有一个目录,其中包含:tool tool.cc tool.o support.cc support.hhsupport.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 帖子中复制的。)


这种生成依赖关系的方法已经过时并且实际上是有害的。请参阅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/…
B
Brendan Long

我一直认为通过详细的示例更容易学习,所以这就是我对 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,但它仍然告诉我。任何想法?
据我所知,只有当您有空格时才会出现该错误消息。确保您没有任何以空格开头的行(空格 + 制表符会给出该错误)。这是我唯一能想到的。。
给未来的编辑者的注意事项:即使您将选项卡编辑到答案中,StackOverflow 也无法呈现选项卡,因此请不要尝试“修复”我对此的注释。
P
Peter Mortensen

为什么大家都喜欢列出源文件?一个简单的 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”而不是“通配符”?
@Nolan 在源目录树中查找源文件
P
Peter Mortensen

你有两个选择。

选项 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 上,但这并不意味着它们正在使用 nmakelink 命令行看起来也非常特定于特定的编译器,并且至少应该记录哪个编译器。
P
Peter Mortensen

我使用了 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++ 库中链接有关的错误。这个配置解决了这个问题。


J
Jérôme Pouiller

我建议(注意缩进是一个 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.ctool)。

注意,没有必要声明来源。中间对象文件是使用隐式规则生成的。因此,此 Makefile 适用于 C 和 C++(也适用于 Fortran 等)。

另请注意,默认情况下,Makefile 使用 $(CC) 作为链接器。 $(CC) 不适用于链接 C++ 目标文件。我们仅因此而修改 LINK.o。如果要编译 C 代码,则不必强制 LINK.o 值。

当然,您也可以使用变量 CFLAGS 添加编译标志,并在 LDLIBS 中添加您的库。例如:

CFLAGS = -Wall
LDLIBS = -lm

附注:如果您必须使用外部库,我建议 use pkg-config 以正确设置 CFLAGSLDLIBS

CFLAGS += $(shell pkg-config --cflags libssl)
LDLIBS += $(shell pkg-config --libs libssl)

细心的读者会注意到,如果更改了一个标头,则此 Makefile 不会正确重建。添加这些行来解决问题:

override CPPFLAGS += -MMD
include $(wildcard *.d)

-MMD 允许构建包含有关标头依赖项的 Makefile 片段的 .d 文件。第二行只是使用它们。

当然,编写良好的 Makefile 还应该包含 cleandistclean 规则:

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)都不需要规则之间的空行。但是,它存在大量具有自身错误^W 特性的 make 实现。
P
Peter Mortensen

您的 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