使用Makefile创建目录

130

我想使用makefile来创建目录。我的项目目录结构如下:

+--Project  
   +--output  
   +--source  
     +Testfile.cpp  
   +Makefile  

我想把所有对象和输出放到相应的输出文件夹中。我希望在编译后创建以下文件夹结构。

+--Project
   +--output
     +--debug (or release)
       +--objs
         +Testfile.o
       +Testfile (my executable file)
   +--source
     +Testfile.cpp
   +Makefile

我尝试了几个选项,但都没有成功。请帮我用makefile创建目录。我在这里贴出我的Makefile供您考虑。

#---------------------------------------------------------------------
# Input dirs, names, files
#---------------------------------------------------------------------
OUTPUT_ROOT := output/

TITLE_NAME := TestProj 

ifdef DEBUG 
    TITLE_NAME += _DEBUG
else
ifdef RELEASE
    TITLE_NAME += _RELEASE
endif
endif


# Include all the source files here with the directory tree
SOURCES := \
        source/TestFile.cpp \

#---------------------------------------------------------------------
# configs
#---------------------------------------------------------------------
ifdef DEBUG
OUT_DIR     := $(OUTPUT_ROOT)debug
CC_FLAGS    := -c -Wall
else
ifdef RELEASE
OUT_DIR     := $(OUTPUT_ROOT)release
CC_FLAGS    := -c -Wall
else
$(error no build type defined)
endif
endif

# Put objects in the output directory.
OUT_O_DIR   := $(OUT_DIR)/objs

#---------------------------------------------------------------------
# settings
#---------------------------------------------------------------------
OBJS = $(SOURCES:.cpp=.o)
DIRS = $(subst /,/,$(sort $(dir $(OBJS))))
DIR_TARGET = $(OUT_DIR)

OUTPUT_TARGET = $(OUT_DIR)/$(TITLE_NAME)

CC_FLAGS +=   

LCF_FLAGS := 

LD_FLAGS := 

#---------------------------------------------------------------------
# executables
#---------------------------------------------------------------------
MD := mkdir
RM := rm
CC := g++

#---------------------------------------------------------------------
# rules
#---------------------------------------------------------------------
.PHONY: all clean title 

all: title 

clean:
    $(RM) -rf $(OUT_DIR)

$(DIR_TARGET):
    $(MD) -p $(DIRS)

.cpp.o: 
    @$(CC) -c $< -o $@

$(OBJS): $(OUT_O_DIR)/%.o: %.cpp
    @$(CC) -c $< -o $@

title: $(DIR_TARGET) $(OBJS)
11个回答

172

在我看来,无论是从技术还是设计的角度考虑,目录都不应该被视为Makefile的目标。你应该创建文件,并且如果需要新的目录才能创建文件,那么就在相关文件的规则中quietly创建该目录。

如果你要针对一个常规或“模式化”的文件进行操作,只需使用make内部变量 $(@D),它表示“当前目标所在的目录”(与目标的$@相对比)。例如:

$(OUT_O_DIR)/%.o: %.cpp
        @mkdir -p $(@D)
        @$(CC) -c $< -o $@

title: $(OBJS)

那么,你实际上是在做同样的事情:为所有的 $(OBJS) 创建目录,但你会用一种更简单的方式来做。

在各种应用程序中都采用相同的策略(文件是目标,目录从不是)。例如,git版本控制系统不存储目录。


注意: 如果要使用它,引入一个方便的变量并利用 make 的扩展规则可能会很有用。

dir_guard=@mkdir -p $(@D)

$(OUT_O_DIR)/%.o: %.cpp
        $(dir_guard)
        @$(CC) -c $< -o $@

$(OUT_O_DIR_DEBUG)/%.o: %.cpp
        $(dir_guard)
        @$(CC) -g -c $< -o $@

title: $(OBJS)

22
将目录要求与文件链接是我认为更好的选择,但您的解决方案也有一个显著的缺点,即对于每个需要重建的文件,都将由make文件调用mkdir进程,其中大多数不需要再次创建目录。当适用于非Linux构建系统(如Windows)时,它实际上会导致无法阻止的错误输出,因为mkdir命令没有-p等效项,更重要的是,由于Shell调用不是最小侵入性的,因此会产生巨大的开销。 - mtalexan
4
为避免尝试创建已存在的目录,我采用了以下方法,而不是直接调用mkdir命令:$(shell [ ! -d $(@D) ] && mkdir -p $(@D)) - Brady

