如何将目标文件放在单独的子目录中

83

我尝试使用make将目标文件放置在单独的子目录中时遇到了问题,这可能是一种非常基本的技术。我已经尝试使用此页面中的信息: http://www.gnu.org/software/hello/manual/make/Prerequisite-Types.html#Prerequisite-Types

运行make后,我得到了以下输出:

make: *** No rule to make target `ku.h', needed by `obj/kumain.o'.  Stop.
然而,ku.h是一个依赖项,而不是一个目标(尽管它显然被包含在C源文件中)。当我不试图使用子目录来存储对象文件(即省略OBJDIR部分)时,它可以正常工作。为什么make会把ku.h视为目标?
我的makefile如下:(这种风格是在阅读各种信息源后得出的)
.SUFFIXES:
.SUFFIXES: .c .o

CC=gcc 
CPPFLAGS=-Wall
LDLIBS=-lhpdf
VPATH=%.c src
VPATH=%.h src
VPATH=%.o obj
OBJDIR=obj

objects= $(addprefix $(OBJDIR)/, kumain.o kudlx.o kusolvesk.o kugetpuz.o kuutils.o \
  kurand.o kuASCboard.o kuPDFs.o kupuzstrings.o kugensud.o \
  kushapes.o )

ku : $(objects)
  $(CC) $(CPPFLAGS) -o ku $(objects) $(LDLIBS)

$(objects) : ku.h kudefines.h kuglobals.h kufns.h | $(OBJDIR)

$(OBJDIR):
  mkdir $(OBJDIR)

.PHONY: clean
clean :
  rm $(objects)

编辑: 我已经应用了使用vpath指令的更改。我的版本是VPATH=xxx和vpath %.c xxx的糟糕混合体。但是,现在我遇到另一个问题(在添加错误的vpath之前是原始问题)。以下是现在的输出:

    gcc  -o ku -lhpdf obj/kumain.o obj/kudlx.o obj/kusolvesk.o ..etc
    gcc: obj/kumain.o: No such file or directory
    gcc: obj/kudlx.o: No such file or directory
    gcc: obj/kusolvesk.o: No such file or directory
    gcc: obj/kugetpuz.o: No such file or directory
    gcc: obj/kuutils.o: No such file or directory
    gcc: obj/kurand.o: No such file or directory
    gcc: obj/kuASCboard.o: No such file or directory
    gcc: obj/kuPDFs.o: No such file or directory
    gcc: obj/kupuzstrings.o: No such file or directory
    gcc: obj/kugensud.o: No such file or directory
    gcc: obj/kushapes.o: No such file or directory
    make: *** [ku] Error 1
看起来 make 没有应用对象文件的隐式规则,尽管手册上说:“隐式规则告诉 make 如何使用常规技术,这样当您想要使用它们时,您就不必详细指定它们。例如,C 编译有一个隐式规则。文件名确定运行哪些隐式规则。例如,C 编译通常需要一个 .c 文件并生成一个 .o 文件。因此,当 make 看到这种文件名后缀组合时,会应用 C 编译的隐式规则。” 以及 “在考虑隐式规则时,make 还会在 VPATH 中指定的目录或使用 vpath 指定的目录中搜索(请参阅使用隐式规则)。”
同样,在这里,“例如,当文件 foo.o 没有显式规则时,make 会考虑隐式规则,例如编译 foo.c 的内置规则(如果该文件存在)。如果当前目录中缺少这样的文件,则会在适当的目录中搜索它。如果 foo.c 存在(或在任何目录中被 makefile 提及),则应用 C 编译的隐式规则。”
如何使隐式规则在我的 makefile 中工作,任何帮助都将不胜感激。
修改2: 由于我无法使用隐式规则,所以感谢 Jack Kelly 我已经制定了显式规则来编译 .c 文件。也感谢 al_miro 提供的 vpath 信息。
这是有效的 makefile:
.SUFFIXES:
.SUFFIXES: .c .o

