【问题标题】:Building C-program "out of source tree" with GNU make使用 GNU make “从源代码树”构建 C 程序
【发布时间】:2016-12-25 05:20:18
【问题描述】:

我想使用 GNU make 工具为我的微控制器构建一个 C 项目。我想以一种干净的方式来做,这样我的源代码在构建后不会被目标文件和其他东西弄乱。所以想象一下,我有一个名为“myProject”的项目文件夹,里面有两个文件夹:

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

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

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

GNU make 还应该为每个 .c 源文件创建一个 .d 依赖文件(事实上,依赖文件本身就是某种 makefile)。 GNU make 手册第 4.14 章“自动生成先决条件”中描述了依赖文件:

对于每个源文件 name.c 都有一个 makefile name.d 列出 目标文件 name.o 依赖于哪些文件。

从 Stackoverflow 的以下问题 About the GNU make dependency files *.d 中,我了解到将选项 -MMD-MP 添加到 GNU gcc 编译器的 CFLAGS 可以帮助实现自动化。

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

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

PS:我想避免使用额外的工具,如 CMake、Autotools 或任何与 IDE 相关的工具。只是纯粹的 GNU make。

我将不胜感激:-)


更新依赖文件
请看一下这个问题:What is the exact chain of events when GNU make updates the .d files?

【问题讨论】:

  • 您需要一个非常简单的 makefile,所以如果有人给您,我不会感到惊讶。但通常我们提供免费帮助,而不是免费代码。
  • 是的,你是对的。
  • 这种简单的 Makefile 实际上很适合新的文档功能,但对于 Q/A 来说它有点过于宽泛了。
  • 通常当我提出问题时,我已经有了我的代码,并尝试准确地指出问题出在哪里。但是这一次,我没有一个好的起点......可能是因为我对makefiles太陌生了。我可以保留这个问题吗?如果有人可以通过一些简单的示例 makefile 将我推向正确的方向,我将不胜感激。我保证稍后会将它添加到 Stackoverflow 的文档部分:-)(当然要归功于 makefile 的实际编写者)
  • 您希望您的 makefile 1) 构建东西 2) 远程 3) 使用它在源代码树中找到的所有内容 4) 具有高级依赖关系处理 5) 在 Windows 上。我建议您一次解决这些问题,从 Windows 上的 HelloWorld makefile 开始。 (我想帮忙,但我不做 Windows)。

标签: makefile gnu-make


【解决方案1】:

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

# 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&gt;NUL 是将错误输出重定向到 NUL,因此它不会出现在控制台中。

第二个,|| true 防止命令提升错误级别。这是与 Makefile 无关的 Windows 东西,它在这里是因为如果我们尝试创建一个已经存在的文件夹,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 通常将其视为“忽略换行符”以允许在多行上写入。

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


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

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

【讨论】:

  • 非常感谢蒂姆!您的 makefile 非常优雅且有据可查。你的回复对我帮助很大。我今晚会测试它。谢谢,谢谢,谢谢:-)
  • 很高兴你喜欢它,实际上我很高兴这样做。这有点挑战性,它与 Makefile 的第三条规则有关:Life is simplest if the targets are built in the current working directory. 但是谁想要简单的生活? ;)
  • 看起来很棒!我在答案的末尾添加了解释,以简化对这个 Makefile 的理解。作为参考,您可以简单地链接到这个 stackoverflow Q/A,这对我来说很好。顺便说一句,我使用的大部分技巧都是在其他 SO Q/A 或其他网站上找到的。
  • @TimF:这个例子真的很有帮助。如果不需要 Windows 支持,$(HIDE)$(CC) -c $$(INCLUDES) -o $$(subst /,$$(PSEP),$$@) $$(subst /,$$(PSEP),$$&lt;) -MMD 会简化成什么? (即如果我们删除 PSEP?)
  • @DavidA 我很高兴它有帮助!如果您只使用 WINdows,则不需要 Makefile 变量,因此您可以简单地删除 subst 函数并仅保留内容:$(HIDE)$(CC) -c $$(INCLUDES) -o $$@ $$&lt; -MMD 。 subst 在这里用斜杠或反斜杠替换斜杠,具体取决于您的操作系统。
【解决方案2】:

这个相当小的 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)

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

【讨论】:

  • 哇,非常感谢!你说它在第一次运行时不适用于-j2。这个选项是为了并行化构建,对吧?我认为它不会在并行构建中工作,因为创建对象文件时文件夹 FolderA 和 FolderB 可能不存在。我说的对吗?
  • VPATH如何处理多个同名源文件?
  • @MaximEgorushkin:你不能在同一个目录中有多个同名文件。子目录(在源代码下)是名称的一部分,因此不同子目录中的相同基本文件名是可以的。对象和依赖文件将具有相同的名称(包括子目录名称),但扩展名已更改。所以你最终会在build 下得到与source 下相同的目录结构
  • 如果-j2 在尝试运行init 操作以创建目录的同时尝试构建第一个源文件,它将(可能)失败——如果发生这种情况使mkdir 运行比cc 慢得多,编译器可能会在目录存在之前尝试打开输出文件。
  • 嗨@ChrisDodd,我刚刚在 Stackoverflow 上发布了另一个 makefile 问题。问题是关于高级变量继承。 stackoverflow.com/questions/39036774/…
【解决方案3】:

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

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)

【讨论】:

    【解决方案4】:

    我会避免直接操作 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
    

    【讨论】:

    • 欢迎来到 Stack Overflow!这实际上是评论,而不是答案。多一点代表,you will be able to post comments.
    • @manetsus 感谢您的评论。我刚刚修改了答案。
    猜你喜欢
    • 2021-12-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多