Makefile - 一次编译多个C文件

时间:2017-01-10 12:08:00

标签: makefile gnu-make

这个问题与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有关,只能一次生成一个目标文件。无论如何,检查$@目标中$(word 2, $^)$(objects)的值表示即使我有多个文件,也只会考虑一个文件。

所以我将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 更改,则point.o目录中的build/mylib/point/不得重新生成。但是在检查了目标文件上的时间戳之后,我可以再次运行make后告诉新的目标文件替换了旧的目标文件。这不好,因为对于大型项目,编译时间仍为O(n)n是要编译的源文件数。

所以这个问题是关于如何在没有make重新生成目标文件的情况下保留第二个makefile 从我从评论中收集的内容来看,我对make的要求太高了。但如果有人知道如何实现这一目标,我会将问题保持开放。

4 个答案:

答案 0 :(得分:2)

生成文件:

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

运行makefile:

$ make -f /cygdrive/c/stackoverflow/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以执行另一种查找源的方法。输出应该相同。

答案 1 :(得分:1)

我终于有时间尝试这个,所以这就是我想出的:

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,但如果您想要保持便携性,请务必使用广泛接受的模式。

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

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

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

答案 2 :(得分:0)

如果你想要的只是一个规则来处理所有目标文件,而不需要同时编译所有目标文件&#34;然后你可能会有这样的事情:

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 $@

[注意:这是从内存中编写的,未经测试。]

答案 3 :(得分:0)

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

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

所以工作的makefile如下。它将目标文件放在单独的目录中,它只编译已更改的文件。

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进行了大量评论,并对其进行了更改。最重要的变化是:

  • 根据GCC的要求,使用$(foreach)单独编译每个文件
  • 使用$?仅适用于比目标
  • 更新的先决条件
  • 使用条件来检测第一个先决条件是否已根据具体情况而改变。如果我们有一个干净的构建(第一次运行make或运行make clean后),则更新的先决条件的数量将与与目标相比的较新先决条件的数量相同。换句话说,$(words $?) == $(words $^)将是真的。因此,我们使用此事实从列出的文件列表中排除列出的第一个先决条件(在我们的例子中为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.