寻找完美的Makefile画布

5
我是一名计算机工程专业的学生,现在我需要实现另一个C程序。目前为止,我总是从以前的项目中创建新的Makefile,这需要花费很多时间去设置。有没有一个强大的Makefile模板,只需要提供包含main函数的C文件的名称就可以了?
我的想法是,对于每个新项目,我会创建一个包含Makefile、bin文件夹和src文件夹的文件夹。然后,我会编辑一些变量到Makefile中(包括包含main() 函数的C文件),然后make 就会自动构建所有内容,并考虑依赖关系。
你知道是否存在这样的Makefile吗?
[通过Alexandre C.编辑]:Automake / Autoconf对于仅使用标准库并运行在标准unix操作系统上的这种项目来说过于繁琐。对于我们需要实现的项目,依赖关系(要链接的文件)可以从“.h”包含中推导出,并且通常涉及的文件非常少。

1
我过去曾经设计了各种Makefile模板,有些包含更多的内容(如自动依赖项生成等)。最好的方法仍然是使用Gnu Make的优秀文档来编写自己的Makefile。你甚至可以使用像src = $(shell find . -name "*.c")或者$(grep -H "main(")这样的东西。这并不难,而且你会感觉自己掌控了整个环境。 - Alexandre C.
@AlexandreC. 你能提供一个脚本或Makefile,让我可以用来编写自己的吗? - Mat
1
@Mat:网上有很多(例如这里)。我强烈建议你仔细阅读Gnu Make的文档。了解Make不需要很长时间,只需要一个下午就可以成为Make专家,而且这些知识在你的职业生涯中非常有用。特别是要学会使用内置规则(这样你就知道何时放弃它们)。 - Alexandre C.
1
相关链接:https://dev59.com/7XRC5IYBdhLWcg3wYf8p请仔细阅读以上链接,了解如何为C/C++项目生成Makefile的依赖关系。 - Alexandre C.
7个回答

3

最接近于一个神奇和完美的Makefile是使用可移植C程序和库的事实标准:Automake

作为一个例子,从这个页面中提取,这是你需要添加到Makefile.am中的示例:

bin_PROGRAMS = zardoz
zardoz_SOURCES = main.c head.c float.c vortex9.c gun.c
zardoz_LDADD = $(LIBOBJS)

我们在这里添加的一个 make 目标的等价物被称为 zardoz。源文件在 zardoz_SOURCES 下指定,任何需要链接的额外库都在 zardoz_LDADD 下指定。您不需要指定 main 函数所在的位置,因为链接器会在链接阶段自动找到它。如果不存在,则链接将失败。


