我知道像CMake和GNU Autotools这样的工具,但我正在尝试自己编写一个通用构建系统,用于我的C和C ++项目。我将简要解释一下它是如何工作的,希望有人可以提出改进建议或更好的设计。
构建系统本身存在于项目的一个子目录中(我将其作为Git子模块导入)。项目的根目录有一个包装器makefile,它定义了几个宏,并包含来自所述子目录的主makefile。这完成了大部分工作:它遵循目录组织方案(即,它输出 lib 中的库, bin 中的二进制文件等),它处理自动依赖关系。源代码和DocBook文档,并提供事实上的标准目标:所有,测试,清理,安装 ,以及其他人。
以下是构建两个二进制文件foo和bar的包装器makefile可能如下所示:
# foo-specific macros
FOO_SRC_FILES = foo1.c foo2.c foo3.c
FOO_OBJ_FILES = $(FOO_SRC_FILES:.c=.o)
FOO_BIN_FILE = foo
# bar-specific macros
BAR_SRC_FILES = bar1.c bar2.c
BAR_OBJ_FILES = $(BAR_SRC_FILES:.c=.o)
BAR_BIN_FILE = bar
# Inform the build system about them
SRC_FILES = $(FOO_SRC_FILES) $(BAR_SRC_FILES)
OBJ_FILES = R(BAR_OBJ_FILES) $(BAR_OBJ_FILES)
BIN_FILES = $(FOO_BIN_FILE) $(BAR_BIN_FILE)
# Only install the binaries. If I were building a library, I would instead
# select the "lib" and perhaps "include" directories.
INSTALL = bin
INSTALL_DIR = /usr/share
# Use the build system
include build/build.mk
现在问题就在于此。虽然 build.mk 可以使用模式规则来创建依赖项和目标文件,但只有一个 OBJ_FILES ,只有一个 BIN_FILES 。因此,如果我在构建系统中放置如下所示的模式规则,如下所示:
$(BIN_DIR)/$(BIN_FILES): $(OBJ_FILES:%=$(OBJ_DIR)/%) $(LIB_FILES:%=$(LIB_DIR)/%) | $(BIN_DIR)
$(CC) $(LDFLAGS) -o $@ $(OBJ_FILES:%=$(OBJ_DIR)/%) -L $(LIB_DIR) $(LIB_FILES:lib%.a=-l %)
然后 foo 将依赖并链接 bar 所做的一切,反之亦然。所以我最终要做的是要求用户将这些规则放在包装器makefile中,即使他们觉得它们属于 build.mk :
$(BIN_DIR)/$(FOO_BIN_FILE): $(FOO_OBJ_FILES:%=$(OBJ_DIR)/%) $(FOO_LIB_FILES:%=$(LIB_DIR)/%) | $(BIN_DIR)
$(CC) $(LDFLAGS) -o $@ $(FOO_OBJ_FILES:%=$(OBJ_DIR)/%) -L $(LIB_DIR) $(FOO_LIB_FILES:lib%.a=-l %)
$(BIN_DIR)/$(BAR_BIN_FILE): $(BAR_OBJ_FILES:%=$(OBJ_DIR)/%) $(BAR_LIB_FILES:%=$(LIB_DIR)/%) | $(BIN_DIR)
$(CC) $(LDFLAGS) -o $@ $(BAR_OBJ_FILES:%=$(OBJ_DIR)/%) -L $(LIB_DIR) $(BAR_LIB_FILES:lib%.a=-l %)
当然,同样的问题也适用于图书馆。好处是这些规则几乎可以逐字复制和粘贴;只需要更改前缀(例如, FOO 或 BAR )。
解决此问题的想法包括:
我真的很感激一些想法。
PS:我知道最后两个代码示例中的模式匹配表达式可以替换为文本函数,但这些是GNU Make特定的。我使用的样式更具可移植性,实际上是下一版POSIX标准的附加列表。
答案 0 :(得分:0)
我已经开始为自己的C项目开发一个类似的系统,但我使用的逻辑依赖于一些我认为特定于GNU Make的功能。
主要思想是使用$(eval)
和$(call)
的组合,定义构建系统的逻辑,然后应用于项目树。
为此,我在每个目录和子目录中都有一个以下表单的Makefile,我将其命名为Srcs.mk
:
SRC := foo.c foo_bar.c bar.c
TARGET := foo_bar
SRC_DIR := src
OBJ_DIR := obj
我定义了一个变量,实际上是一个宏,用$(call)
进行扩展,然后传递给$(eval)
。它以这种方式定义:
define get_local_variables
include Srcs.mk
$1SRC := $(SRC)
$1SRC_DIR := $(SRC_DIR)
$1OBJ_DIR := $(OBJ_DIR)
$1TARGET := $(TARGET)
TARGET :=
SRC :=
SRC_DIR :=
OBJ_DIR :=
$(call get_local_variables, $(DIR))
将扩展为上述内容,$1
替换为$(DIR)
的内容。然后它将被$(eval)
这样,我为每个目录填充每个目录变量。 我有一些使用这些变量的规则或其他规则,使用相同的原则。
### Macros ###
obj = $(patsubst %.c,$($1OBJ_DIR)/%.o,$($1SRC))
define standard_rules
$($1TARGET): $(obj)
$$(LINK)
$(obj): $($1OBJ_DIR)/%.o:$($1SRC_DIR)/%.c | $($1OBJ_DIR)
$$(COMPILE)
endef
变量计算为$(call)
,然后展开并按$(eval)
读取为makefile片段。
(我使用的是静态模式规则,但它并非内在的想法)。
整个想法基本上是将目录定义为一种命名空间,将数据附加到它们,然后对它们运行函数。 我的实际系统有点复杂,但这就是整个想法。
如果你有办法模仿$(eval)
和$(call)
(我认为这些是特定于GNU make,但不确定),你可以尝试这种方法。
您还可以通过在每个目录中添加SUBDIRS
变量并以递归方式运行在当前目录上运行的相同宏来实现非递归方式。但是应该小心谨慎,不要把它与扩展和评估的顺序混为一谈。
因此get_local_variables
需要评估,然后其余的宏展开。
(我的项目在我的Github帐户中可见,如果你想在make-build-system
下查看。但它远远不够完整^)。
但请注意,当出现问题时调试非常痛苦。 Make(至少GNU)基本上可以在较高的$(call)
或$(eval)
扩展中捕获错误(当存在错误时)。
答案 1 :(得分:0)
我为GNU make
开发了自己的非递归构建系统,名为 prorab ,在那里我解决了你所描述的问题如下。
解决问题的方法有点类似于@VannTen在他的回答中描述的方法,除了我在为下一个二进制文件定义构建规则之前使用宏来清理所有状态变量。
例如,构建两个二进制文件的makefile可能如下所示:
include prorab.mk
this_name := AppName
this_ldlibs += -lsomelib1
this_cxxflags += -I../src -DDEBUG
this_srcs := main1.cpp MyClass1.cpp
$(eval $(prorab-build-app))
$(eval $(prorab-clear-this-vars))
this_name := AnotherppName
this_ldlibs += -lsomelib1
this_cxxflags += -I../src -DDEBUG
this_srcs := main2.cpp MyClass2.cpp
$(eval $(prorab-build-app))
因此,在此示例中,它将构建两个二进制文件:AppName
和AnotherppName
。
正如您所看到的那样,通过设置一些this_
- 前缀变量和调用$(eval $(prorab-build-app))
来配置构建,该变量扩展为定义所有构建,安装,清理等规则。
然后调用$(eval $(prorab-clear-this-vars))
清除所有this_
- 前缀变量,以便可以从头开始为下一个二进制文件再次定义,依此类推。
此外,包含prorab.mk
的第一行也会清除所有this_
- 前缀变量,这样makefile就可以安全地相互包含在内。
您可以在此处阅读有关该构建系统概念的更多信息https://github.com/igagis/prorab/blob/master/wiki/TutorialBasicConcepts.md