104

假设您使用类Unix环境,这将起作用。

MKDIR_P = mkdir -p

.PHONY: directories

all: directories program

directories: ${OUT_DIR}

${OUT_DIR}:
        ${MKDIR_P} ${OUT_DIR}

这需要在顶层目录中运行,或者${OUT_DIR}的定义相对于运行位置正确。当然,如果你遵循Peter Miller的"递归式Make被视为有害"论文的原则,那么你将会在顶层目录中运行make。

我正在尝试使用RMCH。它需要一些调整来适应我作为测试基地使用的软件套件。该套件有十几个独立的程序,源文件分布在15个目录中,其中一些是共享的。但是,只需小心一点,就可以做到。另一方面,这可能不适合新手。


正如评论中所指出的,将'mkdir'命令列为'directories'操作是错误的。 正如评论中还指出的那样,有其他方法可以修复导致“不知道如何制作output / debug”错误的问题。 其中一种方法是删除对'directories'行的依赖。 这是可行的,因为'mkdir -p'如果被要求创建的所有目录都已经存在,则不会生成错误。 另一种机制是只有在目录不存在时才尝试创建目录。 “经过修改”的版本是我昨晚想到的-但两种技术都有效(如果output / debug存在但是文件而不是目录,则两者都存在问题)。


谢谢Jonathan。当我尝试那个时,我遇到了一个错误:“make: *** No rule to make target output/debug',needed by directories'. Stop."但我现在不会担心它。将坚持基本规则。:)。谢谢你的指导。我只从顶级目录运行"make"。 - Jabez
只需删除目录后面的 ${OUT_DIR},然后它就可以工作了。 - Doc Brown
实现这个需要你捕获每一个可能的情况,其中你使用了命令行中的目录。此外,如果你让任何生成文件构建规则依赖于“directories”,就会导致它们总是重新构建。 - mtalexan
1
@mtalexan,你提供了一些评论来解释一些答案的问题,但是你没有提出替代答案。很想听听你对这个问题的解决方案。 - Samuel
@Samuel我指出了问题,但没有提供解决方案,因为我也在寻找同样的东西,但从未找到解决方案。最终,我只能处理不太理想的解决方案所带来的后果。 - mtalexan
它现在不支持并行执行,即make -j<N> - Maxim Egorushkin

35

或者,简单易行。

DIRS=build build/bins

... 

$(shell mkdir -p $(DIRS))
这将在解析Makefile之后创建所有目录。

3
我喜欢这种方法,因为我不必在每个目标中添加处理目录的命令以防止混乱。 - Ken Williams
1
我只需要在目录不存在的情况下创建目录。这个答案非常适合我的问题。 - Jeff Pal
不仅如此,这还可以防止每个目录的修改时间戳触发不必要的构建步骤。这应该是答案。 - Assimilater
16
这个更好:$(info $(shell mkdir -p $(DIRS))) 没有 $(info ...)mkdir 命令的输出将会被 粘贴到 Makefile 中,最好情况下会导致语法错误。$(info ...) 呼叫确保 a) 错误(如果有的话)能够被用户看到,以及 b) 函数呼叫展开为空。 - cmaster - reinstate monica
1
喜欢这种方法,但在运行 make clean all 时不起作用,因为 clean 规则会删除在解析 Makefile 时创建的目录。该命令在 CI 中也经常使用。 - Graeme

28

make本身可以处理目录和文件的目标,因此编写以下规则很容易:

outDir/someTarget: Makefile outDir
    touch outDir/someTarget

outDir:
    mkdir -p outDir

唯一的问题是,目录的时间戳取决于对其中文件的操作。对于上述规则,这将导致以下结果:

$ make
mkdir -p outDir
touch outDir/someTarget
$ make
touch outDir/someTarget
$ make
touch outDir/someTarget
$ make
touch outDir/someTarget

这绝对不是您想要的。每当您触及文件时,也会触及目录。由于文件依赖于目录,因此文件看起来过时,需要重新构建。

但是,您可以通过告诉make忽略目录的时间戳轻松打破此循环。这可以通过将目录声明为仅作为先决条件而不是依赖项来实现:

# The pipe symbol tells make that the following prerequisites are order-only
#                           |
#                           v
outDir/someTarget: Makefile | outDir
    touch outDir/someTarget

outDir:
    mkdir -p outDir

这将正确产生:

$ make
mkdir -p outDir
touch outDir/someTarget
$ make
make: 'outDir/someTarget' is up to date.

简述:

编写一条规则以创建目录:

$(OUT_DIR):
    mkdir -p $(OUT_DIR)

并且,将内部的目标仅依赖于目录顺序:

$(OUT_DIR)/someTarget: ... | $(OUT_DIR)

你在哪个破损的操作系统/文件系统上看到touch修改了父目录的任何stat数据?这对我来说毫无意义。目录的修改时间仅取决于它包含的文件名。我无法重现你的问题。 - Johan Boulé
1
@JohanBoulé Debian。 - cmaster - reinstate monica
你是否为这种错误的行为提交了一个漏洞报告? - Johan Boulé
1
我不知道为什么这个答案没有得到足够的赞,因为我认为这是一个正确的答案,没有与其他答案相关的问题。谢谢。 - vbezhenar
2
@vbezhenar 只需查看问题及其答案下的日期:我的答案不仅是最后发布的,而且比最近的答案年轻了一年,比得分最高和被接受的答案年轻了整整9年。我只是来晚了。这可能是 SO 评分系统的主要缺点:快速而有些正确的答案往往比迟到但更好的答案得分高得多。这是由于快速回答享有的先发优势,也由于高得分答案更有可能获得额外的投票。 - cmaster - reinstate monica
1
顺便说一下,我发表这个答案的原因正是因为我对其他答案感到非常失望,而且我不想在我的Makefiles中妥协。所以我深入研究了make文档,并进行了一些测试,得出了这个解决方案;我希望其他人能够免受我必须投入的努力,尽管我相当确定它需要几十年甚至更长时间才能使我的答案获得最高评价。 - cmaster - reinstate monica

6
所有解决方案,包括被接受的答案,在其各自的评论中都存在一些问题。@jonathan-leffler的被接受的答案已经非常好,但没有考虑到先决条件不一定按顺序构建(例如在make -j期间)。然而,仅仅将directories先决条件从all移动到program会在每次运行时引发重建,据我所知。 以下解决方案没有这个问题,并且据我所知可以正常工作。
MKDIR_P := mkdir -p
OUT_DIR := build

.PHONY: directories all clean

all: $(OUT_DIR)/program

directories: $(OUT_DIR)

$(OUT_DIR):
    ${MKDIR_P} $(OUT_DIR)

$(OUT_DIR)/program: | directories
    touch $(OUT_DIR)/program

clean:
    rm -rf $(OUT_DIR)

4

我刚刚想出了一个相当合理的解决方案,可以让您定义要构建的文件,并自动创建目录。首先,定义一个变量ALL_TARGET_FILES,其中包含您的makefile将构建的每个文件的文件名。然后使用以下代码:

define depend_on_dir
$(1): | $(dir $(1))

ifndef $(dir $(1))_DIRECTORY_RULE_IS_DEFINED
$(dir $(1)):
    mkdir -p $$@

$(dir $(1))_DIRECTORY_RULE_IS_DEFINED := 1
endif
endef

$(foreach file,$(ALL_TARGET_FILES),$(eval $(call depend_on_dir,$(file))))

这是它的工作原理。我定义了一个名为depend_on_dir的函数,它接受一个文件名并生成一个规则,使得该文件依赖于包含它的目录,并定义一个规则以在必要时创建该目录。然后,我使用foreach在每个文件名上调用此函数,并eval结果。
请注意,您需要一个支持eval的GNU make版本,我认为版本号应高于3.81。

创建一个变量来保存“makefile将构建的每个文件的文件名”有点繁琐 - 我喜欢定义我的顶级目标,然后是它们所依赖的内容,以此类推。拥有所有目标文件的平面列表违反了makefile规范的分层性质,并且当目标文件依赖于运行时计算时可能不容易实现。 - Ken Williams

3

考虑到您是新手,我建议您暂时不要尝试这样做。虽然这是可行的,但会使您的Makefile变得更加复杂。在您更熟悉make之前,请坚持使用简单的方法。

话虽如此,一种在与源目录不同的目录中构建的方法是VPATH;我更喜欢模式规则


3

对于我来说,操作系统的独立性非常重要,所以 mkdir -p 不是一个选项。我创建了一系列的函数,使用 eval 来创建目录,并将其前置在父目录上。这样做的好处是,由于正确地确定了依赖关系,因此 make -j 2 将正常工作,没有任何问题。