CC=gcc 
CPPFLAGS=-Wall
LDLIBS=-lhpdf
OBJDIR=obj
vpath %.c src
vpath %.h src

objects = $(addprefix $(OBJDIR)/, kumain.o kudlx.o kusolvesk.o kugetpuz.o kuutils.o \
  kurand.o kuASCboard.o kuPDFs.o kupuzstrings.o kugensud.o \
  kushapes.o )

ku : $(objects)
  $(CC) $(CPPFLAGS) -o ku $(objects) $(LDLIBS)

$(OBJDIR) obj/%.o : %.c ku.h kudefines.h kuglobals.h kufns.h 
  $(CC) -c $(CPPFLAGS) $< -o $@

.PHONY : clean
clean :
  rm $(objects)

16
注意事项:$(CPPFLAGS) 通常用于 C 预处理器标志,而 $(CFLAGS) 用于编译器的标志。 - Jack Kelly
9个回答

72

由于您正在使用GNUmake,请使用模式规则来编译对象文件:

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

6
这对我不起作用:它查找的是$(OBJDIR)/%.c而不是%.c。有什么办法可以让它在以$(OBJDIR)为目标的同时使用当前目录中的C文件作为依赖项? - Lelanthran
@Lelanthran,获取C文件的上述解决方案是在当前目录中。如果您使用"$(OBJDIR)/%.c"而不是"%.c",则您的C代码应该放在"$(OBJDIR)"中。 - Neal
也可以参考这个答案:https://codereview.stackexchange.com/a/74139/167000 - p8me
1
如何创建$(OBJDIR)?如果我使用规则$(OBJDIR)/%.o: %.c $(OBJDIR) $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<,它每次都会从头重新编译。 - roschach
7
虽然我很欣赏这样简洁的回答,但作为一个完全的Makefile初学者,我只想知道将目标文件输出到不同目录的标准命令。所有隐含的快捷方式和类似这样的东西都会让答案更加混乱。 - AmagicalFishy
显示剩余3条评论

71
这是我在大多数项目中使用的Makefile,它允许将源文件、头文件和内联文件放置在子文件夹、子文件夹的子文件夹等,而且会为每个目标自动生成依赖文件。这意味着修改头文件和内联文件将触发与其相关的文件的重新编译。
通过shell find命令检测源文件,因此不需要显式指定,只需随心所欲地编写代码即可。
当项目编译时,它还将从“资源”文件夹复制所有文件到bin文件夹中,我发现这很方便。
为了表彰功劳,自动依赖性功能主要基于Scott McPeak的页面,可以在这里找到,对我的需求进行了一些其他的修改/调整。 示例Makefile:
#Compiler and Linker
CC          := g++-mp-4.7

#The Target Binary Program
TARGET      := program

#The Directories, Source, Includes, Objects, Binary and Resources
SRCDIR      := src
INCDIR      := inc
BUILDDIR    := obj
TARGETDIR   := bin
RESDIR      := res
SRCEXT      := cpp
DEPEXT      := d
OBJEXT      := o

#Flags, Libraries and Includes
CFLAGS      := -fopenmp -Wall -O3 -g
LIB         := -fopenmp -lm -larmadillo
INC         := -I$(INCDIR) -I/usr/local/include
INCDEP      := -I$(INCDIR)

#---------------------------------------------------------------------------------
#DO NOT EDIT BELOW THIS LINE
#---------------------------------------------------------------------------------
SOURCES     := $(shell find $(SRCDIR) -type f -name *.$(SRCEXT))
OBJECTS     := $(patsubst $(SRCDIR)/%,$(BUILDDIR)/%,$(SOURCES:.$(SRCEXT)=.$(OBJEXT)))

#Defauilt Make
all: resources $(TARGET)

#Remake
remake: cleaner all