4
@AlexBrown:没有人能够为automake制作一个可行的示例 :-((注:automake是一个自动化构建工具,用于生成Makefile。) - Kerrek SB
1
Automake对于在典型环境下运行的学生项目来说可能过于复杂了。Autotools用于保证可移植性(但这需要很大的设置成本和学习曲线)。 - Alexandre C.
Automake对我所需的功能来说太过庞大。通常我所处理的项目都相当小,只需要几个C文件生成最多2个可执行文件,并使用非常标准的库(如sockets、pthreads等)。我相信可以编写一个相当简单的Makefile而不需要使用automake或autoconf。 - Mat
1
@Mat:你对于使用你目前有效使用的“模板”有什么问题? - Mike Kwan
@MikeKwan:我感觉可以使用一个简单的通用 Makefile,不需要手动编写 .o 文件之间的依赖关系(例如,如果我有两个可执行文件没有使用相同的 .o 文件)。我的意思是,任何阅读我的代码的人(尤其是 #包含“abc.h”和“int main()”)都知道如何编译它。为什么计算机不能完成这项工作呢? - Mat
@Mat: 请参考https://dev59.com/7XRC5IYBdhLWcg3wYf8p,了解如何生成依赖项。有很多方法可以实现这一点(我曾经使用过`gcc -MM`,但现在我依赖于autotools),而这个问题将教你一个解决方案。 - Alexandre C.

1
你可以轻松地编写自己的宏“包”,以执行此操作。例如,将此文件创建为您的样板,命名为program.mk,并将其放置在树形结构中的中央位置:
lang.c.objs   = $(patsubst %.c,%.o,$(1))
lang.c.link   = $(CC) $(CFLAGS) $(LDFLAGS) -o $(1) $(2)
lang.c++.objs = $(patsubst %.cpp,%.o,$(1))
lang.c++.link = $(CXX) $(CXXFLAGS) $(LDFLAGS) -o $(1) $(2)

define make-program
  program.$(1).lang    ?= c
  program.$(1).objects ?= $$(call lang.$$(program.$(1).lang).objs,$$(program.$(1).sources))
  $$(program.$(1).name): $$(program.$(1).objects) $$(program.$(1).extra-deps)
      $$(call lang.$$(program.$(1).lang).link,$$@,$$^ $$(program.$(1).ldlibs))
  CLEANABLE    += $$(program.$(1).name)
  ALL_PROGRAMS += $$(program.$(1).name)
endef

# If the user didn't specify a list of programs, build them all
ifndef PROGRAMS
  PROGRAMS = $(foreach p,$(filter program.%.name,$(.VARIABLES)),\
    $(patsubst program.%.name,%,$(p)))
endif

# Generate the rule to build each program
$(foreach p,$(PROGRAMS),$(eval $(call make-program,$(p))))

.PHONY: all clean
all: $(ALL_PROGRAMS)

clean: ; rm -f $(CLEANABLE)

.DEFAULT_GOAL := all

现在,在您想要构建程序的每个目录中,您的 makefile 可以是这样的:

program.p.name    = Program
program.p.sources = Program1.c Program2.c

include path/to/program.mk

可以使用类似的library.mk来处理库文件。这种方法非常强大且易于扩展。

1


    PROGRAM = 测试程序
    CFLAGS = -c -Wall
OBJECTS += $(patsubst %.c, %.o, $(wildcard ./src/*.c)) OBJECTS += $(patsubst %.c, %.o, $(wildcard ./src/more-sources*.c))
all: $(OBJECTS) gcc -o ./bin/$(PROGRAM) $(OBJECTS)
%.o: %.c %.h gcc $(CFLAGS) $< -o $@

这样做就可以了。显然我没有测试过。 您可以选择在每个文件上覆盖默认编译。


我更喜欢使用src = $(shell find . -name "*.c"),因为它比$(wildcard)更灵活,但这里有很多种方法。 - Alexandre C.
有趣。这个能在运行MinGW或类似的Windows上工作吗? - Honeymustard
你需要一个足够的Bourne shell和find程序。这应该可以在Cygwin上工作,或者任何提供bash和findutils的环境中都可以。 - Alexandre C.

0
我想出了以下解决方案:
# Author Matthieu Sieben (http://matthieusieben.com)
# Version 23/10/2012
#
# This Makefile is licensed under the Creative Commons Attribution
# Partage dans les Mêmes Conditions 2.0 Belgique License.
# To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/2.0/be/.
# Use at your own risk.
#
# Use Makefile.inc to write you own rules or to overwrite the default values defined here.

SRCDIR  = ./src
BINDIR  = ./bin

SHELL   = /bin/bash
CC      = /usr/bin/gcc
LIBS    =
CFLAGS  = -Wall -Wextra -Werror
LDFLAGS =

.PHONY: default all clean distclean
.SUFFIXES: .c .o

# make "all" the default target
default: all

-include Makefile.inc
-include Makefile.d

CFLAGS += -I$(SRCDIR)
ifeq ($(DEBUG), 1)
    CFLAGS += -DDEBUG=1 -ggdb
else
    CFLAGS += -DDEBUG=0 -O2
    LDFLAGS += -O2
endif

%.o: %.c
    @echo "  Compiling `basename $<`";
    @$(CC) $(CFLAGS) -o $@ -c $<;

clean:
    @echo "Cleaning compiled files";
    @find $(SRCDIR) -name "*.o" -exec rm {} \;
    @[ -e Makefile.d ] && rm Makefile.d;

distclean: clean
    @echo "Removing executables";
    @find $(BINDIR) -type f -exec rm {} \;

Makefile.d: $(shell find $(SRCDIR) -type f -name "*.c")
    @echo "Building dependencies";
    @for file in `find $(SRCDIR) -name "*.c" -print`; do\
        $(CC) $(CFLAGS) -MM -MT $${file/%.c/.o} $$file | tr -d "\n\\" >> Makefile.d.tmp;\
        echo -e "\n" >> Makefile.d.tmp;\
    done;\
    for file in `find $(SRCDIR) -name "*.c" -exec grep -Hs "main(" {} \; | cut -f1 -d':'`; do\
        execname=`basename $$file .c`;\
        objs="$${file/%.c/.o}";\
        for header in `$(CC) $(CFLAGS) -MM $$file | tr " " "\n" | grep ".h$$" | sort | uniq | tr "\n" " "`; do\
            if [ -f $${header/%.h/.c} ]; then\
                objs+=" $${header/%.h/.o}";\
            fi;\
            for obj in `grep "^$${header/%.h/.o}" Makefile.d.tmp | tr " " "\n" | grep ".h$$" | tr "\n" " "`; do\
                if [ -f $${obj/%.h/.c} ]; then\
                    objs+=" $${obj/%.h/.o}";\
                fi;\
            done;\
        done;\
        objs=`echo -n "$$objs"  | tr " " "\n" | sort | uniq | tr "\n" " "`;\
        echo "all: $$execname" >> Makefile.d.tmp;\
        echo "$$execname: $(BINDIR)/$$execname" >> Makefile.d.tmp;\
        echo "$(BINDIR)/$$execname: $$objs" >> Makefile.d.tmp;\
        echo "  @echo \"Linking $$execname\";" >> Makefile.d.tmp;\
        echo "  @\$$(CC) \$$(LDFLAGS) -o \$$@ $$objs \$$(LIBS);" >> Makefile.d.tmp;\
        echo >> Makefile.d.tmp;\
    done;\
    mv Makefile.d.tmp $@;\

它并不是非常健壮,所以请随意提供改进。这是它的工作原理。 这里的想法是,在$(SRCDIR)中的每个.c文件中,查找包含main函数的文件(grep "main(" src/*.c)。然后,对于每个本地包含#include "myfile.h",将myfile.o添加到该可执行文件的对象列表中,然后在Makefile.d中编写上述行。

编辑 此脚本的新版本支持用于链接bin文件的依赖项。


嗯...这还不完美,因为文件没有递归加载。如果有人有想法... - Mat

0

我也建议使用自动工具(autotools),你最终会学会欣赏它,并且它绝对值得。你可以寻找一个关于如何使用它的教程,或者如果你不介意阅读一本小书,我建议阅读这个autobook。然而,如果你只想要一个简单的 Makefile 模板,我使用的是这个:

BIN     = <binary name>
OBJ     = <object files *.o>
LIB     = <libraries>
CFLAGS  = -O2 -Wall -I. 
LDFLAGS = <ld flags>

CC = gcc
LD = ld
RM = rm -f 

all:: $(BIN)

$(BIN): $(OBJ)
    $(CC) $(LDFLAGS) $(OBJ) $(LIB) -o $(BIN)

clean:
    $(RM) $(BIN) *.o

%.o: %.c %.h
    $(CC) $(CFLAGS) -c $<

0
如果您对标准的UNIX环境感兴趣,为什么不仅使用POSIX中提供的make呢?
# Lean and mean.
.POSIX:

CFLAGS = -DNDEBUG -O
LDFLAGS = -s

TARGETS = abc def

all: $(TARGETS)

clean:
    rm -f $(TARGETS)

# Some dependencies.
abc: def.h

# Some special rules.
def: def.c
    $(CC) $(CFLAGS) $(LDFLAGS) -o $@ def.c -l m

对于大多数小项目,您不需要比默认设置更复杂的东西;可能您应该集中精力于项目本身,而不是浪费时间编写复杂的 makefile 逻辑。

POSIX make 的一个限制是没有模式规则,后缀规则无法处理目录;因此,在不同目录中分散源文件将要求为每个文件编写单个规则或进行递归构建。就我个人而言,对于小型项目,我不会费心将源文件移动到其他地方。坚持使用 POSIX make 还将在其他 POSIX 兼容的构建系统(例如 BSD make)之间提供可移植性。

关于自动依赖项计算,请阅读有关 gcc -M 的内容。它生成包含依赖关系规则的 makefile。然后,您可以在您的 makefile 中 include 它们。但是,我认为更好的想法是在单个头文件中跟踪所有重要的依赖项,并通过后缀规则使每个文件依赖于此头文件。这并不麻烦,而且仍然节省了一些可移植性。

总之,我的建议是不要过于担心复杂性。它们很少有回报。特别是,我从来不喜欢 autotools,并且我肯定永远不会喜欢。UNIX 就是简单。不必要地使事情复杂与 UNIX 精神相悖。 :)


0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接