代码之家  ›  专栏  ›  技术社区  ›  nt.bas

Makefile-一次编译多个C文件

  •  0
  • nt.bas  · 技术社区  · 8 年前

    这个问题不同于 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) target显示只有一个文件被考虑,即使我有多个文件。

    因此,我将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 是要编译的源文件数。

    因此,这个问题是关于如何在没有 制作 正在重新生成对象文件。
    从我从评论中得到的信息来看,我要求的太多了 制作 .但如果有人知道如何做到这一点,我就不提这个问题了。

    4 回复  |  直到 8 年前
        1
  •  2
  •   Alexey Semenyuk    8 年前

    生成文件:

    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
  •  1
  •   bobbogo    8 年前

    我终于有时间来尝试这个,所以我得出了以下结论:

    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" ,我不确定它是否取决于使用的确切外壳,但如果您想保持便携性,请确保使用广泛接受的模式。

    • 这个 .SECONDEXPANSION: 需要启用 $$ GNU制造规则。需要在的先决条件中允许基于目标的替换规则 $(OBJECTS) .

    • 先决条件 $$(patsubst $(BUILD_DIR)/%.o,$(SRC_DIR)/%.c,$$@) 是说 现在的 目标依赖于具有相同文件夹结构和名称的特定源文件。

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

        3
  •  0
  •   Some programmer dude    8 年前

    如果您只需要一条规则来处理所有对象文件,而不必“一次编译所有对象文件”,那么您可以得到如下结果:

    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
  •  0
  •   nt.bas    8 年前

    在再次阅读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 $^) 会是真的。因此,我们使用这一事实来排除列出的第一个先决条件( build 在我们的情况下)从要传递给GCC的文件列表中选择。

    此外,从对象文件构建可执行文件时,请确保使用 $^ 而不是 $? 选择“先决条件”时,否则可执行文件中将只包含较新的文件,并且不会运行。

    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.