clangd compile_commands 自助工具

如你所知,如果要在 neovim 中使用 clangd 作为 lsp,需要给 clangd 提供一个 compile_commands.json 文件,这个文件包含了整个项目的源文件以及其编译参数信息,如此 clangd 才能找到对应的源文件以及头文件,从而实现文件间的符号跳转。

原教旨上 clangd 的 compile_commands.json 文件,在 cmake 系统下可以自动生成,基于 make 的构建系统可以通过第三方辅助工具如 bear、compiledb 结合 make 来生成,具体用法参考 bear、compiledb 的官方 github。

clangd 官方关于生成 compile_commands.json 文件的说明参阅 https://clangd.llvm.org/installation。

不过然而可是但是,基于 bear、compiledb 结合 make 来生成 compile_commands.json,往往并不具有可实操性,因为此二者要求在你运行 bear、compiledb 的机器上,必须是可以成功 make 项目的。

这一要求并不容易达成,原因如下:

  1. 开发机和测试机不同,也就是说,很可能你是在开发机上编写代码,在测试机上编译执行(这很常见,对吧?),开发机上很可能并没有编译构建环境。最典型的就是你在 Windows 下开发 linux 程序,或者是在 MacOS 下开发需要在 Windows 下 keil 环境中编译执行的程序。

  2. 对于内核开发人员来说,如果是 kernel 源代码,要生成 compile_commands.json 还得先 make,虽然好像可以不真正 make 构建,只是做做样子,但这也还是过于 heavy,并且开发机上可能还并不具备内核编译环境(也就是你会 make 失败,compile_commands.json 无法正常生成);如果是在开发 module,要能成功编译 module,需要在开发机上安装对应版本的内核开发包,这本身就可能无法满足(比如你是在 Windows 或 MacOS 下开发),如果模块是要在多个内核版本上编译运行的话,还需要安装多个内核开发包,就更难以满足了。

针对以上问题,笔者分析了一下 clangd compile_commands.json 的结构与要求,写了个简单的脚本,此脚本直接对项目的目录结构文件进行分析,并生成 compile_commands.json,无需依赖任何第三方工具以及构建环境。

脚本本身很简单,主要是提供一个绕过上述问题的思路。

基本原理是:扫描项目中的所有源文件(.c)以及头文件(.h),根据头文件的路径,为所有源文件生成 arguments 配置的 -I 参数(会做一定的头文件路径层级回溯),并填写到 compile_commands.json 文件中。感兴趣的读者在自己的工程中跑一下此脚本并观察输出即可。

此脚本在我的项目上 works fine,clangd lsp 可以正常工作。

下面是脚本:

#!/usr/bin/python
import osimport json
hated_dirs = [ ".cache", ".git", "rpmbuild" ]# wanted_dirs = [ "kernel", os.path.join("include", "linux") ]wanted_dirs = []header_dir_backtrace_level = 1compile_commands_json = "compile_commands.json"
def is_header_file(file): return file.endswith(".h")
def is_source_file(file): return file.endswith(".c")
def contains_include(path): return "include" in path
def contains_wanted_dir(dir): for tdir in wanted_dirs: if tdir in dir: return True
return False
def is_wanted_dir(root, cwd, dir): # for thedir in [os.path.join(root, _) for _ in wanted_dirs]: for thedir in wanted_dirs: if thedir.startswith(os.path.join(cwd, dir)) or os.path.join(cwd, dir).startswith(thedir): return True
return False
def filter_wanted_dir(root, cwd, dirs): return [dir for dir in dirs if is_wanted_dir(root, cwd, dir)] if wanted_dirs else dirs
def is_hated_dir(root, cwd, dir): # for thedir in [os.path.join(root, _) for _ in hated_dirs]: for thedir in hated_dirs: if os.path.join(cwd, dir).startswith(thedir): return True
return False
def filter_hated_dir(root, cwd, dirs): return [dir for dir in dirs if not is_hated_dir(root, cwd, dir)] if hated_dirs else dirs
def all_files(root): for cwd, dirs, files in os.walk(root): # dirs[:] = [dir for dir in dirs if dir not in hated_dirs] dirs[:] = filter_hated_dir(root, cwd, dirs) dirs[:] = filter_wanted_dir(root, cwd, dirs)
for file in files: yield (file, os.path.join(cwd, file), cwd)
def scan_files(root): header_dirs = list() source_files = list()
for (file, filepath, cwd) in all_files(root): if is_header_file(file) and cwd not in header_dirs: header_dirs.append(cwd) elif is_source_file(file): source_files.append(filepath)
return header_dirs, source_files
def fetch_upper_dirs(dir): for _ in range(header_dir_backtrace_level): dir = os.path.dirname(dir) yield dir
def cook_header_dirs(root, header_dirs): for dir in header_dirs: for upperdir in fetch_upper_dirs(dir): if contains_include(dir) and not contains_include(upperdir): continue if not root.startswith(upperdir) and upperdir not in header_dirs: header_dirs.append(upperdir)
def canonical_path(root, path): return path.replace(root, ".")
def gen_arguments(root, header_dirs): arguments = [ "gcc" ]
for dir in header_dirs: arguments.append("-I") arguments.append(canonical_path(root, dir))
return arguments
def gen_compile_commands(root, header_dirs, source_files): compile_commands = list()
for filepath in source_files: desc = { "directory": root, "file": canonical_path(root, filepath) } desc["arguments"] = gen_arguments(root, header_dirs) compile_commands.append(desc)
return compile_commands
def write2file(path, content): with open(path, 'w') as f: f.write(content)
def preproccess(root): global wanted_dirs global hated_dirs
hated_dirs[:] = [os.path.join(root, _) for _ in hated_dirs] wanted_dirs[:] = [os.path.join(root, _) for _ in wanted_dirs]
def main(root): preproccess(root) header_dirs, source_files = scan_files(root) cook_header_dirs(root, header_dirs) write2file(compile_commands_json, json.dumps(gen_compile_commands(root, header_dirs, source_files)))
if __name__ == "__main__": main(os.getcwd())


原文始发于微信公众号(窗有老梅):clangd compile_commands 自助工具

版权声明:admin 发表于 2023年9月29日 上午10:02。
转载请注明:clangd compile_commands 自助工具 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...