在离线环境的 CentOS 上使用 ccls
背景
系统环境
- 操作系统:CentOS 7.4
- 网络情况:内网/离线,RPM 源可用,可从外部导入文件
- glibc 版本:
GLIBC_2.17
基本思路
由于环境限制,只能手动编译安装 ccls。系统自带的 gcc 版本过低,不满足 ccls 所需的 C++17 版本需求。因此,优先考虑导入二进制的 Clang/LLVM 工具链。
操作、问题及解决方案
1. 导入二进制 clang
LLVM 官网提供预编译包,但由于 glibc 版本限制,最新的高版本 LLVM 工具链无法在 CentOS 7.4 上正常运行。
经过反复尝试,确认当前环境下最高可用的版本为 LLVM 8.0.1,官网提供基于 x86-64 SUSE 编译的二进制兼容,可正常运行。
手动安装到如 /usr/local 目录。
2. 编译 ccls
Clang 工具链已经可以正常使用,且支持 C++17,可用于编译 ccls。若上述工具链安装到了 /usr/local 编译命令则如下。
cmake -H. -BRelease -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=/usr/local
cmake --build Release
问题:
ccls 基于 CMake 工具链,且 cmake_minimum_required(VERSION 3.8)。系统包管理器提供的 CMake 版本仅为 2.18。
解决方案:
CMake 提供一个最新版本(3.19) 40MB 大小的 sh 安装文件,里面包含二进制内容。这么严肃的安装文件,直接把二进制内容塞到文本文件里的操作我是没见过……直接安装即可,经测试,在 CentOS 7.4 上正常兼容。
3. 安装 VS Code 以及相关插件
还是由于版本过低的原因,Vim 7.4 不支持 ccls 相关插件,因此可以选择 VSCode 和 ccls 插件,并搭配 Vim 插件。
4. 启用 ccls
这大概是最重要的一步,但遇到了不少问题。
1. ccls 扫描文件
问题:
启用 ccls 需要提供一些文件让其进行符号扫描、生成缓存等操作,最常见的提供方案为 .ccls 文件(多用于小项目)和基于 CMake 工具链(大型项目)。然而,如果工作的项目使用 Makefile 则很困难——要么迁移一个 CMakeLists.txt,要么手写 .ccls 并配置好包含目录等等,成本都比较高。
解决方案:
如 ccls 文档所述,Bear 可以拦截 make 的输出,并生成 compile_commands.json 文件—— 对于基于 CMake 的项目 ccls 真正需要的文件。
2. Bear 安装
问题:
Bear 的编译安装过于困难——新版本依赖的库、工具过多,虽然可以通过 submodule 解决,但拉取速度、编译速度都堪忧,特别是在离线的网络环境、版本较旧的系统环境下。
解决方案:
使用旧版 Bear,通过 tags 可以找到今年早些时候的 2.4.4 版本,还没有依赖 gRPC 库,编译安装较为简单。
3. Bear 的工作问题
这一条大概是使用 ccls 方案中最困难的问题,也是唯一解决得比较有趣的问题。
问题:
Bear 拦截 make 指令输出,因此第一次需要清空所有缓存的目标文件重新生成,对编译一次半小时以上的大项目这样做效率有些低。
而真正棘手的是,Bear 无法识别工作项目的 make 输出……通过 bear -vvvv 查看,它正常拦截了 make 输出,但无法正常整理生成 compile_commands.json 文件
解决方案:
最为低效的解决方案大概是去调试 Bear 项目源码找出解析问题。
既然 Bear 是通过拦截 Makefile 输出,解析类如 gcc test.c -o test.o -Itest/ 之类的文本生成的 compile_commands.json,那么为什么不能模拟这个过程呢?
这类文件被称为 JSON Compilation Database Format,Clang 8.0 版本下每一项需要 "directory"、"file"、"command"。只需要编写脚本即可完成这个任务。
脚本部分比较 ad hoc,这里就不给出代码了,仅简述思路。
我选择了 Perl 编写脚本,因为其结构性、可读性(?)都比 Bash 脚本好一些,处理字符串也比较迅速。然而,由于需要读取部分 Makefile 中求值的变量,不方便直接用 Perl 取值,因此可以这么操作:
- 调用 Perl 脚本,若未检查到指定环境变量,则复制一份 Makefile;
- 在复制的 Makefile 中添加伪目标——用于调用自身,将 Makefile 内部变量作为环境变量,通过命令行调用再传递此 Perl 脚本;
- Perl 脚本再次被调用,指定环境变量已经从 Makefile 中导出则从
%ENV中读取需要处理的目标目录、文件、包含文件等等; - 模拟 Makefile 输出项,整理为 JSON 格式,最后删除复制的 Makefile。
至此,ccls 已经可以正常工作。
小结
在软件版本较旧的环境上引入新技术如同戴着镣铐跳舞,一方面需要考虑花费代价引入的新技术收益如何,一方面需要考虑如何尽量减少代价。
而在这个案例中两者平衡得较好。穿插半周的调研、思考、实践和编码,还包括一上午的 Perl 快速入门,换来了较少的手动编译安装过程,也没有变动编译工具链,那这些微小的代价都是可接受的。
而同时,ccls 的成功启用也将显著提升来年的工作效率,收益无限——至少 ctags 提供不了基于类型和成员的快速补全、基于类型和成员的快速跳转、函数引用实时查看以及实时的错误提示。