第 2 章 wmake:OpenFOAM 自研构建系统与编译配置体系
OpenFOAM 拒绝使用 CMake、Autotools、Bazel 等主流构建系统,而是自研了一套名为 wmake 的构建系统。理解 wmake,才能理解"同一份源码为什么能在几十种平台/编译器/精度组合下被编译出来",也才能自己编写一个 OpenFOAM 库或求解器。
2.1 wmake 的设计目标
wmake/wmake 文件顶部的注释(第 18--47 行)直接给出了设计目标:
sh
# Script
# wmake
#
# Description
# General, wrapped make system for multi-platform development.
#
# Intermediate object and dependency files retain the tree structure
# of the original source files, with its location depending on the
# build context.
#
# 1. Building within the OpenFOAM project:
# The tree is located under $WM_PROJECT_DIR/build/$WM_OPTIONS/
#
# 2. Building applications or libraries outside the OpenFOAM project:
# The tree is located under its local Make/$WM_OPTIONS/
归纳为四条:
- 多平台:不依赖外部构建工具,只要有 POSIX shell + Make + C++ 编译器即可工作。
- 零配置 :源码目录里只需一个
Make/子目录(内含files与options两个文件),wmake自动完成其余一切。 - 隔离的中间产物 :
.o与.dep文件不复用源码目录,而是镜像到独立的构建树 里(build/$WM_OPTIONS/或Make/$WM_OPTIONS/),从而支持同一份源码在不同$WM_OPTIONS下并存编译。 - 递归构建 :
wmake -all自动遍历子目录,遇到Allwmake就执行。
2.2 平台标识:WM_OPTIONS 的组装
wmake 的一切都围绕一个字符串变量 WM_OPTIONS 展开。它由 etc/config.sh/settings 组装(第 127--134 行):
sh
export WM_LABEL_OPTION="Int$WM_LABEL_SIZE"
export WM_OPTIONS="$WM_ARCH$WM_COMPILER$WM_PRECISION_OPTION$WM_LABEL_OPTION$WM_COMPILE_OPTION"
export FOAM_APPBIN="$WM_PROJECT_DIR/platforms/$WM_OPTIONS/bin"
export FOAM_LIBBIN="$WM_PROJECT_DIR/platforms/$WM_OPTIONS/lib"
以最常见的组合(etc/bashrc 默认值:WM_COMPILER=Gcc、WM_PRECISION_OPTION=DP、WM_LABEL_SIZE=32、WM_COMPILE_OPTION=Opt)为例,Linux x86_64 上 WM_ARCH=linux64,于是:
ini
WM_OPTIONS = linux64 Gcc DP Int32 Opt
└─┬──┘ └┬┘ └┬┘ └─┬─┘ └┬┘
架构 编译器 精度 label 编译选项
= linux64GccDPInt32Opt
逐段解释:
| 段 | 取值来源 | 含义 |
|---|---|---|
linux64 |
uname -s + uname -m 决定 |
平台架构(linux64/linuxARM64/darwin64/win64...) |
Gcc |
WM_COMPILER |
编译器族(Gcc/Clang/Icc/Icx/Amd/Arm/Cray/Fujitsu/Nvidia/Mingw) |
DP |
WM_PRECISION_OPTION |
浮点精度:DP(double)、SP(float)、SPDP |
Int32 |
WM_LABEL_SIZE |
label(索引类型)位宽:Int32(int) / Int64(long) |
Opt |
WM_COMPILE_OPTION |
编译模式:Opt/Dbg/Debug/Prof |
这个字符串同时决定了三样东西:头文件/库的安装路径 (platforms/$WM_OPTIONS/{bin,lib})、中间产物的路径 (build/$WM_OPTIONS/)、以及编译宏。最后一点尤其关键,见 2.6。
架构判定逻辑见 etc/config.sh/settings 第 32--123 行,对 Linux、Darwin、MinGW、SunOS 等做了分支。例如 Windows 交叉编译会设 WM_ARCH=win64(或 winARM64);在 Darwin 上若指定 WM_COMPILER=Gcc 但没给 version=,会被自动改写为 Clang(第 76--90 行)。
2.3 wmake 工具族
wmake/ 目录下除了主脚本 wmake,还有一整套配套脚本(Glob wmake/* 可见):
| 脚本 | 职责 |
|---|---|
wmake |
主构建器 |
wmakeLnInclude |
生成 lnInclude/ 符号链接目录(扁平头文件) |
wmakeLnIncludeAll |
全项目递归生成 lnInclude |
wclean |
删除中间产物与出产物 |
wcleanLnIncludeAll |
清理所有 lnInclude |
wrmo |
删除某个 .o |
wrmdep |
删除依赖文件(触发重编) |
wdep |
定位某个源文件对应的 .dep |
wmakeCollect |
把子目录 Makefile 收集为单个 Makefile |
wmake-with-bear |
配合 bear 生成 compile_commands.json(供 clangd/IDE 用) |
以及 wmake/scripts/ 下的一众辅助脚本:wmakeFilesAndOptions、makeFiles、makeOptions、makeDepend、makeTargetDir、AllwmakeParseArguments,和一组 have_* 第三方库探测脚本。
2.3.1 自动生成 Make/files 与 Make/options
对于一个全新目录,wmakeFilesAndOptions(第 54--62 行)会:
sh
if [ -e Make ]
then
echo "$Script error: Make directory already exists" 1>&2
exit 1
fi
mkdir Make
[ -e Make/files ] || "${0%/*}/makeFiles"
[ -e Make/options ] || "${0%/*}/makeOptions"
它先扫描当前目录的源文件与子目录,再由 makeFiles 生成 Make/files(列出要编译的源文件与产物类型),由 makeOptions 生成 Make/options(列出 include 路径与链接库)。
2.4 Make/files 与 Make/options:两个"编译清单"
2.4.1 Make/files 的格式与产物类型
Make/files 用宏来区分产物:
make
dictionary.C
primitiveEntry.C
...
LIB = $(FOAM_LIBBIN)/libOpenFOAM
上面声明编译若干源文件并打成共享库 libOpenFOAM.so。产物类型由目标名决定(见 wmake/makefiles/general 第 191--278 行的 Make 规则):
| 目标 | 产物 | 触发方式 |
|---|---|---|
libso |
共享对象 .so |
LIB = .../libFoo(默认) |
lib |
静态库 .a |
由 wmake lib 显式指定 |
libo |
单一 .o(把多个目标文件 ld -r 合并) |
由 wmake libo 指定,常用于解决循环依赖 |
exe |
可执行文件 | EXE = $(FOAM_APPBIN)/foo |
objects |
仅编 .o,不链接 |
用于语法检查 |
关键片段(wmake/makefiles/general 第 199--204 行,可执行文件链接规则):
make
$(EXE)$(EXT_EXE): $(OBJECTS)
@$(WM_SCRIPTS)/makeTargetDir $(EXE)
$(call QUIET_MESSAGE,link,$(EXE)$(EXT_EXE))
$E $(LINKEXE) $(FOAM_EXTRA_LDFLAGS) $(OBJECTS) -L$(LIB_PLATFORMS) \
$(EXE_LIBS) $(PROJECT_LIBS) $(SYS_LIBS) \
$(LINK_LIBS) $(GLIBS) -o $(EXE)$(EXT_EXE)
第 238--243 行是共享库链接规则,注意它会 -L$(LIB_PLATFORMS)(指向 platforms/$WM_OPTIONS/lib):
make
$(LIB)$(EXT_SO): $(OBJECTS)
@$(WM_SCRIPTS)/makeTargetDir $(LIB)
$(call QUIET_MESSAGE,link,$(LIB)$(EXT_SO))
$E $(LINKLIBSO) $(FOAM_EXTRA_LDFLAGS) $(OBJECTS) -L$(LIB_PLATFORMS) \
$(LIB_LIBS) $(GLIB_LIBS) -o $(LIB)$(EXT_SO)
2.4.2 Make/options 的格式
Make/options 提供五组变量(wmake/makefiles/general 第 27--33 行的注释给出定义):
make
EXE_INC = -I$(LIB_SRC)/finiteVolume/lnInclude \
-I$(LIB_SRC)/meshTools/lnInclude
EXE_LIBS = -lfiniteVolume -lmeshTools
| 变量 | 用途 |
|---|---|
EXE_INC |
可执行/库编译时的额外 include 路径与宏定义 |
EXE_LIBS |
可执行文件链接的库 |
LIB_LIBS |
共享库(.so)链接的库 |
LIBO_LIBS |
单对象库(.o)链接的库 |
注意 OpenFOAM 惯例:EXE_INC 里写 -I.../lnInclude,而不是写具体头文件路径------这正是 2.5 节"扁平头文件"策略的直接后果。
2.4.3 预处理:Make/{files,options} 会被 cpp 处理
一个容易被忽略的细节:Make/files、Make/options 并非直接被 Make 读取,而是先经 C 预处理器 cpp 过滤,再抽取。wmake/makefiles/files 第 52--54 行:
make
define PREFILTER_MAKE_OPTIONS
@$(CPP) -Ulinux -Uunix $(GFLAGS) $1 | sed -e 's@ *@ @g' > $2
endef
也就是说,你可以在 Make/options 里写 #ifdef、#include、以及像 -DWM_LABEL_SIZE=$(WM_LABEL_SIZE) 这样的宏------它们会先被 cpp 展开。-Ulinux -Uunix 特意取消定义了 linux/unix 宏,避免把路径中的 x86_64-linux-gnu 误替换。
处理 Make/files 的那段(第 73--83 行)尤为巧妙:
make
$(SFILES): $(MAKE_DIR)/files
$(call PREFILTER_MAKE_OPTIONS,$<,${FILES})
# Extracted macro definitions.
@sed -n -e '/=/p' $(FILES) > $(VARS)
@echo "SOURCE = \\" > $(SFILES)
@sed -e '/=/d; /^#/d; /^[ \t]*$$/d' -e 's@[ \t]*$$@ \\@' $(FILES) >> $(SFILES)
- 含
=的行被抽到variables文件(这就是LIB = ...、EXE = ...); - 不含
=、非注释、非空的行被当作源文件列表,逐行加行尾续行符\,写入sourceFiles,形成SOURCE = a.C \ b.C \ ...。
最终 Make/$WM_OPTIONS/{options,variables,sourceFiles} 被 wmake/makefiles/general 第 164--166 行 include 进来,并据此推导出对象文件列表:
make
include $(OBJECTS_DIR)/options
include $(OBJECTS_DIR)/variables
include $(OBJECTS_DIR)/sourceFiles
DEPENDENCIES=$(SOURCE:%=$(OBJECTS_DIR)/%.dep)
BASENAMES=$(basename $(SOURCE))
OBJECTS=$(BASENAMES:%=$(OBJECTS_DIR)/%.o)
2.5 lnInclude:扁平化头文件
OpenFOAM 的库内部文件是按功能分目录的(如 src/OpenFOAM/db/dictionary/dictionary.H),但对外只暴露一个 lnInclude/。wmakeLnInclude 会遍历库内所有 .H,在 lnInclude/ 下建立指向它们(相对路径)的符号链接。于是跨库引用只需:
make
PROJECT_INC = \
-I$(LIB_SRC)/$(WM_PROJECT)/lnInclude \
-I$(LIB_SRC)/OSspecific/$(WM_OSTYPE)/lnInclude
(wmake/makefiles/general 第 88--90 行)。这就是为什么所有库都在 src/Allwmake 开头执行 wmakeLnInclude -u(第 25--26 行)------必须先建立链接,再编译。
lnInclude 目录本身通过 .gitignore 排除,不进入版本库,属于"生成物"。
2.6 Rules:编译器与平台规则的组装
wmake/rules/ 是编译规则的核心。它分两级(见 wmake/rules/General/version 第 17--21 行):
make
ARCHITECTURE_RULES = $(WM_DIR)/rules/$(WM_ARCH) # 例如 rules/linux64
COMPILER_FAMILY = $(shell echo "$(WM_COMPILER)" | sed -e 's/[-+.0-9~].*//')
DEFAULT_RULES = $(ARCHITECTURE_RULES)$(COMPILER_FAMILY) # 例如 rules/linux64Gcc
RULES = $(ARCHITECTURE_RULES)$(WM_COMPILER) # 例如 rules/linux64Gcc11
rules/General/:与平台无关的通用规则(general、standard、version、transform、flex、bison、lemon、ragel、moc、X...)。rules/<arch>/:架构特定规则(如rules/linux64/general)。rules/<arch><compiler>/:平台+编译器特定规则(如rules/linux64Gcc/c++、rules/linux64GccOpt/c++Opt、rules/linux64Gcc/general)。rules/<arch><compiler><version>/:特定编译器版本的可覆盖规则(sinclude $(RULES)/general/$(RULES)/c++,来自version第 55--57 行)。
2.6.1 一条完整的编译命令是怎么拼出来的
以 GCC 为例,rules/linux64Gcc/c++(第 1--21 行)把各段拼成 c++FLAGS:
make
include $(GENERAL_RULES)/Gcc/c++ # 编译器族规则
c++ARCH := -m64 -pthread
ifneq (,$(strip $(WM_COMPILE_OPTION)))
sinclude $(DEFAULT_RULES)/c++$(WM_COMPILE_OPTION) # 如 c++Opt / c++Debug
endif
c++FLAGS = \
$(c++ARCH) $(GFLAGS) $(c++WARN) $(c++OPT) $(c++DBUG) $(ptFLAGS) \
$(FOAM_EXTRA_CXXFLAGS) $(LIB_HEADER_DIRS) -fPIC
Ctoo = $(WM_SCHEDULER) $(CC) $(c++FLAGS) -c $< -o $@
而编译器族规则 rules/General/Gcc/c++(第 9--19 行)给出语言标准与告警:
make
CC := g++$(COMPILER_VERSION) -std=c++17
c++ARCH :=
c++OPT := -O2
ptFLAGS := -DNoRepository -ftemplate-depth=1000
c++WARN = \
-Wall -Wextra -Wold-style-cast \
-Wnon-virtual-dtor -Wno-unused-parameter -Wno-invalid-offsetof \
-Wno-attributes -Wno-unknown-pragmas
这里面有四个必须理解的要点:
-std=c++17:v2606 已要求 C++17。-ftemplate-depth=1000:OpenFOAM 的模板嵌套极深(GeometricField<Type, fvPatchField, volMesh>之上再叠表达式模板),默认模板深度不够,必须放大到 1000。-DNoRepository:告诉编译器/链接器"不需要显式实例化模板仓库",配合.C里已做的手工实例化(src/.../Make/files常含?或.C显式实例化)。OpenFOAM 通过在每个库的files里显式实例化所需的Type(scalar/vector/tensor 等),避免链接期找不到模板符号。-fPIC(在rules/linux64Gcc/c++中):所有库默认编译为位置无关代码,便于共享库链接。
2.6.2 链接规则与"符号裁剪"陷阱
rules/General/Gcc/link-c++(第 1--33 行)定义链接方式,并处理一个经典陷阱:
make
LINKLIBSO = $(CC) $(c++FLAGS) -shared
LINKEXE = $(CC) $(c++FLAGS)
...
# Default linker, assume ld - no dummy stub
else
LINKLIBSO += -Xlinker --add-needed -Xlinker --no-as-needed
LINKEXE += -Xlinker --add-needed -Xlinker --no-as-needed
endif
# Link dummy stub to resolve libPstream symbols used by libOpenFOAM
ifneq (,$(LINKEXE_STUB))
ifneq (,$(findstring -lOpenFOAM,$(PROJECT_LIBS)))
LINKEXE += -L$(FOAM_LIBBIN)/dummy -lPstream
endif
endif
--no-as-needed 强制保留"看起来没被直接引用"的库依赖,否则在启用 --as-needed 的发行版上,libOpenFOAM 对 libPstream 的隐式依赖会被裁剪,导致运行期符号缺失。而 LINKEXE_STUB / dummy/libPstream 则专门用来在"纯串行"构建里,为 libOpenFOAM 中引用的 Pstream 符号提供一个空实现(dummy)。
2.6.3 MPI 与 OpenMP 规则
- MPI :
rules/General/mplib*(如mplibOPENMPI、mplibINTELMPI、mplibHPMPI、mplibMSMPI)按WM_MPLIB选择。核心库Pstream通过WM_MPLIB决定编译哪个后端。 - OpenMP :由
WM_COMPILE_CONTROL的+openmp/~openmp控制(wmake/makefiles/general第 75--79 行):
make
ifeq (,$(findstring ~openmp,$(WM_COMPILE_CONTROL)))
ifneq (,$(findstring +openmp,$(WM_COMPILE_CONTROL)))
SYS_LIBS = $(LINK_OPENMP)
endif
endif
~openmp 优先级高于 +openmp("禁用优先"),这是 WM_COMPILE_CONTROL 的一贯约定。
2.7 WM_COMPILE_CONTROL:一个字符串控制一切
etc/bashrc 第 85--101 行列出了 WM_COMPILE_CONTROL 支持的开关,这是 OpenFOAM 构建最灵活的旋钮:
| 开关 | 效果 |
|---|---|
+gold / +lld / +mold / +link-ld |
换链接器 |
~libz |
关闭 libz 压缩支持 |
~rpath |
关闭 macOS 的 rpath 处理 |
+openmp / ~openmp |
开关 OpenMP |
+ccache |
用 ccache 加速重复编译 |
+strict / ++strict |
更严格的弃用告警(-DFOAM_COMPILE_STRICT / =2) |
version=... |
编译器版本后缀(version=11 → g++-11) |
~openfoam |
完全不链接 OpenFOAM 库(编译纯独立程序) |
version= 的解析见 rules/General/version 第 29--33 行,用 sed 从字符串里抠出版本号,拼成 g++-11 之类。~openfoam 的生效见 wmake/makefiles/general 第 94--98 行:一旦启用,PROJECT_INC 与 PROJECT_LIBS 被清空。
2.8 wmake -all 的递归语义
wmake 顶部注释(第 34--37 行)明确:
perl
When `wmake -all` is used, the following rules are applied:
1. If `Allwmake.override` exists, use it.
2. (OR) If `Allwmake` exists, use it.
3. (OR) descend into each sub-directory and repeat.
Allwmake.override 的优先级高于 Allwmake,这是给打包者/发行版留的"打补丁"入口。而对于纯应用目录(没有自定义 Allwmake),wmake -all 会退化为"遍历所有子目录逐个编译",其规则定义在 wmake/makefiles/apps:
make
.PHONY: $(FOAM_APPS)
$(FOAM_APPS):
+@wmake -all $(TARGET) $@
2.9 构建元数据:Cvertoo 与 build-info
OpenFOAM 有一类特殊的 .Cver 源文件(见 rules/General/version),它们在编译前会被 wmake-build-info 处理,把 Git 提交哈希、分支、日期、API 等注入到源码字符串里,再送入编译器:
make
Cvertoo = \
$(WM_SCRIPTS)/wmake-build-info -update -filter $< > $(@D)/$(<F).cc; \
$(CC) $(c++FLAGS) -c $(@D)/$(<F).cc -o $@
这就是为什么每个 OpenFOAM 可执行文件都能自报身世(Using: OpenFOAM-com (2012)、Build: b830beb5ea-20210429)。而 WM_VERSION = OPENFOAM=2606(rules/General/general 第 2 行)会被展开成编译宏 -DOPENFOAM=2606,用于源码里的条件编译(例如区分不同的特性开关)。
wmake -build-info 子命令(wmake 帮助第 119 行)可查询或管理 {api, branch, build} 元信息,foamEtcFile -show-api / -show-patch 则给出脚本友好的一行输出。
2.10 与 CMake 路线的对比
| 维度 | wmake | CMake |
|---|---|---|
| 依赖 | POSIX shell + Make + 编译器 | CMake + generator(Ninja/Make) |
| 配置方式 | 环境变量 WM_* + Make/{files,options} |
CMakeLists.txt |
| 多平台 | 平台字符串 WM_OPTIONS 直接编码进路径 |
toolchain file + generator |
| 头文件组织 | 库内 lnInclude/ 符号链接 |
target_include_directories |
| 循环依赖 | 用 libo(单对象)显式打破 |
需拆库或 target_link_libraries 调序 |
| 元数据注入 | .Cver + wmake-build-info |
configure_file |
| IDE 支持 | wmake-with-bear 生成 compile_commands.json |
原生支持 |
CMake 更"标准",wmake 更"自洽且零依赖"。选择后者,是 OpenFOAM 由来已久的历史与技术惯性,但也确实换来了"在任何 HPC 集群上,只要有一个 C++ 编译器就能构建"的极强可移植性。
2.11 一次完整的构建到底发生了什么
把上面所有要素串起来,当你在某个应用目录执行 wmake 时,实际流程是:
wmake读Make/files与Make/options,交给cpp预处理,抽取源文件列表与产物声明,写入Make/$WM_OPTIONS/{options,variables,sourceFiles}。- 依据
wmake/rules/<arch><compiler>/*组装c++FLAGS、LINKEXE、LINKLIBSO。 - 以
wmake/makefiles/general为主 Makefile,编译每个.C为.o(路径镜像源码树),链接成libso/exe/libo。 - 依赖由
wmkdepend(wmake/src/wmkdepend.cc,由wmkdepend.rl+ Ragel 生成)自动生成.dep文件,供增量编译。 - 产物落到
platforms/$WM_OPTIONS/{bin,lib};中间产物落在build/$WM_OPTIONS/(项目内)或Make/$WM_OPTIONS/(项目外)。
理解这条链路后,任何"编译找不到符号"、"改了头文件没重编"、"链接器裁剪了依赖"的问题,都能快速定位到 wmake 的对应环节。
本章要点回顾
WM_OPTIONS=架构 + 编译器 + 精度 + label位宽 + 编译选项,它同时决定安装路径、中间产物路径、编译宏。Make/files声明编译什么、产物是什么 (libso/lib/libo/exe);Make/options声明头文件路径与链接库。- 两个文件都先经
cpp预处理,因此支持宏与条件编译。 lnInclude/用符号链接 实现扁平头文件,跨库引用只需-I.../lnInclude。- 编译标配
-std=c++17 -O2 -DNoRepository -ftemplate-depth=1000 -fPIC,这是为 OpenFOAM 的深模板量身定制。 WM_COMPILE_CONTROL(+openmp/+gold/~openfoam/+strict/version=)是一站式编译开关。- 构建元数据用
.Cver+wmake-build-info注入,实现可执行文件"自报身世"。