使用 GNU make 构建 C 程序 "out of source tree"

Building C-program "out of source tree" with GNU make

我想使用 GNU make 工具为我的微控制器构建一个 C 项目。我想以一种干净的方式来做,这样我的源代码就不会在构建后被目标文件和其他东西弄得乱七八糟。假设我有一个名为 "myProject" 的项目文件夹,其中包含两个文件夹:

- myProject
     |
     |---+ source
     |
     '---+ build

构建文件夹只包含一个生成文件。下图显示了当我 运行 GNU make 工具时会发生什么:

所以 GNU make 应该为它在源文件夹中找到的每个 .c 源文件创建一个目标文件。目标文件的结构应该类似于源文件夹中的结构。

GNU make 还应该为每个 .c 源文件制作一个 .d 依赖文件(事实上,依赖文件本身就是某种 makefile)。依赖文件在 GNU make 手册第 4.14 章中描述 "Generating Prerequisites Automatically":

For each source file name.c there is a makefile name.d which lists what files the object file name.o depends on.

从下面的 Whosebug 问题 ,我了解到将选项 -MMD-MP 添加到 GNU gcc 编译器的 CFLAGS 可以帮助自动化.

那么问题来了。有没有人有执行这种源外构建的示例生成文件?或者关于如何开始的一些好的建议?

我敢肯定大多数编写过这样的 makefile 的人都是 Linux 人。但是微控制器项目也应该建立在 Windows 机器上。无论如何,即使您的 makefile 是 Linux-only,它也提供了一个很好的起点 ;-)

PS:我想避免额外的工具,例如 CMake、Autotools 或任何与 IDE 有关的工具。纯 GNU make。

我将不胜感激:-)


正在更新依赖文件
请看看这个问题:

这个相当小的 makefile 应该可以解决问题:

VPATH = ../source
OBJS = FolderA/fileA1.o FolderA/fileA2.o FolderB/fileB1.o
CPPFLAGS = -MMD -MP

all: init myProgram

myProgram: $(OBJS)
        $(CC) $(LDFLAGS) -o $@ $(OBJS) $(LDLIBS)

.PHONY: all init

init:
        mkdir -p FolderA
        mkdir -p FolderB

-include $(OBJS:%.o=%.d)

主要棘手的部分是确保 FolderAFolderB 存在于构建目录中,然后再尝试 运行 将写入它们的编译器。上面的代码将按顺序构建,但第一次可能会失败 -j2 运行,因为一个线程中的编译器可能会尝试在另一个线程创建目录之前打开输出文件。它也有些不干净。通常使用 GNU 工具,你有一个配置脚本,它会在你尝试 运行 make 之前为你创建这些目录(和 makefile)。 autoconf 和 automake 可以为您构建它。

另一种适用于并行构建的方法是重新定义编译 C 文件的标准规则:

VPATH = ../source
OBJS = FolderA/fileA1.o FolderA/fileA2.o FolderB/fileB1.o
CPPFLAGS = -MMD -MP

myProgram: $(OBJS)
        $(CC) $(LDFLAGS) -o $@ $(OBJS) $(LDLIBS)

%.o: %.c
        mkdir -p $(dir $@)
        $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<

-include $(OBJS:%.o=%.d)

缺点是您还需要为要编译的任何其他类型的源文件重新定义内置规则

这是我添加到文档中的 Makefile(目前正在审核中,所以我将 post 放在这里):

# Set project directory one level above the Makefile directory. $(CURDIR) is a GNU make variable containing the path to the current working directory
PROJDIR := $(realpath $(CURDIR)/..)
SOURCEDIR := $(PROJDIR)/Sources
BUILDDIR := $(PROJDIR)/Build

# Name of the final executable
TARGET = myApp.exe

# Decide whether the commands will be shown or not
VERBOSE = TRUE

# Create the list of directories
DIRS = Folder0 Folder1 Folder2
SOURCEDIRS = $(foreach dir, $(DIRS), $(addprefix $(SOURCEDIR)/, $(dir)))
TARGETDIRS = $(foreach dir, $(DIRS), $(addprefix $(BUILDDIR)/, $(dir)))

