不同目录下的源文件如何编写Makefile

161

我有一个项目,目录结构如下:

                         $projectroot
                              |
              +---------------+----------------+
              |               |                |
            part1/          part2/           part3/
              |               |                |
       +------+-----+     +---+----+       +---+-----+
       |      |     |     |        |       |         |
     data/   src/  inc/  src/     inc/   src/       inc/

我应该如何编写一个makefile,使其可以在part/src(或其他任何地方)中完成C/C++源文件在part?/src中的编译和链接?

我能否做类似以下的事情: -I$projectroot/part1/src -I$projectroot/part1/inc -I$projectroot/part2/src ...

如果这样行得通,是否有更简单的方法呢?我看过一些项目,其中每个相应的part?文件夹中都有一个makefile。[在本帖子中,我使用了类似于bash语法的问号]


https://dev59.com/OlrUa4cB1Zd3GeqPj3Af#7321954 - Maxim Egorushkin
2
在原始的gnu手册(https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html)中,关于虚拟目标的部分有一个关于“递归调用”的示例,这可能非常优雅。 - Frank N
你用什么工具创建了那个文本图形? - Khalid Hussain
10个回答

134
传统的方法是在每个子目录(part1、part2等)中都有一个Makefile,使您能够独立地构建它们。此外,在项目的根目录中还有一个Makefile,用于构建所有内容。 "根" Makefile的示例如下:
all:
    +$(MAKE) -C part1
    +$(MAKE) -C part2
    +$(MAKE) -C part3

由于在make目标中的每一行都在自己的shell中运行,所以无需担心返回到目录树的上层或其他目录。

我建议查看GNU make手册的第5.7节。它非常有帮助。


27
这句话的翻译是:“这应该是 +$(MAKE) -C part1 等等。这样可以让 Make 的作业控制在子目录中生效。” - ephemient
29
这是一种经典的方法,被广泛使用,但随着项目规模的增长,在几个方面存在不够优化的问题。Dave Hinton有相关指针可供参考。 - dmckee --- ex-moderator kitten
5
“+”符号的意思是什么? - Izzo

98
如果你的一个子目录中的代码依赖于另一个子目录中的代码,那么最好在顶层使用单个makefile。详见递归式make的有害作用,但基本上你希望让make拥有决定是否需要重建文件的全部信息,如果只告诉它你项目的三分之一,它就无法做到这一点。
上面的链接似乎无法访问。同样的文档可以在这里找到:

4
谢谢,我不知道这一点。知道“正确的方法”做事情比那些“只管用”或被接受为标准的方式更有用。 - tjklemz
4
递归式的 make 在它表现不佳的时候被认为是有害的。但是现在它已经不再被认为是有害的了,事实上,这是 autotools / automake 管理大型项目的方式。 - Edwin Buck
1
截至2020年9月,所有这些链接似乎都无法访问。不过你可以通过谷歌搜索来找到它们。 - mizkichan

45

VPATH选项可能会很有用,它告诉make在哪些目录中查找源代码。不过你仍然需要为每个包含路径使用一个-I选项。下面是一个例子:

CXXFLAGS=-Ipart1/inc -Ipart2/inc -Ipart3/inc
VPATH=part1/src:part2/src:part3/src

OutputExecutable: part1api.o part2api.o part3api.o

这将自动在指定的VPATH目录中查找匹配的partXapi.cpp文件并编译它们。但是,如果您的src目录被分成子目录,那么此方法更有用。对于您所描述的情况,正如其他人所说,如果每个部分都是独立的,那么每个部分最好使用一个makefile。


4
我无法相信这个简单而完美的答案没有得到更多的投票。我会给它点赞 (+1)。 - Nicholas Hamilton
3
我有一些常见的源文件放在一个更高级别的目录中,供不同子文件夹中的几个项目使用,“VPATH=..”对我很有用! - EkriirkE
太完美了,太简单了。谢谢! - Sujay Phadke

28

您可以在根Makefile中添加规则,以编译其他目录中必要的cpp文件。以下Makefile示例应该是一个好的起点,可以帮助您到达您想要的地方。