# convenience function for getting parent directory, will eventually return ./
#     $(call get_parent_dir,somewhere/on/earth/) -> somewhere/on/
get_parent_dir=$(dir $(patsubst %/,%,$1))

# function to create directory targets.
# All directories have order-only-prerequisites on their parent directories
# https://www.gnu.org/software/make/manual/html_node/Prerequisite-Types.html#Prerequisite-Types
TARGET_DIRS:=
define make_dirs_recursively
TARGET_DIRS+=$1
$1: | $(if $(subst ./,,$(call get_parent_dir,$1)),$(call get_parent_dir,$1))
    mkdir $1
endef

# function to recursively get all directories 
#     $(call get_all_dirs,things/and/places/) -> things/ things/and/ things/and/places/
#     $(call get_all_dirs,things/and/places) -> things/ things/and/
get_all_dirs=$(if $(subst ./,,$(dir $1)),$(call get_all_dirs,$(call get_parent_dir,$1)) $1)

# function to turn all targets into directories
#     $(call get_all_target_dirs,obj/a.o obj/three/b.o) -> obj/ obj/three/
get_all_target_dirs=$(sort $(foreach target,$1,$(call get_all_dirs,$(dir $(target)))))

# create target dirs
create_dirs=$(foreach dirname,$(call get_all_target_dirs,$1),$(eval $(call make_dirs_recursively,$(dirname))))

TARGETS := w/h/a/t/e/v/e/r/things.dat w/h/a/t/things.dat

all: $(TARGETS)

# this must be placed after your .DEFAULT_GOAL, or you can manually state what it is
# https://www.gnu.org/software/make/manual/html_node/Special-Variables.html
$(call create_dirs,$(TARGETS))

# $(TARGET_DIRS) needs to be an order-only-prerequisite
w/h/a/t/e/v/e/r/things.dat: w/h/a/t/things.dat | $(TARGET_DIRS)
    echo whatever happens > $@

w/h/a/t/things.dat: | $(TARGET_DIRS)
    echo whatever happens > $@

例如,运行以上代码将创建:
$ make
mkdir w/
mkdir w/h/
mkdir w/h/a/
mkdir w/h/a/t/
mkdir w/h/a/t/e/
mkdir w/h/a/t/e/v/
mkdir w/h/a/t/e/v/e/
mkdir w/h/a/t/e/v/e/r/
echo whatever happens > w/h/a/t/things.dat
echo whatever happens > w/h/a/t/e/v/e/r/things.dat

命令mkdir -p是UNIX标准(Open Group Base Specifications Issue 7,2018版,也称POSIX)中的一部分。我熟悉的所有类Unix操作系统都符合POSIX标准。我很好奇:您考虑的哪个类Unix操作系统既支持您在此处使用的GNU make功能,又没有mkdir -p - MetaEd
@MetaEd,显然是我正在使用的那个 ;-)。问题和回答并没有表明这是针对Unixy操作系统的。我正在使用某个可以运行的Make版本的Windows。 - SimplyKnownAsG
编写可在Unix和Windows之间移植的makefile是一个值得称赞的目标,甚至可能是唐吉诃德式的。话虽如此,OP在Unix上的上下文中提出了问题,但并没有明确说明。一个线索是他们的目标文件名为.o,而不是.OBJ。 - MetaEd
我看到这对你很重要。继续去贬低吧!我希望它能给你带来极大的满足和内心的平静。 - SimplyKnownAsG
无意贬低。我想表达的是,我也非常重视可移植性,并且Unix和Windows之间的差异使得它们之间的makefile可移植性可能是一个难以实现的目标。 - MetaEd

1

0
src_dir := src
obj_dir := obj
build_dir := build

dirs := $(src_dir) $(obj_dir) $(build_dir)   # new variable


all: $(dirs) $(other_dependencies)           # added dependency (*before* any others)

$(dirs):                                     # rule which makes missing directories
    mkdir $@
  1. 不会因为"无法创建目录"错误消息而使您的终端混乱。如果目录已经存在,则无需构建它们。
  2. 像任何其他依赖项一样工作,只需要一个规则和一个变量。

我得到了 % make graphs Makefile:73: 警告:覆盖目标“graphs”的命令 Makefile:69: 警告:忽略目标“graphs”的旧命令 make: 循环依赖图形<- 图形依赖已删除。也许我太过于严格地遵循你的代码了? - Harry Moreno

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