简介:gflags-2.1.1 是 Google 开源命令行标志解析库的稳定历史版本,面向需在 C++ 程序中动态管理运行参数的开发者,尤其是仍在维护旧版 Caffe 框架的工程人员。它将参数定义、命令行解析、配置文件读取与 --help 帮助输出集于一体,使学习率、批处理大小等训练参数无需改代码重编译即可调整。压缩包共 50 个文件、约 100KB,以 9 个 cc 源文件与 4 个 h 头文件承载核心实现,辅以 7 个 cmake 构建脚本和 10 个 in 配置模板说明编译安装流程,另有 txt 文档、flagfile 示例与单元测试用例,可完整走通配置到验证的链路。目前已有 222 人学习下载。读者可获取可直接编译的完整源码树、覆盖解析与标志注册的测试用例、flagfile 配置样例及 CMake 集成参考,便于在旧项目中复现稳定行为,或借此理解 gflags 的标志注册、参数校验与多语言接口设计。
1. 版本先定死:为什么 Caffe 这条链上偏偏要 gflags 2.1.1
装 gflags 2.1.1 这件事本身不复杂,复杂的是它在 Caffe 编译链里的位置。很多人第一反应是去拉最新版,结果 Caffe 一编译就是满屏的'ParseCommandLineFlags' is not a member of 'google'——因为 2.2 之后默认命名空间从google换成了gflags,老代码里写死的google::前缀集体失效。反过来,把 2.1.1 老老实实装进 /usr/local,却忘了执行ldconfig,跑起来又是libgflags.so.2.1: cannot open shared object file。这套环境在复现老 Caffe 工程、跑只认 2.1.x 的推理脚本、给旧版训练代码补编译依赖时特别常见,装之前把版本、prefix、命名空间三件事定死,比装完再救省事得多。
2. 装前清场:编译器、CMake 与系统里那几份 gflags
2.1 工具链版本下限与一条命令自检
gflags 2.1.1 的 CMakeLists.txt 里写的是cmake_minimum_required(VERSION 2.8.12),也就是说 CMake 已经低到几乎不用挑;它本身还是 C++98 兼容的代码,不像 2.2.2 那样硬性要求 C++11。真正卡人的是它下游的 Caffe:Caffe 要 C++11,所以编译器版本反而成了实际下限。我这边常用的一套组合在下表里,Ubuntu 16.04 到 20.04 都能照这个范围配。
| 组件 | 最低可用 | 我这边实际 | 说明 |
|---|---|---|---|
| GCC / G++ | 4.8 | 7.5.0 | Caffe 需要 C++11,gflags 2.1.1 自身不需要 |
| CMake | 2.8.12 | 3.10.2 | 2.1.1 的构建脚本按老版本写,新版 CMake 也能跑 |
| make | 3.81 | 4.1 | 并行编译-j依赖它 |
| 系统 | Ubuntu 16.04 | Ubuntu 18.04 | 只影响包名,不影响源码编译 |
进机器第一件事不是解压源码,而是把这三条敲一遍,确认没有多版本混装:
# 确认编译器与构建工具的真实指向,不要只看 which gcc --version | head -1 g++ --version | head -1 cmake --version | head -1 make --version | head -1这里的关键是head -1。有些机器上/usr/bin/gcc是 7.x,但 PATH 前面挂了个 conda 或某个自编译工具链,gcc --version会打出另一套版本。Caffe 编译时用的是 Makefile 里展开的$(CC),和你手动敲的未必是同一个,先看清楚再往下走。
提示:如果你打算在同一台机器上同时维护两套 Caffe(一套老版、一套新版),建议把 gflags 装到独立 prefix,比如
/opt/gflags-2.1.1,别急着塞进 /usr/local,后面第 4 章会讲怎么在 Makefile.config 里指过去。
2.2 把系统自带的 gflags 揪出来并决定怎么处理
这一步是整套安装里最容易被跳过、也最容易在链接期爆炸的环节。Ubuntu 的libgflags-dev装的是 2.1.2 或 2.2.x,头文件在/usr/include/gflags/,库在/usr/lib/x86_64-linux-gnu/,和你要装的 2.1.1 完全平行。两份头文件、两份 so 同时存在,编译器先看到哪一份,取决于-I的顺序,链接器先找到哪一份,取决于-L的顺序和ld.so.cache——这就是玄学感的来源。
先查清楚现场:
# 包管理器层面:有没有 apt 装的 gflags dpkg -l | grep -i gflags # 文件层面:三处常见位置分别看一眼 ls -l /usr/lib/x86_64-linux-gnu/libgflags* 2>/dev/null ls -l /usr/local/lib/libgflags* 2>/dev/null ls -l /usr/include/gflags/ 2>/dev/null # 动态链接器缓存里注册了哪个版本 ldconfig -p | grep gflags如果dpkg -l有输出,说明系统级已经有一份。三种处理策略,按你的实际情况选:
第一种,直接卸掉sudo apt remove libgflags-dev,最干净,但要注意它可能被别的包依赖(apt remove会提示),此时改用第二种。第二种,保留但让 /usr/local 优先:Caffe 的 Makefile.config 里INCLUDE_DIRS默认就把/usr/local/include放在前面,链接时-L/usr/local/lib也排在/usr/lib前面,只要不动默认顺序就不会串。第三种,装到独立 prefix,在 Caffe 里显式指定路径,适合多人共用一台机器、谁也不敢动系统库的场景。
还有一种藏得深的:conda 环境。echo $CONDA_PREFIX如果有值,去看$CONDA_PREFIX/lib/libgflags*和$CONDA_PREFIX/include/gflags/。conda 装的 gflags 常常是 2.2.x,而且它的include目录会被某些构建脚本自动加进搜索路径。我一般会在编译 Caffe 前conda deactivate,把变量环境清干净再动手,省得后面拿ldd一层层倒查。
2.3 源码包落盘与目录约定
目录约定看着是小事,实际上决定了后面出问题时好不好回滚。我的习惯是源码统一放/opt/src,构建目录和源码目录分离,安装 prefix 用/usr/local——因为 Caffe 的 Makefile.config 默认就去这两个地方找,不用改任何路径。
# 源码统一存放,避免散落在 home 目录里找不回来 sudo mkdir -p /opt/src cd /opt/src # 解压后顶层目录就是 gflags-2.1.1 sudo tar -zxvf gflags-2.1.1.tar.gz cd gflags-2.1.1 # 确认包是完整的:这几个文件缺一个都别往下走 ls CMakeLists.txt configure.ac include/gflags/ src/解开后应该能看到CMakeLists.txt、configure.ac、Makefile.am、include/gflags/、src/、cmake/这几项。include/gflags/下有三个头文件:gflags.h、gflags_declare.h、gflags_completions.h,其中gflags_completions.h是 2.1.x 时代的东西,2.2 之后被移除了,后面第 5 章讲升级翻车时会用到这个细节。
configure.ac和Makefile.am同时存在,说明 2.1.1 还保留了 autotools 那一套,这一点和后来的版本不一样,第 3 章末尾会单独说什么时候才值得走那条路。
注意:解压时用
tar -zxvf而不是直接双击解压,一是保证权限位正确,二是避免某些归档工具把符号链接解成普通文件,后面make会报一些莫名其妙的错。
3. 编译安装:gflags 2.1.1 的 CMake 参数逐个过
3.1 配置阶段:五个开关决定后面会不会翻车
gflags 2.1.1 的 CMake 默认值有几个和直觉相反的地方,不显式覆盖就会在后面某个环节找你麻烦。最典型的是BUILD_SHARED_LIBS默认 OFF——默认只产出静态库libgflags.a,而 Caffe 编出来的是libcaffe.so,动态库链接没有开 PIC 的静态库,直接报relocation R_X86_64_32 ... can not be used when making a shared object。
我用的配置命令是这样,每一条都有理由:
# 构建目录和源码分离,改参数时直接删掉重建,不留缓存 cd /opt/src/gflags-2.1.1 rm -rf build && mkdir build && cd build cmake .. \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=/usr/local \ -DBUILD_SHARED_LIBS=ON \ -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ -DBUILD_TESTING=OFF \ -DGFLAGS_NAMESPACE=googleCMAKE_BUILD_TYPE=Release:2.1.1 不设这个变量时构建类型是空的,编译器不加优化,产出的库性能明显偏低。Caffe 前向推理里参数解析不是瓶颈,但没理由留个 O0 的库在系统里。
CMAKE_INSTALL_PREFIX=/usr/local:这是 Caffe 默认搜索路径。改成别的目录不是不行,但后面 Makefile.config 的INCLUDE_DIRS和LIBRARY_DIRS都得跟着改,多一处手工维护就多一处出错的地方。
BUILD_SHARED_LIBS=ON:产出libgflags.so。如果某个项目非要静态链接,2.1.1 在同一个构建里也会生成libgflags_nothreads.a之类,够用。反过来只编静态库再想补动态库,就得整个重编。
CMAKE_POSITION_INDEPENDENT_CODE=ON:即使这次编的是动态库,打开它也保险。将来有人把这套 gflags 改成静态库链接进别的 .so,不会因为 PIC 再翻一次车。这一条属于后悔药,成本几乎为零。
BUILD_TESTING=OFF:不关的话,2.1.1 会把gflags_unittest一起编出来,多花几十秒到几分钟。第一次装建议留着跑一遍单测,确认库本身没问题;跑完再改 OFF 重编一次,或者干脆留着,反正不影响安装产物。
GFLAGS_NAMESPACE=google:这条最容易被忽略。2.1.1 的默认值本来就是google,显式写出来的意义在于:万一以后有人在这份 CMakeCache 上改成gflags,或者你把这套参数抄到别的版本上,差异会立刻暴露,而不是在 Caffe 链接阶段才以undefined reference的形式出现。
配置跑完,别急着make,先确认缓存里的值真的是你写进去的:
# 确认关键变量落地,防止命令行拼错被 CMake 静默忽略 grep -E "GFLAGS_NAMESPACE|BUILD_SHARED_LIBS|CMAKE_INSTALL_PREFIX" CMakeCache.txt输出里应该能看到GFLAGS_NAMESPACE:STRING=google、BUILD_SHARED_LIBS:BOOL=ON、CMAKE_INSTALL_PREFIX:PATH=/usr/local。如果某个变量不见了,多半是拼写问题——CMake 对不认识的-D变量不报错,只在缓存里留下一个 unused 警告,很容易看漏。
3.2 编译安装与 ldconfig:并行度、DESTDIR 和 sudo 的边界
配置对了,编译本身没什么悬念,但有两个细节值得说:并行度和安装后的动态链接器缓存。
# 并行编译,核数用 nproc 拿,别拍脑袋写 -j8 make -j$(nproc) # 安装到 prefix 指定的位置,需要写 /usr/local 所以要 sudo sudo make install # 刷新动态链接器缓存,这一步不做,运行时就找不到 so sudo ldconfigmake -j$(nproc)在小内存机器上要小心。gflags 2.1.1 的源码量不大,一般不会因为并行度过高把内存打爆,但如果你的机器同时还跑着别的构建任务,-j开到核数容易触发 OOM Killer,表现是make突然被 kill、没有任何报错。稳妥做法是-j$(($(nproc)/2)),慢一点但不会中断。
sudo ldconfig是必须的。/usr/local/lib在多数发行版里已经写进/etc/ld.so.conf.d/的某个配置文件中,ldconfig会把它扫进缓存,程序运行时按 SONAME 找库就能命中。少了这一步,make install明明成功,ldd却告诉你libgflags.so.2.1 => not found,很多人到这一步开始怀疑人生。
装完立刻验证产物,别等到 Caffe 编译失败才回头查:
# 1. 库文件:应该有实体文件和两条符号链接 ls -l /usr/local/lib/libgflags* # 2. 头文件:三个 .h 都在 ls -l /usr/local/include/gflags/ # 3. CMake 配置文件:供 find_package 使用 ls -l /usr/local/lib/cmake/gflags/ # 4. pkg-config 文件 ls -l /usr/local/lib/pkgconfig/gflags.pc预期看到libgflags.so.2.1.1是实体,libgflags.so.2.1和libgflags.so是指向它的符号链接。SONAME 是libgflags.so.2.1,所以运行时找的是中间那条链接,这也是为什么后面报错信息里总是出现libgflags.so.2.1而不是2.1.1。
/usr/local/lib/cmake/gflags/下的几个文件决定了find_package(gflags)能不能走 config 模式,Caffe 的 CMake 构建会用到,第 4 章展开。gflags.pc则是 pkg-config 那条路的入口,注意它装在/usr/local/lib/pkgconfig,这个路径默认不在pkg-config的搜索列表里,需要手动加环境变量。
如果这次装完想推倒重来,2.1.1 的 Makefile 里保留了卸载规则:
# 卸载依赖安装时生成的 install_manifest.txt,必须在 build 目录里执行 cd /opt/src/gflags-2.1.1/build sudo make uninstallmake uninstall只删它自己记录过的文件。如果你中间手动往/usr/local/include/gflags/里拷过东西,或者装过另一个版本覆盖了同名文件,卸载是不彻底的,得手工比对install_manifest.txt清理。
3.3 autotools 那条老路,什么时候才值得走
2.1.1 的源码包里configure.ac和Makefile.am都还在,理论上可以走./configure && make && make install那条经典路径:
# autotools 方式,仅在 CMake 不可用时考虑 cd /opt/src/gflags-2.1.1 ./configure --prefix=/usr/local --enable-shared make -j$(nproc) sudo make install命令能跑通,但产物和 CMake 方式不一样:autotools 不会生成/usr/local/lib/cmake/gflags/那一套 config 文件。如果你的下游是 Caffe 的 CMake 构建,它走find_package(gflags)时会退回FindGFlags.cmake里的手工查找逻辑,能找到库,但版本判断和 include 路径的推断不如 config 模式精确。
我的取舍是:只要机器上有 CMake 3.x,一律走 CMake。autotools 这条路留给两种情况——一是目标环境是某种精简系统,CMake 装不上;二是你要做交叉编译,需要--host参数那套工具链传递机制。除此之外没有理由绕远路。
提示:两条路不要混着走。先用 autotools 装一遍,再用 CMake 装一遍覆盖,
/usr/local/lib下容易出现同名但来源不同的 .so,ldconfig之后到底加载哪个,取决于文件名排序和时间戳,排查起来非常费劲。要么彻底make uninstall再换,要么一开始就选对。
4. 接进 Caffe:路径怎么指、库名怎么给
4.1 Makefile.config 三个变量的填法
Caffe 用 Makefile 构建时,头文件搜索路径和库搜索路径全在Makefile.config里,三个变量决定 gflags 能不能被找到:
# Makefile.config 片段:路径类变量的默认值已经包含 /usr/local INCLUDE_DIRS := $(PYTHON_INCLUDE) /usr/local/include LIBRARY_DIRS := $(PYTHON_LIB) /usr/local/lib /usr/lib # 是否用 pkg-config 找依赖,默认关闭 USE_PKG_CONFIG := 0INCLUDE_DIRS展开后变成一堆-I参数,LIBRARY_DIRS展开成一堆-L参数。因为默认值里已经有/usr/local/include和/usr/local/lib,gflags 装在默认 prefix 时这三行一个字都不用改,这也是第 3 章坚持用/usr/local的原因。
如果你把 gflags 装到了别的目录,比如/opt/gflags-2.1.1,就要在这两行前面各加一项,顺序放在最前面,保证优先命中:
# 自定义 prefix 时,放在最前面强制优先 INCLUDE_DIRS := /opt/gflags-2.1.1/include $(PYTHON_INCLUDE) /usr/local/include LIBRARY_DIRS := /opt/gflags-2.1.1/lib $(PYTHON_LIB) /usr/local/lib /usr/lib顺序不能反。C 编译器的-I是按出现顺序查找的,链接器的-L同理,谁在前谁生效。系统里同时存在/usr/include/gflags/gflags.h(2.1.2 的)和/usr/local/include/gflags/gflags.h(2.1.1 的)时,顺序写反就会编进另一份头文件,然后链接时库又是另一份,版本错位的经典现场。
USE_PKG_CONFIG这个开关打开后会走 pkg-config 找 gflags、glog、protobuf 等依赖。用它能省掉手写路径,但前提是gflags.pc在pkg-config的搜索路径里。前面说过/usr/local/lib/pkgconfig默认不在列表内,所以要配合环境变量:
# 让 pkg-config 能找到 /usr/local 下安装的 .pc 文件 export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH # 验证:版本号应该输出 2.1.1 pkg-config --modversion gflags pkg-config --cflags --libs gflags--modversion输出2.1.1才算找对了。如果输出的是2.1.2或者2.2.2,说明命中的是系统包的那份.pc文件,检查PKG_CONFIG_PATH有没有生效、/usr/lib/x86_64-linux-gnu/pkgconfig/gflags.pc是不是还在。
4.2 CMake 构建 Caffe 时 FindGFlags 的查找顺序
换到 CMake 构建 Caffe,找 gflags 的逻辑在cmake/Modules/FindGFlags.cmake里。它大致分两步:先尝试 config 模式,也就是找gflags-config.cmake;找不到再退回手工find_path+find_library,在标准路径和GFlags_ROOT之类的提示变量里搜。
判断有没有找对,看 CMake 配置阶段的这一行输出:
# 在 Caffe 的 build 目录里执行,从输出里捞出 gflags 那一行 cmake .. 2>&1 | grep -i gflags正确的结果长这样,include 和 library 都指向/usr/local:
Found gflags (include: /usr/local/include, library: /usr/local/lib/libgflags.so)如果 library 指向/usr/lib/x86_64-linux-gnu/libgflags.so,说明系统那套抢先了;如果 include 指向/usr/include而 library 指向/usr/local/lib,就是典型的两份混用,必须停下来处理,不能带着这个状态往下编译。这种组合在链接期不一定报错,但运行时行为会非常诡异,比如某些 flag 解析不出来、--help输出缺项。
实在要强制指定,可以用这两个变量覆盖:
# 显式指定,绕过 FindGFlags 的自动搜索 cmake .. \ -DGFlags_INCLUDE_DIR=/usr/local/include \ -DGFlags_LIBRARIES=/usr/local/lib/libgflags.soCMake 的缓存机制在这里也是个坑。第一次配置时找错了,改完环境变量重新cmake ..,它可能还用缓存里的旧值。稳妥做法是把build/CMakeCache.txt删掉重新配置,而不是反复cmake ..试运气。
4.3 用 pkg-config 和一段 CMake 脚本先解耦验证
在把 Caffe 牵扯进来之前,最好单独确认 gflags 的两种查找方式都通。pkg-config 那条刚才已经验过,CMake 这边写个最小工程,几行就够:
# /tmp/check_gflags/CMakeLists.txt cmake_minimum_required(VERSION 3.5) project(check_gflags CXX) # 走 config 模式,找 /usr/local/lib/cmake/gflags/gflags-config.cmake find_package(gflags REQUIRED) message(STATUS "gflags_INCLUDE_DIR = ${gflags_INCLUDE_DIR}") message(STATUS "gflags_LIBRARIES = ${gflags_LIBRARIES}")# 配置并观察输出,不编译任何东西 mkdir -p /tmp/check_gflags/build && cd /tmp/check_gflags/build cmake .. 2>&1 | grep gflagsfind_package(gflags REQUIRED)优先走 config 模式,2.1.1 安装的那套gflags-config.cmake会把gflags_INCLUDE_DIR和gflags_LIBRARIES两个变量填好。如果输出里这两个值都是空的,但也没报Could not find的错误,说明命中了系统里某个不完整的 config 文件,ls /usr/lib/x86_64-linux-gnu/cmake/下一层层找就能定位。
这一步的价值在于把问题隔离开。gflags 自己的查找链路通了,再去编 Caffe,出问题就只可能是 Caffe 的路径配置或依赖顺序,不用在两个项目之间来回怀疑。
5. 踩坑记录:五个把编译卡死的现场
5.1 编译链接阶段的三次翻车
坑一:relocation R_X86_64_32 against symbol ... can not be used when making a shared object
现象:Caffe 的make all走到链接libcaffe.so那一步报错,错误信息很长,关键词就是recompile with -fPIC。原因是 gflags 2.1.1 的 CMake 默认BUILD_SHARED_LIBS=OFF,产出的libgflags.a没开位置无关代码,往动态库里塞就炸。
解决:回第 3 章的配置命令,加-DBUILD_SHARED_LIBS=ON和-DCMAKE_POSITION_INDEPENDENT_CODE=ON,删掉 build 目录重编重装。已经装过静态版的,先把/usr/local/lib/libgflags.a和对应的 cmake 配置文件清掉,再装一次,别让两份产物并存。
坑二:undefined reference to 'google::ParseCommandLineFlags(int*, char***, bool)'
现象:编译能过,链接阶段报符号找不到,而且符号名里带着google::。原因多半是头文件和库版本不匹配——头文件来自 2.1.2(/usr/include),库里导出的是别的命名空间;或者链接顺序有问题,-lgflags排在-lcaffe前面,静态库的符号没被拉进来。
解决:先ldd .build_release/lib/libcaffe.so | grep gflags看实际链到了哪个路径,再nm -D /usr/local/lib/libgflags.so | grep ParseCommandLineFlags确认符号在不在。Caffe 的 Makefile 里库顺序是写死的,真遇到顺序问题,改用 `-Wl,--start-group ... -Wl,--
本文还有配套的精品资源,点击获取