首页 文章

使用GNU make构建C-program“out of source tree”

提问于
浏览
8

我想用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来执行这样的源外构建?或者有关如何入门的一些好建议?

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

PS:我想避免使用像CMake,Autotools或任何与IDE有关的任何工具 . 只是纯粹的GNU make .

我会很感激 :-)


Updating the dependency files
请看一下这个问题:What is the exact chain of events when GNU make updates the .d files?

4 回答

  • 2

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

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

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

  • 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)
    
  • -2

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

    Main features

    • 自动检测指定文件夹中的 C

    • 多个源文件夹

    • 对象和依赖项文件的多个相应目标文件夹

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

    • 在目标文件夹不存在时创建它们

    • 使用 gcc 进行依赖关系管理:仅构建必要的内容

    • 适用于 UnixDOS 系统

    • GNU Make 撰写


    How to use this Makefile

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

    • 更改 TARGET 变量以匹配您的目标名称

    • 更改 SOURCEDIRBUILDDIRSourcesBuild 文件夹的名称

    • 在Makefile本身或make调用中更改Makefile的详细级别( make all VERBOSE=FALSE

    • 更改 DIRS 中文件夹的名称以匹配源和构建文件夹

    • 如果需要,请更改编译器和标志

    在这个Makefile Folder0 中, Folder1Folder2 相当于你的 FolderAFolderBFolderC .

    请注意,我目前还没有机会在Unix系统上进行测试,但它在Windows上运行正常 .


    Explanation of a few tricky parts :

    忽略Windows mkdir错误

    ERRIGNORE = 2>NUL || true
    

    这有两个影响:第一个, 2>NUL 是将错误输出重定向到NUL,因为它没有出现在控制台中 .

    第二个, || true 防止命令上升错误级别 . 这是与Makefile无关的Windows内容,如果我们尝试创建一个已经存在的文件夹,'s here because Windows' mkdir 命令会提高错误级别,而我们不会这样做 . 常见的解决方案是使用 if not exist 结构,但是这很棘手,我认为我的解决方案更清晰 .


    创建包含所有具有正确路径的目标文件的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 变量的空格字符 .


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

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

    's maybe the trick that is the most related with your usecase. It'是一个可以使用 $(eval $(call generateRules, param)) 生成的规则模板,其中 param 是您可以在模板中找到的 $(1) . 对于每个目标文件夹,这基本上会为Makefile填充这样的规则:

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

相关问题