# Generate the GCC includes parameters by adding -I before each source folder
INCLUDES = $(foreach dir, $(SOURCEDIRS), $(addprefix -I, $(dir)))

# Add this list to VPATH, the place make will look for the source files
VPATH = $(SOURCEDIRS)

# Create a list of *.c sources in DIRS
SOURCES = $(foreach dir,$(SOURCEDIRS),$(wildcard $(dir)/*.c))

# Define objects for all sources
OBJS := $(subst $(SOURCEDIR),$(BUILDDIR),$(SOURCES:.c=.o))

# Define dependencies files for all objects
DEPS = $(OBJS:.o=.d)

# Name the compiler
CC = gcc

# OS specific part
ifeq ($(OS),Windows_NT)
    RM = del /F /Q 
    RMDIR = -RMDIR /S /Q
    MKDIR = -mkdir
    ERRIGNORE = 2>NUL || true
    SEP=\
else
    RM = rm -rf 
    RMDIR = rm -rf 
    MKDIR = mkdir -p
    ERRIGNORE = 2>/dev/null
    SEP=/
endif

# Remove space after separator
PSEP = $(strip $(SEP))

# Hide or not the calls depending of VERBOSE
ifeq ($(VERBOSE),TRUE)
    HIDE =  
else
    HIDE = @
endif

# Define the function that will generate each rule
define generateRules
$(1)/%.o: %.c
    @echo Building $$@
    $(HIDE)$(CC) -c $$(INCLUDES) -o $$(subst /,$$(PSEP),$$@) $$(subst /,$$(PSEP),$$<) -MMD
endef

# Indicate to make which targets are not files
.PHONY: all clean directories 

all: directories $(TARGET)

$(TARGET): $(OBJS)
    $(HIDE)echo Linking $@
    $(HIDE)$(CC) $(OBJS) -o $(TARGET)

# Include dependencies
-include $(DEPS)

# Generate rules
$(foreach targetdir, $(TARGETDIRS), $(eval $(call generateRules, $(targetdir))))

directories: 
    $(HIDE)$(MKDIR) $(subst /,$(PSEP),$(TARGETDIRS)) $(ERRIGNORE)

# Remove all objects, dependencies and executable files generated during the build
clean:
    $(HIDE)$(RMDIR) $(subst /,$(PSEP),$(TARGETDIRS)) $(ERRIGNORE)
    $(HIDE)$(RM) $(TARGET) $(ERRIGNORE)
    @echo Cleaning done ! 

主要特点

  • 自动检测指定文件夹中的 C 个来源
  • 多个源文件夹
  • 对象和依赖文件的多个相应目标文件夹
  • 为每个目标文件夹自动生成规则
  • 目标文件夹不存在时创建目标文件夹
  • 使用 gcc 进行依赖管理:只构建必要的东西
  • 适用于 UnixDOS 系统
  • GNU Make
  • 而写

如何使用这个 Makefile

要使此 Makefile 适应您的项目,您必须:

  1. 更改 TARGET 变量以匹配您的目标名称
  2. 更改 SOURCEDIRBUILDDIRSourcesBuild 文件夹的名称
  3. 在 Makefile 本身或 make 调用中更改 Makefile 的详细级别 (make all VERBOSE=FALSE)
  4. 更改 DIRS 中文件夹的名称以匹配您的源和构建文件夹
  5. 如果需要,更改编译器和标志

在此 Makefile 中,Folder0Folder1Folder2 等同于您的 FolderAFolderBFolderC

请注意,我目前还没有机会在 Unix 系统上测试它,但它在 Windows 上可以正常工作。


几个棘手部分的解释:

忽略 Windows mkdir 错误

ERRIGNORE = 2>NUL || true

这有两个作用: 第一个 2>NUL 是将错误输出重定向到 NUL,因为它不会出现在控制台中。

第二个,|| true 防止命令提升错误级别。这是 Windows 与 Makefile 无关的东西,它在这里是因为如果我们尝试创建一个已经存在的文件夹,Windows' mkdir 命令会提高错误级别,而我们并不关心,如果它确实存在那很好。常见的解决方案是使用 if not exist 结构,但这与 UNIX 不兼容,所以即使它很棘手,我认为我的解决方案更清晰。


创建包含所有目标文件及其正确路径的 OBJS

OBJS := $(subst $(SOURCEDIR),$(BUILDDIR),$(SOURCES:.c=.o))

这里我们希望 OBJS 包含所有目标文件及其路径,而我们已经有了包含所有源文件及其路径的 SOURCES。 $(SOURCES:.c=.o) 将所有来源的 *.o 中的 *.c 更改,但路径仍然是来源之一。 $(subst $(SOURCEDIR),$(BUILDDIR), ...) 将简单地用构建路径减去整个源路径,因此我们最终得到一个包含 .o 文件及其路径的变量。


处理Windows和Unix风格的路径分隔符

SEP=\
SEP = /
PSEP = $(strip $(SEP))

这只是为了允许 Makefile 在 Unix 和 Windows 上工作,因为 Windows 在路径中使用反斜杠,而其他人都使用斜杠。

SEP=\ 这里双反斜杠用于转义反斜杠字符,make 通常将其视为 "ignore newline character" 以允许在多行上书写。

PSEP = $(strip $(SEP)) 这将删除自动添加的 SEP 变量的 space 字符。


为每个目标文件夹自动生成规则

define generateRules
$(1)/%.o: %.c
    @echo Building $$@
    $(HIDE)$(CC) -c $$(INCLUDES) -o $$(subst /,$$(PSEP),$$@)   $$(subst /,$$(PSEP),$$<) -MMD
endef

这可能是与您的用例最相关的技巧。这是一个可以使用 $(eval $(call generateRules, param)) 生成的规则模板,其中 param 是您可以在模板中找到的 $(1)。 这基本上会为每个目标文件夹用这样的规则填充 Makefile:

path/to/target/%.o: %.c
    @echo Building $@
    $(HIDE)$(CC) -c $(INCLUDES) -o $(subst /,$(PSEP),$@)   $(subst /,$(PSEP),$<) -MMD

这是我一直使用的一个基本的,它几乎是一个框架,但对于简单的项目来说工作得很好。对于更复杂的项目,它当然需要进行调整,但我总是以这个为起点。

APP=app

SRC_DIR=src
INC_DIR=inc
OBJ_DIR=obj
BIN_DIR=bin

CC=gcc
LD=gcc
CFLAGS=-O2 -c -Wall -pedantic -ansi
LFLGAS=
DFLAGS=-g3 -O0 -DDEBUG
INCFLAGS=-I$(INC_DIR)

SOURCES=$(wildcard $(SRC_DIR)/*.c)
HEADERS=$(wildcard $(INC_DIR)/*.h)
OBJECTS=$(SOURCES:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)
DEPENDS=$(OBJ_DIR)/.depends


.PHONY: all
all: $(BIN_DIR)/$(APP)

.PHONY: debug
debug: CFLAGS+=$(DFLAGS)
debug: all


$(BIN_DIR)/$(APP): $(OBJECTS) | $(BIN_DIR)
    $(LD) $(LFLGAS) -o $@ $^

$(OBJ_DIR)/%.o: | $(OBJ_DIR)
    $(CC) $(CFLAGS) $(INCFLAGS) -o $@ $<

$(DEPENDS): $(SOURCES) | $(OBJ_DIR)
    $(CC) $(INCFLAGS) -MM $(SOURCES) | sed -e 's!^!$(OBJ_DIR)/!' >$@

ifneq ($(MAKECMDGOALS),clean)
-include $(DEPENDS)
endif


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

.PHONY: clean
clean:
    rm -rf $(BIN_DIR) $(OBJ_DIR)

我会避免直接操作 Makefile,而是使用 CMake。 只需在 CMakeLists.txt 中描述您的源文件,如下所示:

创建文件 MyProject/source/CMakeLists.txt 包含;

project(myProject)
add_executable(myExec FolderA/fileA1.c FolderA/fileA2.c FolderB/fileB1.c)

下 MyProject/build、运行

cmake ../source/

您现在将获得一个 Makefile。构建,在同一个build/目录下,

make

您可能还想切换到闪电般快速的构建工具 ninja,只需添加一个开关,如下所示。

cmake -GNinja ..
ninja