【问题标题】:Makefile - compile multiple C file at onceMakefile - 一次编译多个 C 文件
【发布时间】:2017-01-10 12:08:00
【问题描述】:

这个问题与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 的另一个目的:只应重新编译更改的源文件。

编辑

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

上面源代码中的第二个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 重新生成目标文件的情况下保留第二个 makefile。
从我从 cmets 收集到的信息来看,我对 make 的要求太多了。但是,如果有人知道如何做到这一点,我会留下这个问题。

【问题讨论】:

  • 为什么不使用-j 选项运行并行make?
  • @Someprogrammerdude 我不熟悉-j 标志,但我上次使用它的印象是它加快了编译速度,这不是我的问题。
  • 为什么要一次性创建它们?通常 make 为每个对象调用编译器并一个接一个地创建它。如果不加快速度,您的请求的目的是什么?
  • 在不查看您文件的详细信息的情况下,我几乎可以确定 -j 就是您想要的。 makefile 应该简单地正确列出所有依赖项(不要太多,也不要太少:完全是现有的)。然后-j <n> 选项将开始并行构建尽可能多的可用目标,最大为 。 (编辑:我看到您可能希望在一次 gcc 运行中构建它们。这当然是 -j 未解决的不同要求。)
  • 核心问题:你想在源码树的不同深度构建源码文件;好的。这很难做到,违背了make 的原则,make 不会让这变得容易。您可能想阅读Recursive make considered harmful(和其他地方),尽管我认为米勒有些夸大了这种方法的问题,而递归 make 是 I 将如何处理这个问题。 简答:对此没有简答。 (即,您可能需要重新考虑您的要求)

标签: makefile gnu-make


【解决方案1】:

生成文件:

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,以使用另一种方法来定位源。输出应该是一样的。

【讨论】:

    【解决方案2】:

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

    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,$$@) 是说,当前 目标依赖于具有相同文件夹结构和名称的特定源文件。

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

    【讨论】:

    • 我确实花了一些时间来理解这里发生了什么。我认为这也可能有效。将对其进行测试并回复您。此外,在$(OBJECTS) 目标配方中,您希望使用$? 以便仅使用更新的先决条件($&lt; 将仅选择已更改文件列表中的一个文件)。另外请考虑从 C 标志中删除 -o,因为 GCC 至少不允许同时使用 -c-o。除了最初的反馈,我会在测试后回复您。
    • @nt.bas 您绝对应该对此进行测试,因为我认为您还没有得到它的所有部分。 $(OBJECTS) 规则以某种方式起作用,其中每个对象只有一个先决条件,并且这个先决条件应该保留(它是用于创建对象的确切 *.c 文件,因此只有在以下情况下才会重新创建对象c 文件较新)。在命令中使用$^$?$&lt; 不会改变任何东西,因为只有一个先决条件,并且在执行命令时它更新 - 使用你喜欢的任何东西,但我如果您想添加其他先决条件,建议不要使用$?
    • @nt.bas 关于gcc -c -o &lt;file&gt;:这对gcc 完全有效。使用-c 生成对象文件而不是链接,使用-o &lt;file&gt; 指定对象的路径。您还如何在 gcc 中指定输出名称?
    • 我要完蛋了!有用。刚测试完。一路上学到了3个新东西。我将此答案标记为正确。谢谢!
    • @nt.bas 关于$(OBJECTS),您应该将其视为规则配方,为每个单独的目标创建具体规则。因此,当创建单个目标规则时,会针对该特定目标扩展自动变量 - 而不是针对整个目标列表。我真的不知道如何解释,你必须自己学习,possibly read some documentation 并接受我花了几个月的时间才走到这一步;)
    【解决方案3】:

    如果您只需要一条规则来处理所有目标文件,而不必“一次编译所有”,那么您可以有这样的东西:

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

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

    【讨论】:

    • 我实际上是在努力让它发挥作用。会尽快回复您。
    • 我试图让你的例子工作,但我失败了。感谢您的贡献。
    【解决方案4】:

    再次阅读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 有大量注释,其中包含使其工作的更改。最重要的变化是:

    • 使用$(foreach)按照GCC的要求单独编译每个文件
    • 使用$? 仅适用于比目标更新的先决条件
    • 使用条件来检测第一个先决条件是否已根据情况发生变化。如果我们有一个干净的构建(第一次运行 make 或在运行 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.
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-04-16
      • 2011-08-22
      • 2015-10-08
      • 2015-01-25
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多