CC=g++
TARGET=cppTest
OTHERDIR=../../someotherpath/in/project/src
SOURCE = cppTest.cpp SOURCE = $(OTHERDIR)/file.cpp ## 定义源文件 INCLUDE = -I./ $(AN_INCLUDE_DIR) INCLUDE = -I.$(OTHERDIR)/../inc ## 定义更多的include VPATH=$(OTHERDIR) OBJ=$(join $(addsuffix ../obj/, $(dir $(SOURCE))), $(notdir $(SOURCE:.cpp=.o)))
## 修正依赖关系目标为相对于src目录的../.dep DEPENDS=$(join $(addsuffix ../.dep/, $(dir $(SOURCE))), $(notdir $(SOURCE:.cpp=.d)))
## 执行默认规则 all: $(TARGET) @true ## 清理规则 clean: @-rm -f $(TARGET) $(OBJ) $(DEPENDS)
## 创建实际目标的规则 $(TARGET): $(OBJ) @echo "=============" @echo "链接目标 $@" @echo "=============" @$(CC) $(CFLAGS) -o $@ $^ $(LIBS) @echo -- 链接完成 --
## 通用编译规则 %.o : %.cpp @mkdir -p $(dir $@) @echo "=============" @echo "编译 $<" @$(CC) $(CFLAGS) -c $< -o $@
## 从cpp文件生成目标文件的规则 ## 每个文件的目标文件都放在obj目录中, ## 与实际源目录相邻。 ../obj/%.o : %.cpp @mkdir -p $(dir $@) @echo "=============" @echo "编译 $<" @$(CC) $(CFLAGS) -c $< -o $@
# “其他目录”的规则。您需要每个“其他”目录一个。 $(OTHERDIR)/../obj/%.o : %.cpp @mkdir -p $(dir $@) @echo "=============" @echo "编译 $<"## 生成依赖关系规则 ../.dep/%.d: %.cpp @mkdir -p $(dir $@) @echo "=============" @echo 正在为 $*.o 构建依赖关系文件 @$(SHELL) -ec '$(CC) -M $(CFLAGS) $< | sed "s^$*.o^../obj/$*.o^" > $@'
## “其他”目录的依赖规则 $(OTHERDIR)/../.dep/%.d: %.cpp @mkdir -p $(dir $@) @echo "=============" @echo 正在为 $*.o 构建依赖关系文件 @$(SHELL) -ec '$(CC) -M $(CFLAGS) $< | sed "s^$*.o^$(OTHERDIR)/../obj/$*.o^" > $@'
## 包含依赖文件 -include $(DEPENDS)
该段代码是一个Makefile的一部分,用于创建与更新C++源代码文件的依赖关系。它使用GCC编译器和sed命令来生成依赖文件。Makefile中的变量被定义为$(CFLAGS)、$(CC)、$(DEPENDS)和$(OTHERDIR)。

7
我知道这已经很久了,但是,不是在下一行被另一个赋值覆盖了SOURCE和INCLUDE吗? - skelliam
@skelliam 是的,它确实如此。 - nabroyan
1
这种方法违反了DRY原则。为每个“其他目录”复制代码是一个不好的想法。 - Jesus H

26

如果源代码分散在多个文件夹中,需要使用单独的Makefile进行管理,则如先前建议的那样,递归式Make是一个不错的方式。但对于较小的项目,我发现在Makefile中列出所有源文件及其相对于Makefile的路径更加方便:

# common sources
COMMON_SRC := ./main.cpp \
              ../src1/somefile.cpp \
              ../src1/somefile2.cpp \
              ../src2/somefile3.cpp \

我可以这样设置 VPATH:

VPATH := ../src1:../src2

然后我构建对象:

COMMON_OBJS := $(patsubst %.cpp, $(ObjDir)/%$(ARCH)$(DEBUG).o, $(notdir $(COMMON_SRC)))

现在规则很简单:

# the "common" object files
$(ObjDir)/%$(ARCH)$(DEBUG).o : %.cpp Makefile
    @echo creating $@ ...
    $(CXX) $(CFLAGS) $(EXTRA_CFLAGS) -c -o $@ $<

构建输出甚至更加容易:

# This will make the cbsdk shared library
$(BinDir)/$(OUTPUTBIN): $(COMMON_OBJS)
    @echo building output ...
    $(CXX) -o $(BinDir)/$(OUTPUTBIN) $(COMMON_OBJS) $(LFLAGS)

通过以下方法,甚至可以自动化生成 VPATH:

VPATH := $(dir $(COMMON_SRC))

或者利用sort会删除重复元素的特点(尽管这并不重要):

VPATH := $(sort  $(dir $(COMMON_SRC)))

这对于一些较小的项目非常有效,你只需要添加一些自定义库并将它们放在一个单独的文件夹中。非常感谢。 - zitroneneis