#Copy Resources from Resources Directory to Target Directory
resources: directories
    @cp $(RESDIR)/* $(TARGETDIR)/

#Make the Directories
directories:
    @mkdir -p $(TARGETDIR)
    @mkdir -p $(BUILDDIR)

#Clean only Objecst
clean:
    @$(RM) -rf $(BUILDDIR)

#Full Clean, Objects and Binaries
cleaner: clean
    @$(RM) -rf $(TARGETDIR)

#Pull in dependency info for *existing* .o files
-include $(OBJECTS:.$(OBJEXT)=.$(DEPEXT))

#Link
$(TARGET): $(OBJECTS)
    $(CC) -o $(TARGETDIR)/$(TARGET) $^ $(LIB)

#Compile
$(BUILDDIR)/%.$(OBJEXT): $(SRCDIR)/%.$(SRCEXT)
    @mkdir -p $(dir $@)
    $(CC) $(CFLAGS) $(INC) -c -o $@ $<
    @$(CC) $(CFLAGS) $(INCDEP) -MM $(SRCDIR)/$*.$(SRCEXT) > $(BUILDDIR)/$*.$(DEPEXT)
    @cp -f $(BUILDDIR)/$*.$(DEPEXT) $(BUILDDIR)/$*.$(DEPEXT).tmp
    @sed -e 's|.*:|$(BUILDDIR)/$*.$(OBJEXT):|' < $(BUILDDIR)/$*.$(DEPEXT).tmp > $(BUILDDIR)/$*.$(DEPEXT)
    @sed -e 's/.*://' -e 's/\\$$//' < $(BUILDDIR)/$*.$(DEPEXT).tmp | fmt -1 | sed -e 's/^ *//' -e 's/$$/:/' >> $(BUILDDIR)/$*.$(DEPEXT)
    @rm -f $(BUILDDIR)/$*.$(DEPEXT).tmp

#Non-File Targets
.PHONY: all remake clean cleaner resources

'mp' 意味着 MacPorts。g++-mp-4.7 是来自 GCC 的 C++ 编译器,适用于 MacPorts。 - The Beast
1
这看起来好像编译规则不会在头文件更改时触发,只有源文件更改才会触发。 - Shayna
在*.$(SRCEXT)周围添加“ ”,以便查找源文件时可以进行递归搜索(第26行)。 - Shlomo Fel
2
在制作目标资源时,请使用 cp -r 进行递归复制,并复制 $(RESDIR)/. 而不是 *,这样即使资源目录为空也可以正常工作。 - Shlomo Fel
1
不要使用cp -r进行递归复制。-r是一个历史选项,不应再使用。如果您想要递归复制,请使用cp -R。 - undefined
@ryandesign 这可能适用于 macOS 版本的 cpman cp macOS),但并不适用于所有版本的 cp。例如,man cp GNU coreutils 8.32。还有这个 linuxman cp,它区分 -r-R - undefined

25
VPATH 行有误,应该改成:
vpath %.c  src
vpath %.h  src
即不要大写且不带 = 号。目前情况是它找不到.h文件并认为它是一个需要生成的目标。

10

2
其实,这是最好的答案。有趣的是,在我点赞之前,所有那些不成熟、不完整、错误等尝试都有得分。;) 感谢你,Theo B.,你为我省了几小时! - Sz.

5

从输出目录构建

与从顶层目录构建不同,可以从输出目录构建。您可以通过设置vpath来访问源目录。此选项的优点是可以使用内置规则。

build.sh

#!/bin/bash
mkdir -p obj
cp Makefile.template obj/Makefile
cd obj
make "$*"

Makefile

.SUFFIXES:
.SUFFIXES: .c .o

CC=gcc 
CPPFLAGS=-Wall
LDLIBS=-lhpdf
VPATH=%.c ../src
VPATH=%.h ../src

objects=kumain.o kudlx.o kusolvesk.o kugetpuz.o kuutils.o \
  kurand.o kuASCboard.o kuPDFs.o kupuzstrings.o kugensud.o \
  kushapes.o

