使用GNU make在“源码树外”构建C程序

24
我希望使用GNU make工具为我的微控制器构建一个C项目。我希望以一种清晰的方式来完成它,这样在构建后我的源代码就不会被对象文件和其他东西所混淆。因此,请想象我有一个名为“myProject”的项目文件夹,在其中有两个文件夹:
- myProject
     |
     |---+ source
     |
     '---+ build

构建文件夹仅包含一个Makefile。下图显示了当我运行GNU make工具时应该发生的情况:

enter image description here

因此,GNU make应该为在源文件夹中找到的每个.c源文件创建一个对象文件。对象文件应该以类似于源文件夹中的结构的目录树形式组织。
GNU make还应该为每个.c源文件创建一个.d依赖项文件(实际上,依赖项文件本身就是某种类型的makefile),依赖项文件在GNU make手册第4.14章“自动生成先决条件”中有描述:
对于每个源文件name.c,都有一个makefile name.d,其中列出了对象文件name.o依赖的文件。
从以下Stackoverflow问题关于GNU make依赖文件*.d中,我了解到将选项-MMD和-MP添加到GNU gcc编译器的CFLAGS中可以帮助自动化这一过程。
现在问题来了。有没有人有执行这样的离线构建的示例makefile?或者有一些好的建议如何入门?
我相信大多数编写此类makefile的人都是Linux用户。但是微控制器项目也应该在Windows机器上构建。无论如何,即使您的makefile仅适用于Linux,它也提供了一个很好的起点;-)
PS:我想避免使用CMake、Autotools或任何与IDE有关的额外工具。只使用纯GNU make。
我将非常感激 :-)

更新依赖文件
请查看这个问题:GNU make 在更新 .d 文件时的具体事件链是什么?


1
你提出了一个相当简单的makefile请求,所以如果有人在这里给你提供它,我不会感到惊讶。但是一般情况下,我们提供的是免费的帮助,而不是免费的代码。 - Beta
是的,你说得对。 - K.Mulier
这种简单的Makefile实际上非常适合新文档功能,但对于问答来说有点太宽泛了。 - Tim
通常当我提出问题时,我已经有了我的代码,并尝试精确指定问题所在。但这一次,我没有一个好的起点...可能是因为我对makefile还很陌生。我能保留这个问题吗?如果有人能给我一些简单的示例makefile并将我引导到正确的方向,我将不胜感激。 我承诺稍后将其添加到Stackoverflow文档部分 :-)(当然要归功于实际编写makefile的作者) - K.Mulier
1
您想让您的makefile实现以下功能:1)构建事物;2)远程构建;3)使用源代码树中找到的所有内容;4)具有高级依赖项处理能力;5)在Windows上运行。我建议您逐一解决这些问题,从在Windows上创建一个“HelloWorld” makefile开始(我想帮忙,但我不擅长Windows)。 - Beta
也许我们可以取消第5步,在Linux上继续操作;-) 这对我来说是一个很好的起点。也许我可以自己将配方中的Linux shell命令转换为Windows shell命令 :-) - K.Mulier
4个回答

23

这是我添加到文档中的 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 ! 

主要特点

  • 自动检测指定文件夹中的源文件
  • 多个源文件夹
  • 多个相应的目标文件和依赖项文件的目标文件夹
  • 为每个目标文件夹自动生成规则
  • 当目标文件夹不存在时创建它们
  • 使用gcc进行依赖管理:仅构建必要的内容
  • 适用于UnixDOS系统
  • 编写于GNU Make之上

如何使用此Makefile

要将此Makefile适应到您的项目中,您需要执行以下操作:

  1. TARGET变量更改为与您的目标名称匹配
  2. SOURCEDIRBUILDDIR中更改SourcesBuild文件夹的名称
  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 防止命令提高错误级别。这是与 Makefile 无关的 Windows 东西,它在这里是因为 Windows 的 mkdir 命令如果我们试图创建一个已经存在的文件夹会提高错误级别,而实际上我们并不关心,如果它已经存在那也没关系。常见的解决方案是使用 if not exist 结构,但这不符合 UNIX 兼容性,所以尽管有些棘手,我认为我的解决方案更清晰。


创建 OBJS 变量,其中包含所有对象文件及其正确的路径

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

在这里,我们希望OBJS包含所有对象文件及其路径,并且我们已经有了SOURCES,其中包含所有源文件及其路径。 $(SOURCES:.c=.o)将*.c更改为所有源的*.o,但路径仍然是源路径。 $(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

非常感谢Tim!你的makefile非常优雅并且文档写得很好。你的回复对我来说是非常大的帮助。我今晚会测试它。谢谢,谢谢,谢谢 :-) - K.Mulier
我很高兴你喜欢它,实际上我在做这件事时感到非常愉快。这有点具有挑战性,与Makefile的第三条规则相关:如果目标在当前工作目录中构建,则生活最简单。但是谁想要简单的生活呢?;) - Tim
看起来很不错!我在答案末尾添加了解释,以便更容易理解这个Makefile。至于参考资料,您可以简单地链接到此stackoverflow Q/A,对我来说没问题。顺便说一句,我使用的大多数技巧都是在其他SO Q/A或其他网站上发现的。 - Tim
@TimF 再次感谢,那个很好用(顺便说一下,我想你的意思是“如果你只使用 Linux”)。你能解释一下 $$@ 是什么意思吗?(我懂 $@)。 - DavidA
@DavidA 没错 ;) 双 $ 符号与单个 $ 符号具有相同的含义,但是将其加倍是为了转义它。如果我们不进行转义,make 将在定义部分中对其进行评估,由于此处 @ 不是变量,因此它将为空。我们希望它在生成的目标中进行评估,而不是在定义函数中进行评估,因此必须对其进行转义。 - Tim
显示剩余2条评论

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存在于构建目录中。以上代码将按顺序进行构建,但在第一次运行时可能会因为一个线程中的编译器在另一个线程创建目录之前尝试打开输出文件而失败,这也有点不够干净。通常情况下,使用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可能不存在。我理解得对吗? - K.Mulier
VPATH 如何处理具有相同名称的多个源文件? - Maxim Egorushkin
1
@MaximEgorushkin:在同一目录中,不能有相同名称的多个文件。子目录(在源代码下)是名称的一部分,因此不同子目录中具有相同基本文件名是可以的。对象和依赖文件将具有相同的名称(包括子目录名称),只是扩展名不同。因此,您最终会在“build”目录下获得与“source”目录相同的目录结构。 - Chris Dodd
1
如果-j2同时尝试构建第一个源文件并运行init动作来创建目录,那么可能会失败——如果发生了一些事情使得mkdircc运行得慢得多,编译器可能会在目录不存在时尝试打开输出文件。 - Chris Dodd
嗨 @ChrisDodd,我刚在Stackoverflow上发布了另一个关于makefile的问题。这个问题是关于高级变量继承的。http://stackoverflow.com/questions/39036774/advanced-variable-inheritance-in-gnu-make - K.Mulier

2

这是我经常使用的基本模板,它本身就像一个骨架,对于简单的项目来说已经足够了。对于更复杂的项目,需要进行适当的调整,但我总是把这个作为起点。

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)

1
我建议避免直接操作 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

1
欢迎来到 Stack Overflow!这实际上是一条评论,而不是答案。当您的声望更高时,您将能够发布评论 - Enamul Hassan
1
@manetsus 感谢您的评论。我刚刚修改了我的答案。 - exavolt

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