6
我认为更好的做法是指出使用 Make(递归或非递归)通常应该避免,因为与今天的工具相比,它难以学习、维护和扩展。这是一个很棒的工具,但是在2010年及以后,直接使用它应该被视为过时的。当然,除非你在特殊环境下工作,例如遗留项目等。使用 IDE、CMake 或者如果你很专业,Autotools

1
希望能够解释一下负评的原因。我也曾经自己编写过Makefile,感觉很不错。 - IlDan
3
我支持“不要这样做”的建议——KDevelop有一个非常图形化的界面来配置Make和其他构建系统,虽然我自己也会写Makefile,但KDevelop是我给我的同事使用的工具。但我不认为Autotools链接有所帮助。要理解Autotools,你需要了解m4、libtool、automake、autoconf、shell、make以及整个堆栈。 - ephemient
8
下投票可能是因为你在回答自己的问题。问题不是关于是否应该使用Makefile,而是关于如何编写它们。 - Honza
8
现代工具如何比编写Makefile更方便?请用简洁几句话解释一下。它们是否提供了更高级别的抽象,或者其他什么原因? - Abhishek Anand
1
xterm、vi、bash、makefile 统治世界,cmake 是给那些不会编码的人用的。 - μολὼν.λαβέ
显示剩余2条评论

5

我一直在寻找这样的东西,在尝试和失败后,我创建了自己的makefile。我知道这不是“惯用方式”,但这是了解makefile的开始,对我有用,也许你可以在你的项目中尝试。

PROJ_NAME=mono

CPP_FILES=$(shell find . -name "*.cpp")

S_OBJ=$(patsubst %.cpp, %.o, $(CPP_FILES))

CXXFLAGS=-c \
         -g \
        -Wall

all: $(PROJ_NAME)
    @echo Running application
    @echo
    @./$(PROJ_NAME)

$(PROJ_NAME): $(S_OBJ)
    @echo Linking objects...
    @g++ -o $@ $^

%.o: %.cpp %.h
    @echo Compiling and generating object $@ ...
    @g++ $< $(CXXFLAGS) -o $@

main.o: main.cpp
    @echo Compiling and generating object $@ ...
    @g++ $< $(CXXFLAGS)

clean:
    @echo Removing secondary things
    @rm -r -f objects $(S_OBJ) $(PROJ_NAME)
    @echo Done!

我知道这很简单,对于有些人来说我的标志可能是错误的,但正如我所说,这是我第一个Makefile,用于在多个目录中编译我的项目并将它们链接在一起以创建我的二进制文件。

我接受建议:D


3

RC的帖子非常有用。我从未想过使用$(dir $@)函数,但它确切地实现了我所需要的。

在parentDir中,有许多包含源文件的目录:dirA、dirB、dirC等。各种文件依赖于其他目录中的目标文件,因此我希望能够从一个目录中制作一个文件,并通过调用与该依赖项关联的makefile来进行该依赖项的制作。

基本上,我在parentDir中制作了一个Makefile,其中包含(除其他内容之外)类似于RC的通用规则:

%.o : %.cpp
        @mkdir -p $(dir $@)
        @echo "============="
        @echo "Compiling $<"
        @$(CC) $(CFLAGS) -c $< -o $@
每个子目录都包含了这个上层makefile,以便继承这个通用规则。在每个子目录的Makefile中,我为每个文件编写了一个自定义规则,以便我可以跟踪每个单独文件所依赖的所有内容。
每当我需要制作一个文件时,我使用(基本上)这个规则来递归地制作任何/所有依赖项。非常完美!
注意:有一个名为“makepp”的实用程序,似乎更加直观地执行此任务,但出于可移植性和不依赖其他工具的考虑,我选择了这种方式。
希望这能帮到你!

1

使用递归的Make

all:
    +$(MAKE) -C part1
    +$(MAKE) -C part2
    +$(MAKE) -C part3

这使得make可以分割成多个任务并使用多个核心


2
这种做法与make -j4相比有什么优势呢? - devin
1
@devin,我认为这并不是“更好”,而是使make能够使用作业控制。当运行独立的make进程时,它不受作业控制的限制。 - Michael Pankov

-1
我建议使用autotools//##将生成的目标文件(.o)放置在与源文件相同的目录中,以避免在使用非递归make时发生冲突。
AUTOMAKE_OPTIONS = subdir-objects

只需将其与其他相当简单的内容一起放入Makefile.am中。

这里是tutorial


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接