ku : $(objects)

$(objects) : ku.h kudefines.h kuglobals.h kufns.h

.PHONY: clean
clean :
  rm $(objects)

缺点是错误信息与当前工作目录不匹配。可以通过跳过build.sh,直接从obj目录构建来解决这个问题。
另一个优点是这种方法有一定的流行度。 cmake 也以类似的方式工作。

按输出选项创建规则

以下解决方案在我看来不是很好,因为我非常喜欢内置规则。然而,GNU make不支持像vpath那样针对输出目录的选项。而内置规则无法匹配,因为%.o中的%会匹配obj/foo.o中的obj/foo,导致makevpath %.c src/中搜索src/obj/foo.c之类的内容,但不能搜索src/foo.c
但这是最接近内置规则的方法,因此根据我的了解是可用的最佳解决方案。
$(OBJDIR)/%.o: %.c
        $(COMPILE.c) $(OUTPUT_OPTION) $<

解释:实际上,“$(COMPILE.c) $(OUTPUT_OPTION) $<”是如何实现“.c.o”的,详见http://git.savannah.gnu.org/cgit/make.git/tree/default.c(甚至在手册中都有提到)。
此外,如果“$(OBJDIR)”只包含自动生成的文件,您可以通过一个仅限于顺序的先决条件来动态创建它,使清理规则稍微简单些。
$(OBJDIR):
        mkdir -p $(OBJDIR)

$(OBJDIR)/%.o: %.c | $(OBJDIR)
        $(COMPILE.c) $(OUTPUT_OPTION) $<

.PHONY: clean
clean:
        $(RM) -r $(OBJDIR)

这要求特性order-only可用,你可以使用$(filter order-only, $(.FETAURES))进行检查。我在Kubuntu 14.04 GNU make 3.81和OpenSUSE 13.1 GNU make 3.82上进行了检查。两者都启用了order-only,现在我很困惑为什么Kubuntu 14.04带有比OpenSUSE 13.1更旧版本的GNU make。无论如何,我现在要下载make 4.1 :)


2

如果你正在使用这样的目录结构:

project
    > src
        > pkgA     
        > pkgB
        ...
    > bin
        > pkgA
        > pkgB
        ...

