Makefile - 一次编译多个 C 文件

Makefile - compile multiple C file at once

这个问题与 makefiles - compile all c files at once 的问题不同,因为我有一个额外的要求:我想将所有目标文件重定向到一个单独的目录中。

设置如下:

我在一个目录中有多个来源 src/mylib
我希望对象文件以 build/mylib.
结尾 另请注意,在 mylib 下有子目录。

第一次尝试如下:

sources = $(shell find src/ -name ".c")
objects_dirs = $(subst src/, build/, $(dir $(sources)) # This variable is used by the build rule to create directories for objects files prior to compilation
objects = $(subst src/, build/, $(patsubst %.c, %.o, $(sources))) # This variable has the paths to the objects files that will be generated in the build directory

# This is where things aren't working as expected
$(objects): build $(sources)
    $(cc) $(cflags) -o $@ $(word 2, $^))

build:
    $(foreach dir, $(objects_dirs), $(shell mkdir -p $(dir)))

对于上面的 makefile,只生成了一个目标文件。我猜想这可能与 GCC 一次只能生成一个目标文件有关。不管怎样,检查 $(objects) 目标中 $@$(word 2, $^) 的值表明即使我有多个文件,也只考虑了一个文件。

所以我将 makefile 更改为以下内容:

sources = $(shell find src/ -name ".c")
objects = $(subst src/, build/, $(patsubst %.c, %.o, $(sources))) # This variable has the paths to the objects files that will be generated in the build directory

# This works as expected but it appears to me like make is generating all the objects files even though source files did not change. This can be seen by checking the timestamps on new object files after running make again.
$(objects): build $(sources)
    $(foreach source, $(sources), $(shell $(cc) $(cflags) -o $(subst src/,build/, $(patsubst %.o,%.c,$(source))) $(source)))

build:
    $(foreach dir, $(objects_dirs), $(shell mkdir -p $(dir)))

第二个 makefile 按预期工作,但目标文件再次重建,这违背了使用 make 的另一个目的:仅重新编译那些自上次编译以来更改的源文件。

因此我的问题是:如何在一个单独的目录中生成所有目标文件一次(我的意思是在一个规则中执行所有源文件的编译)同时制作确保如果源文件没有更改,则不应重新生成关联的目标文件。

我不是在加速编译之后。我寻求的是一条规则,它将生成所有目标文件,以便只重新编译更新的源文件。

最后一个 makefile 可以完成工作,但是需要重新编译所有源文件,这违背了使用 make 的另一个目的:只应重新编译更改的源文件。

编辑

阅读评论后,我似乎没有正确表达我的问题。由于我已经提供了详细信息,因此我将问题原样保留在下面,并提供更多详细信息。

上面源代码中的第二个 makefile 可以工作 。但它只做了一半的工作。 build 目录实际上是 src 目录的镜像。
因此,如果我说一个文件为 src/mylib/point/point.c,我会生成 build/mylib/point/point.o。这是第一部分。
第二部分是如果point.c没有改变,build/mylib/point/目录下的point.o一定不能重新生成。但是在检查目标文件的时间戳后,我可以看出一个新的目标文件再次在 运行 make 之后替换了旧的。这不好,因为对于大型项目,编译时间保持 O(n),其中 n 是要编译的源文件数。

所以这个问题是关于如何在不make重新生成目标文件的情况下保留第二个生成文件。
从我从评论中收集到的信息来看,我对 make 的要求太多了。但是,如果有人知道如何实现这一点,我将保留这个问题。

如果您只需要一个规则来处理所有目标文件,而不必 "compile all at once" 那么您可以这样:

BUILD_DIR = build
SOURCES = ...
TARGET  = ...
OBJECTS = $(SOURCES:%.c=$(BUILD_DIR)/%.o)

default: target

target: $(TARGET)

$(TARGET): $(OBJECTS)
    $(LD) -o $@ $(LDFLAGS) $^ $(LIBS)

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

$(BUILD_DIR):
    -mkdir $@

[注:凭记忆写,未经测试。]

