【问题标题】:How to write a Makefile with separate source and header directories?如何编写具有单独源目录和头目录的 Makefile?
【发布时间】:2026-02-16 11:10:02
【问题描述】:

关注this tutorial...

我有 2 个源文件和 1 个头文件。我想将它们放在教程中的单独目录中。

所以我设置了这个项目:

.
├── include
│   └── hellomake.h
├── Makefile
└── src
    ├── hellofunc.c
    └── hellomake.c

生成文件:

IDIR =../include
CC=gcc
CFLAGS=-I$(IDIR)

ODIR=obj
LDIR =../lib

_DEPS = hellomake.h
DEPS = $(patsubst %,$(IDIR)/%,$(_DEPS))

_OBJ = hellomake.o hellofunc.o 
OBJ = $(patsubst %,$(ODIR)/%,$(_OBJ))

$(ODIR)/%.o: %.c $(DEPS)
    $(CC) -c -o $@ $< $(CFLAGS)

hellomake: $(OBJ)
    gcc -o $@ $^ $(CFLAGS)

.PHONY: clean

clean:
    rm -f $(ODIR)/*.o *~ core $(INCDIR)/*~ 

我生成的错误说:

gcc -o hellomake  -I../include
gcc: fatal error: no input files
compilation terminated.
make: *** [hellomake] Error 4

发生了什么事?

【问题讨论】:

  • 发布您的实际代码,并对其进行格式化以使其可读
  • 我认为我不应该为 gcc 提供一个文件,所有文件都包含在 makefile 中,我调用的所有文件都在目录中,因为所有 3 个文件都已包含在 makefile 中
  • 在调试 make 文件时,我发现一个有用的技巧是设置一个“显示”目标(或任何名称),没有仅打印变量的依赖项,即:@echo "OBJ=$(OBJ)"(一行用于每个变量)。然后使用make show,您将清楚地看到它们是否包含正确的内容。
  • Makefile 的目的是调用命令。其中之一是 gcc 例如。如果你不向gcc 提供要链接在一起的文件,它就不会发明它。
  • $info 几乎可以放在 makefile 中的任何位置,而 echo 只能放在变量声明或规则体中。

标签: c++ c makefile gnu-make


【解决方案1】:

您的教程宣传了旧的和不良的做法,恕我直言,您应该避免它。

在这里你的规则:

$(ODIR)/%.o: %.c $(DEPS)

您告诉 make 在当前目录中查找源,而它们实际上位于 src 目录中,因此这种模式从未使用过,您也没有合适的模式。


确保你像这样组织你的项目目录:

root
├── include/
│   └── all .h files here
├── lib/
│   └── all third-party library files (.a/.so files) here
├── src/
│   └── all .c files here
└── Makefile

然后让我们使用良好的做法一步一步地完成这个过程。

首先,如果不需要,请不要定义任何内容。 Make 有很多预定义的变量和函数,您应该在尝试手动操作之前使用它们。其实他有这么多,你可以编译一个简单的文件,甚至目录里没有Makefile!

  1. 列出您的源代码并构建输出目录:

     SRC_DIR := src
     OBJ_DIR := obj
     BIN_DIR := bin # or . if you want it in the current directory
    
  2. 命名您的最终目标,即您的可执行文件:

     EXE := $(BIN_DIR)/hellomake
    
  3. 列出您的源文件:

     SRC := $(wildcard $(SRC_DIR)/*.c)
    
  4. 从源文件中,列出目标文件:

     OBJ := $(SRC:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)
     # You can also do it like that
     OBJ := $(patsubst $(SRC_DIR)/%.c, $(OBJ_DIR)/%.o, $(SRC))
    
  5. 现在让我们处理标志

     CPPFLAGS := -Iinclude -MMD -MP # -I is a preprocessor flag, not a compiler flag
     CFLAGS   := -Wall              # some warnings about bad code
     LDFLAGS  := -Llib              # -L is a linker flag
     LDLIBS   := -lm                # Left empty if no libs are needed
    

(CPP 在这里代表 C PreP处理器,而不是 CPlusPlus!使用 CXXFLAGS 代表 C++ 标志和 @987654332 @ 用于 C++ 编译器。)

-MMD -MP 标志用于自动生成标头依赖项。稍后我们将使用它来在只有标头更改时触发编译。

好的,现在我们的变量已正确填充,是时候推出一些食谱了。

默认目标应该被称为all 并且它应该是你的 Makefile 中的第一个目标,这一点被广泛传播。它的先决条件应该是你在命令行上只写make时要构建的目标:

all: $(EXE)

一个问题是 Make 会认为我们实际上想要创建一个名为 all 的文件或文件夹,所以让我们告诉他这不是一个真正的目标:

.PHONY: all

现在列出构建可执行文件的先决条件,并填写其配方以告诉 make 如何处理这些:

$(EXE): $(OBJ)
    $(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@

CC 代表 C C编译器。)

请注意,您的$(BIN_DIR) 可能还不存在,因此对编译器的调用可能会失败。让我们告诉 make 你希望它首先检查它:

$(EXE): $(OBJ) | $(BIN_DIR)
    $(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@

$(BIN_DIR):
    mkdir -p $@

一些快速的附加说明:

  • $(CC) 是一个内置变量,已经包含在 C 中编译和链接时所需的内容
  • 为避免链接器错误,强烈建议将$(LDFLAGS)放在您的目标文件和$(LDLIBS)之后
  • $(CPPFLAGS)$(CFLAGS)在这里没用,编译阶段已经结束,这里是链接阶段

下一步,由于您的源文件和目标文件不共享相同的前缀,您需要准确地告诉 make 要做什么,因为它的内置规则不涵盖您的具体情况:

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

同样的问题,您的$(OBJ_DIR) 可能还不存在,因此对编译器的调用可能会失败。让我们更新规则:

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c | $(OBJ_DIR)
    $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

$(BIN_DIR) $(OBJ_DIR):
    mkdir -p $@

好的,现在可执行文件应该可以很好地构建了。我们想要一个简单的规则来清理构建工件:

clean:
    @$(RM) -rv $(BIN_DIR) $(OBJ_DIR) # The @ disables the echoing of the command

(再次重申,clean 不是需要创建的目标,因此请将其添加到 .PHONY 特殊目标中!)

最后一件事。还记得自动依赖生成吗? GCC 和 Clang 将创建与您的 .o 文件对应的 .d 文件,其中包含供我们使用的 Makefile 规则,因此我们将其包含在此处:

-include $(OBJ:.o=.d) # The dash is used to silence errors if the files don't exist yet

最终结果:

SRC_DIR := src
OBJ_DIR := obj
BIN_DIR := bin

EXE := $(BIN_DIR)/hellomake
SRC := $(wildcard $(SRC_DIR)/*.c)
OBJ := $(SRC:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)

CPPFLAGS := -Iinclude -MMD -MP
CFLAGS   := -Wall
LDFLAGS  := -Llib
LDLIBS   := -lm

.PHONY: all clean

all: $(EXE)

$(EXE): $(OBJ) | $(BIN_DIR)
    $(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c | $(OBJ_DIR)
    $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

$(BIN_DIR) $(OBJ_DIR):
    mkdir -p $@

clean:
    @$(RM) -rv $(BIN_DIR) $(OBJ_DIR)

-include $(OBJ:.o=.d)

【讨论】:

  • 谢谢!写得很好的关于如何编写 makefile 的介绍:-)
  • 很棒的报道。我一直在使用这些家伙多年来一直在使用的相同教程!这是如此清晰和简洁。
  • 这很棒。这最终让我可以按照自己想要的方式进行组织,而无需在超空间中乱搞(autoconf)
  • makefile 需要制表符,而不是空格。请注意,从此处复制时。
  • 关于 SO 上 Makefile 的最佳答案之一。有没有可以链接到的博客文章,它描述的和你一样清楚?我想扩展您的 Makefile,使其能够创建发布和调试版本。
【解决方案2】:

没有特定“目标”的 make 实用程序将创建文件中的第一个目标。

第一个目标通常被命名为'all'

对于已发布的文件,将制作目标文件,并且在命令行中未给出目标时不会继续制作可执行文件

建议如下:

SHELL := /bin/sh

# following so could define executable name on command line
# using the '-D' parameter
#ifndef $(NAME)
    NAME := hellomake
#endif

# use ':=' so macros only evaluated once


MAKE    :=  /usr/bin/make
CC      :=  /usr/bin/gcc

CFLAGS  := -g -Wall -Wextra -pedantic
LFLAGS  :=

ODIR    := obj
IDIR    := ../include
LIBS    :=
LIBPATH := ../lib

DEPS    := $(wildcard $(IDIR)/*.h)
SRCS    := $(wildcard *.c)
OBJS    := $(SRCS:.c=.o)

.PHONY: all
all: $(NAME) $(OBJS)

$(ODIR)/%.o: %.c $(DEPS)
    $(CC) $(CFLAGS) -c -o $@ $< -I$(DEPS)

$(NAME): $(OBJS)
    $(CC) $(LFLAGS) -o $@ $^ -L$(LIBPATH) -l$(LIBS)

.PHONY: clean
clean:
    rm -f $(ODIR)/*.o
    rm -f $(NAME)


however, in your proposed project,
not every source file needs every header file
so should use either gcc or sed to generate the dependency files
then use makefile rules similar to the following,
which may need a little 'tweaking' for your project
because the include files are not in the same directory
as the source files:

DEP := $(SRCS:.c=.d)

#
#create dependency files
#
%.d: %.c 
    # 
    # ========= START $< TO $@ =========
    $(CC) -M $(CPPFLAGS) $< > $@.$$$$;                      \
    sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@;     \
    rm -f $@.$$$$
    # ========= END $< TO $@ =========

# 
# compile the .c files into .o files using the compiler flags
#
%.o: %.c %.d 
     # 
     # ========= START $< TO $@ =========
     $(CC) $(CCFLAGS) -c $< -o $@ -I$(IDIR) 
     # ========= END $< TO $@ =========
     # 

# include the contents of all the .d files
# note: the .d files contain:
# <filename>.o:<filename>.c plus all the dependencies for that .c file 
# I.E. the #include'd header files
# wrap with ifneg... so will not rebuild *.d files when goal is 'clean'
#
ifneq "$(MAKECMDGOALS)" "clean"
-include $(DEP)
endif

【讨论】:

  • 您的评论不正确。模式规则和后缀规则(隐式规则)不能算作默认目标。上面生成文件中的第一个显式规则是helloworld,如果在命令行上没有给出其他目标,则将创建该目标。虽然添加all 目标不是一个坏主意,但它不是必需的。此外,您对使用 -D 的评论是错误的:make 不使用 -D 来定义变量。而且您不能在 makefile 中编写像 #ifdef 这样的预处理器语句(这只是一个注释)。这里还有其他问题。
【解决方案3】:

简单的 Makefile 定义对我来说似乎没问题,因为它们出现在您的问题中。尝试在文件名之前指定编译器选项:

$(ODIR)/%.o: %.c $(DEPS)
    $(CC) $(CFLAGS) -c -o $@ $<

hellomake: $(OBJ)
    gcc $(CFLAGS) -o $@ $^

您需要从源目录运行make

【讨论】:

  • 切换标志的顺序无济于事......问题是命令行中实际上缺少目标文件。
  • @MadScientist:切换选项不是解决方案,但它是可取的。至于为什么$(OBJ)为空,要么是OP没有使用Gnu make,要么是问题中缺少了什么。