以下内容对我非常有效。我自己制作了这个,使用GNU make手册作为我的主要参考;这个特别是对于我的最后一个规则非常有帮助,这也是我最重要的规则。
我的Makefile:
PROG := sim
CC := g++
ODIR := bin
SDIR := src
MAIN_OBJ := main.o
MAIN := main.cpp
PKG_DIRS := $(shell ls $(SDIR))
CXXFLAGS = -std=c++11 -Wall $(addprefix -I$(SDIR)/,$(PKG_DIRS)) -I$(BOOST_ROOT)
FIND_SRC_FILES = $(wildcard $(SDIR)/$(pkg)/*.cpp) 
SRC_FILES = $(foreach pkg,$(PKG_DIRS),$(FIND_SRC_FILES)) 
OBJ_FILES = $(patsubst $(SDIR)/%,$(ODIR)/%,\
$(patsubst %.cpp,%.o,$(filter-out $(SDIR)/main/$(MAIN),$(SRC_FILES))))

vpath %.h $(addprefix $(SDIR)/,$(PKG_DIRS))
vpath %.cpp $(addprefix $(SDIR)/,$(PKG_DIRS)) 
vpath $(MAIN) $(addprefix $(SDIR)/,main)

# main target
#$(PROG) : all
$(PROG) : $(MAIN) $(OBJ_FILES) 
    $(CC) $(CXXFLAGS) -o $(PROG) $(SDIR)/main/$(MAIN) 

# debugging
all : ; $(info $$PKG_DIRS is [${PKG_DIRS}])@echo Hello world

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

# This one right here, folks. This is the one.
$(OBJ_FILES) : $(ODIR)/%.o : $(SDIR)/%.h
    $(CC) $(CXXFLAGS) -c $< -o $@

# for whatever reason, clean is not being called...
# any ideas why???
.PHONY: clean

clean :
    @echo Build done! Cleaning object files...
    @rm -r $(ODIR)/*/*.o

通过使用$(SDIR)/%.h作为$(ODIR)/%.o的先决条件,这迫使make在源包目录中查找源代码而非与目标文件相同的文件夹中查找。
我希望这能帮助一些人。如果您发现我提供的内容有任何问题,请让我知道。
顺便说一下:正如您从我的最后一个评论中看到的那样,clean未被调用,我不知道为什么。有什么想法吗?

1

对于所有使用隐式规则(和GNU MAKE)的人。这里是一个支持不同目录的简单makefile:

#Start of the makefile

VPATH = ./src:./header:./objects

OUTPUT_OPTION = -o objects/$@

CXXFLAGS += -Wall -g -I./header

Target = $(notdir $(CURDIR)).exe

Objects := $(notdir $(patsubst %.cpp,%.o,$(wildcard src/*.cpp)))



all: $(Target)

$(Target): $(Objects)
     $(CXX) $(CXXFLAGS) -o $(Target) $(addprefix objects/,$(Objects))


#Beware of -f. It skips any confirmation/errors (e.g. file does not exist)

.PHONY: clean
clean:
     rm -f $(addprefix objects/,$(Objects)) $(Target)

让我们仔细看一下(我将用curdir来指代当前目录):

这行代码用于获取curdir/src中使用的.o文件列表。

Objects := $(notdir $(patsubst %.cpp,%.o,$(wildcard src/*.cpp)))
#expands to "foo.o myfoo.o otherfoo.o"

通过变量,可以将输出设置为不同的目录(curdir/objects)。
OUTPUT_OPTION = -o objects/$@
#OUTPUT_OPTION will insert the -o flag into the implicit rules

为了确保编译器能够在新的对象文件夹中找到对象,需要将路径添加到文件名中。
$(Target): $(Objects)
     $(CXX) $(CXXFLAGS) -o $(Target) $(addprefix objects/,$(Objects))
#                                    ^^^^^^^^^^^^^^^^^^^^    

这只是一个示例,肯定还有改进的空间。

如需更多信息,请参阅: 制作文档。请参见第10.2章节

或者: Oracle:编程实用工具指南


1
您可以在编译命令中指定 -o $@ 选项,强制将编译命令的输出名称设置为目标名称。例如,如果您有以下文件:
  • 源文件: cpp/class.cpp 和 cpp/driver.cpp
  • 头文件: headers/class.h

...并且您想要将对象文件放置在:

  • 目标文件:obj/class.o 和 obj/driver.o

...那么您可以将 cpp/class.cpp 和 cpp/driver.cpp 分别编译成 obj/class.o 和 obj/driver.o,然后使用以下 Makefile 进行链接:

CC=c++
FLAGS=-std=gnu++11

INCS=-I./headers
SRC=./cpp
OBJ=./obj
EXE=./exe

${OBJ}/class.o:    ${SRC}/class.cpp
                   ${CC} ${FLAGS} ${INCS} -c $< -o $@

${OBJ}/driver.o:    ${SRC}/driver.cpp ${SRC}/class.cpp
                    ${CC} ${FLAGS} ${INCS} -c $< -o $@

driver: ${OBJ}/driver.o ${OBJ}/class.o
        ${CC} ${FLAGS} ${OBJ}/driver.o ${OBJ}/class.o -o ${EXE}/driver

0

这些答案似乎都不够简单 - 问题的关键在于不需要重新构建:

Makefile

OBJDIR=out
VPATH=$(OBJDIR)

# make will look in VPATH to see if the target needs to be rebuilt
test: moo
    touch $(OBJDIR)/$@

使用示例

touch moo
# creates out/test
make test
# doesn't update out/test
make test
# will now update test
touch moo
make test

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