再次阅读GNU make手册后,这里有解决第二个问题的方案。

第一次尝试是正确的路径。第二次尝试在先决条件中有 $(sources) 但没有在命令中使用它,这很愚蠢。

接下来是工作生成文件。它将目标文件放在单独的目录中 and 它只编译已更改的文件。

sources = $(shell find src/ -name ".c")
$objects_dirs = $(subst src/, build/, $(dir $(sources)) # This variable is used by the build rule to create directories for objects files prior to compilation
objects = $(subst src/, build/, $(patsubst %.c, %.o, $(sources))) # This variable has the paths to the objects files that will be generated in the build directory

# This should now work as expected: object files go into their designated directories under "build/" and only updated files will be recompiled.
$(objects): build $(sources)
# After running say "make clean", make will figure out the need to run the first prerequisite.
# If we are doing a clean build, the number of prerequisites will equal the number of new prerequisites.
ifeq ($(words $?), $(words $^))
    # Note the use of "$?" instead of "$^". $? is used since it holds prerequisites that are newer than the target while $^ will holds all prerequisites whether they are new or not.
    $(foreach source, $(wordlist 2, $(words $?), $?), $(shell $(cc) $(cflags) -o $(subst src/,build, $(patsubst %.c,%.o, $(source))) $(source)))
else
    # If we have a few new targets, no need to exclude "build" from prerequisites because the first prerequisite will be a file that changed.
    $(foreach source, $?, $(shell $(cc) $(cflags) -o $(subst src/,build, $(patsubst %.c,%.o, $(source))) $(source)))
endif

.PHONY: build
build:
    $(foreach dir, $(objects_dirs), $(shell mkdir -p $(dir)))

.PHONY: clean
clean:
    @rm -rf build/

makefile 被大量评论,其中包含使其工作的更改。最重要的变化是:

  • 使用 $(foreach) 根据 GCC 的要求单独编译每个文件
  • 使用 $? 仅适用于比目标更新的先决条件
  • 使用条件来检测第一个先决条件是否根据情况发生了变化。如果我们有一个干净的构建(运行ning make 第一次或 运行ning make clean 之后),更新的先决条件的数量将与与目标相比更新的先决条件。换句话说 $(words $?) == $(words $^) 将为真。所以我们使用这个事实从要传递给 GCC 的文件列表中排除列出的第一个先决条件(build 在我们的例子中)。

此外,当从目标文件构建可执行文件时,请确保在选择先决条件时使用 $^ 而不是 $? 否则您最终将在可执行文件中只有较新的文件,并且它将不是 运行.

target = bin/mylib.a

.PHONY: all
all: $(target)

$(target): $(objects)
    ar -cvq $@ $^ # Notice that we're not using $? else only updated object files will end up in the archive.

我终于有时间对此进行试验,所以这是我想出的:

BUILD_DIR = build
SRC_DIR = src
SOURCES = $(shell find $(SRC_DIR)/ -name "*.c")
TARGET  = program
OBJECTS = $(SOURCES:$(SRC_DIR)/%.c=$(BUILD_DIR)/%.o)

default: $(TARGET)

.SECONDEXPANSION:

$(OBJECTS) : $$(patsubst $(BUILD_DIR)/%.o,$(SRC_DIR)/%.c,$$@)
        mkdir -p $(@D)
        $(CC) -c -o $@ $(CFLAGS) $<

$(TARGET): $(OBJECTS)
        $(CC) -o $@ $(CFLAGS) $^

.PHONY: default

兴趣点:

  • 我不得不将来源 find 模式从 ".c" 更改为 "*.c",我不确定它是否取决于确切的 shell 使用过,但如果您想保持便携性,请务必使用广泛接受的模式。

  • 需要 .SECONDEXPANSION: 才能为 GNU Make 启用 $$ 规则。需要在 $(OBJECTS).

  • 的先决条件中允许基于目标的替换规则
  • 先决条件 $$(patsubst $(BUILD_DIR)/%.o,$(SRC_DIR)/%.c,$$@) 是说,current 目标依赖于具有相同文件夹结构和名称的特定源文件。

  • 命令 mkdir -p $(@D) 确保当前目标的路径在丢失时创建。

生成文件:

all:
clean:

src_root := src
src_subdirs := foo foo/bar foo/bar/buz
build_root := build

o_suffix := .o

# Build list of sources. Iterate every subfolder from $(src_subdirs) list 
# and fetch all existing files with suffixes matching the list.
source_suffixes := .c .cpp .cxx
sources := $(foreach d,$(addprefix $(src_root)/,$(src_subdirs)),$(wildcard $(addprefix $d/*,$(source_suffixes))))

# If src_subdirs make variable is unset, use 'find' command to build list of sources.
# Note that we use the same list of suffixes but tweak them for use with 'find'
ifeq ($(src_subdirs),)
  sources := $(shell find $(src_root) -type f $(foreach s,$(source_suffixes),$(if $(findstring $s,$(firstword $(source_suffixes))),,-o) -name '*$s'))
endif

$(info sources=$(sources))

# Build source -> object file mapping.
# We want map $(src_root) -> $(build_root) and copy directory structure 
# of source tree but populated with object files.
objects := $(addsuffix $(o_suffix),$(basename $(patsubst $(src_root)%,$(build_root)%,$(sources))))
$(info objects=$(objects))

# Generate rules for every .o file to depend exactly on corresponding source file.
$(foreach s,$(sources),$(foreach o,$(filter %$(basename $(notdir $s)).o,$(objects)),$(info New rule: $o: $s)$(eval $o: $s)))

# This is how we compile sources:
# First check if directory for the target file exists. 
# If it doesn't run 'mkdir' command.
$(objects): ; $(if $(wildcard $(@D)),,mkdir -p $(@D) &&) g++ -c $< -o $@

# Compile all sources.
all: $(objects)
clean: ; rm -rf $(build_root)

.PHONY: clean all

环境:

$ find
.
./src
./src/foo
./src/foo/bar
./src/foo/bar/bar.cxx
./src/foo/bar/buz
./src/foo/bar/buz/buz.c
./src/foo/bar/foo.c
./src/foo/foo.cpp

运行 生成文件:

$ make -f /cygdrive/c/Whosebug/Makefile.sample -j
sources=src/foo/bar/bar.cxx src/foo/bar/buz/buz.c src/foo/bar/foo.c src/foo/foo.cpp
objects=build/foo/bar/bar.o build/foo/bar/buz/buz.o build/foo/bar/foo.o build/foo/foo.o
New rule: build/foo/bar/bar.o: src/foo/bar/bar.cxx
New rule: build/foo/bar/buz/buz.o: src/foo/bar/buz/buz.c
New rule: build/foo/bar/foo.o: src/foo/bar/foo.c
New rule: build/foo/foo.o: src/foo/bar/foo.c
New rule: build/foo/bar/foo.o: src/foo/foo.cpp
New rule: build/foo/foo.o: src/foo/foo.cpp
mkdir -p build/foo/bar && g++ -c src/foo/bar/bar.cxx -o build/foo/bar/bar.o
mkdir -p build/foo/bar/buz && g++ -c src/foo/bar/buz/buz.c -o build/foo/bar/buz/buz.o
mkdir -p build/foo/bar && g++ -c src/foo/bar/foo.c -o build/foo/bar/foo.o
mkdir -p build/foo && g++ -c src/foo/bar/foo.c -o build/foo/foo.o

再次环境:

$ find
.
./build
./build/foo
./build/foo/bar
./build/foo/bar/bar.o
./build/foo/bar/buz
./build/foo/bar/buz/buz.o
./build/foo/bar/foo.o
./build/foo/foo.o
./src
./src/foo
./src/foo/bar
./src/foo/bar/bar.cxx
./src/foo/bar/buz
./src/foo/bar/buz/buz.c
./src/foo/bar/foo.c
./src/foo/foo.cpp

使用 'src_subdirs=' 尝试 运行 这个 Makefile 来练习另一种定位源的方法。输出应该是一样的。