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

我们被要求使用Makefile把我们项目的所有东西整合在一起,但我们的教授从来没有教过我们怎么做。

我只有一个文件,a3driver.cpp。驱动程序从位置"/user/cse232/Examples/example32.sequence.cpp"导入一个类。

就是这样。其他所有内容都包含在.cpp中。

我将如何着手创建一个简单的Makefile来创建一个名为a3a.exe的可执行文件?

546797 次浏览

我一直认为通过一个详细的示例更容易学习,因此下面是我对makefile的看法。对于每个section,你都有一行不缩进的行,它显示了section的名称和依赖项。依赖关系可以是其他section(将在当前section之前运行)或文件(如果更新将导致下次运行make时再次运行当前section)。

下面是一个快速的例子(请记住,我使用了4个空格,我应该使用一个选项卡,Stack Overflow不让我使用选项卡):

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

我之所以展示第一个示例,是因为它展示了makefiles的强大功能。如果需要编译另一个文件,只需添加另一个部分。下面是一个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

Make文件将有一个或两个依赖规则,这取决于您是使用单个命令进行编译和链接,还是使用一个编译命令和一个链接命令。

依赖关系是一个规则树,看起来像这样(注意,缩进必须是一个制表符):

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

因为这是针对Unix的,所以可执行文件没有任何扩展。

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

让我成为宝贝

或者你永远不会忘记你的第一次成功

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

Make是什么?我为什么要在意?

名为使的工具是一个构建依赖管理器。也就是说,它负责了解需要以何种顺序执行哪些命令,以便从源文件、目标文件、库、头文件等集合中获取软件项目(其中一些可能最近发生了更改),并将它们转换为程序的正确的最新版本。

实际上,你也可以用Make来做其他事情,但我不打算讨论这个。

一个简单的Makefile

假设你有一个目录包含:tool tool.cc tool.o support.cc support.hhsupport.o,它们依赖于root,并且应该被编译成一个名为tool的程序,并且假设你一直在入侵源文件(这意味着现有的tool现在已经过时),并且想要编译程序。

你可以自己做这件事

  1. 检查support.ccsupport.hh是否比support.o更新,如果是,运行如下命令

    g++ -g -c -pthread -I/sw/include/root support.cc
    
  2. Check if either support.hh or tool.cc are newer than tool.o, and if so run a command like

    g++ -g  -c -pthread -I/sw/include/root tool.cc
    
  3. Check if tool.o is newer than tool, and if so run a command like

    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
    

Phew! What a hassle! There is a lot to remember and several chances to make mistakes. (BTW-- the particulars of the command lines exhibited here depend on our software environment. These ones work on my computer.)

Of course, you could just run all three commands every time. That would work, but it doesn't scale well to a substantial piece of software (like DOGS which takes more than 15 minutes to compile from the ground up on my MacBook).

Instead you could write a file called makefile like this:

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。它将自动执行上面所示的三个步骤。

这里的未缩进行具有“目标:依赖关系”的形式,并告诉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)。< / p >

你可以在规则中使用变量,比如改进版的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

这样打字更容易,可读性更强。

请注意,

  1. 我们仍然显式地声明每个目标文件和最终可执行文件的依赖关系
  2. 我们必须为两个源文件显式地键入编译规则

隐式规则和模式规则

我们通常期望所有c++源文件都应该以相同的方式处理,Make提供了三种方式来说明这一点:

  1. 后缀规则(在GNU版本中被认为过时,但为了向后兼容而保留)
  2. 隐式规则
  3. 模式的规则

隐式规则是内置的,下面将讨论一些隐式规则。模式规则以类似的形式指定

%.o: %.c
$(CC) $(CFLAGS) $(CPPFLAGS) -c $<

这意味着目标文件是通过运行所示命令从C源文件生成的,其中“automatic”变量$<展开为第一个依赖项的名称。

内置规则

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

请注意,

  1. 源文件不再有任何依赖行!?!
  2. 有一些奇怪的魔法与。依赖和依赖有关
  3. 如果你执行make,然后执行ls -A,你会看到一个名为.depend的文件,其中包含类似于make依赖行的内容

其他阅读

了解bug和历史笔记

Make的输入语言是空格敏感的。特别是依赖项后面的操作行必须以制表符开始。但是一系列空格看起来可能是一样的(确实有编辑器会无声地将制表符转换为空格,反之亦然),这将导致Make文件看起来是正确的,但仍然不能工作。这在早期被确定为一个错误,但(故事是这样的)它没有被修复,因为已经有10个用户。

(这篇文章摘自我为物理研究生写的一篇维基文章。)

你有两个选择。

选项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

为什么每个人都喜欢列出源文件?一个简单的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

我使用了friedmud的回答。我研究了一段时间,这似乎是一个开始的好方法。这个解决方案还有一个定义良好的添加编译器标志的方法。我又回答了一遍,因为我做了一些改变,让它在我的环境中工作,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++库中没有链接有关的错误。这个配置解决了这个问题。

我建议(注意缩进是一个制表符):

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

旁注:如果你必须使用外部库,我建议使用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默认为空。用户可以将其设置为在替代系统上安装程序(对于交叉编译过程是必需的)。为了在/usr中安装你的包,多重分发的包维护者也可以改变PREFIX

最后一句话:不要将源文件放在子目录中。如果你真的想这样做,将这个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