第20章 - 源码架构、编译与贡献
读源码之前先建立一个判断:PDAL 的代码量在 GIS 库里不算大,但它的插件化程度很高。核心库只包含基础类型、pipeline 引擎和”无额外依赖”的 stage;所有依赖第三方库(HDF、E57、TileDB、PostgreSQL、FBX……)的功能都是独立编译的插件。这解释了两件常见困惑:为什么同一个版本在不同发行版里 stage 数量不同;为什么自己编译时常发现某些 driver 不存在。
本章讲仓库结构、编译选项、测试体系与贡献流程。目标读者是需要在团队内维护 PDAL 分支或写自定义插件的工程师。
20.1 仓库结构
PDAL/
├── apps/ # pdal 命令行程序(CLI 入口、帮助、驱动列表)
├── pdal/ # 核心库:Stage/PointTable/PointView/SRS/Plugin 框架
├── io/ # 内建 reader/writer(LAS、Text、BPF、Buffer 等)
├── filters/ # 内建 filter(smrf、cluster、reprojection 等)
├── kernels/ # CLI 子命令(info、translate、tindex、split…)
├── plugins/ # 可选插件:每个子目录一个插件(依赖第三方库)
├── dimensions/ # 维度定义(Json 描述 → 生成的 C++ 头文件)
├── vendor/ # 第三方代码(Eigen、lazperf、nanoflann、nlohmann json…)
├── cmake/ # CMake 模块与选项
├── doc/ # 文档源(stages/*.md、tutorial/、apps/…)
├── test/ # 单元测试数据与测试代码
└── examples/ # 官方示例(writing-filter、writing-reader…)
几个结构上的关键点:
- 核心库不直接依赖 GDAL 以外的重型库:GDAL 是硬依赖(SRS、重投影、VSI 都靠它),PROJ 是 GDAL 的依赖;其他全部按插件组织。
- 维度是生成的:
dimensions/下的 JSON 经dimbuilder生成Dimension::Id枚举,所以新增标准维度要走生成流程,不能手改枚举。 - 每个 stage 一个文件对:
xxxFilter.cpp+xxxFilter.hpp,注册宏写在 .cpp 里。想找某个 stage 的实现,按filters/下的类名直接找最快。 doc/stages/*.md就是线上文档:改一个 stage 的参数时,文档与源码在同一 PR 里改是惯例。
20.2 编译
20.2.1 最小构建
依赖只有 CMake ≥ 3.13、C++17 编译器、GDAL、PROJ;其余都能通过 vendor/ 里的代码或系统库满足:
git clone https://github.com/PDAL/PDAL.git
cd PDAL
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
cmake --build . -j$(nproc)
ctest --output-on-failure
cmake --install .
Windows 上把 -j$(nproc) 换成 --parallel,并确保 GDAL 的 CMake 配置能找到(用 OSGeo4W 或 vcpkg)。
20.2.2 关键 CMake 选项
cmake/options.cmake 里的选项全部默认关闭,只有少数默认开启。下表是实际会用到的:
| 选项 | 默认 | 作用 |
|---|---|---|
WITH_TESTS |
ON | 编译单元测试(关掉能省一半构建时间) |
WITH_BACKTRACE |
ON | 崩溃时打印调用栈 |
WITH_GCS |
ON | Google Cloud Storage 支持(需要 OpenSSL) |
BUILD_TOOLS_LASDUMP |
ON | 编译 lasdump 调试工具 |
BUILD_PLUGIN_PGPOINTCLOUD |
OFF | PostGIS Pointcloud 读写 |
BUILD_PLUGIN_E57 |
OFF | E57 读写 |
BUILD_PLUGIN_TILEDB |
OFF | TileDB 读写 |
BUILD_PLUGIN_HDF |
OFF | HDF 读取 |
BUILD_PLUGIN_ARROW |
OFF | Arrow 读写 |
BUILD_PLUGIN_DRACO |
OFF | Draco 压缩格式 |
BUILD_PLUGIN_TEASER |
OFF | TEASER++ 配准 |
BUILD_PLUGIN_CPD |
OFF | Coherent Point Drift 配准 |
BUILD_PLUGIN_RIVLIB / RDBLIB / MBIO |
OFF | 专用传感器格式 |
BUILD_PLUGIN_FBX |
OFF | FBX 输出(需 Autodesk SDK) |
BUILD_PLUGIN_TRAJECTORY |
OFF | 轨迹计算(需 Ceres) |
BUILD_PLUGIN_SPZ |
OFF | 3D Gaussian Splatting 格式 |
BUILD_DOCS |
OFF | 文档构建目标 |
ENABLE_CTEST |
OFF | 提交测试结果到 CDash |
对照本机安装(OSGeo4W 的 PDAL 2.10.0)能列出 writers.e57 而 conda 版可能没有,原因就在这里:发行版编译时开的插件集合不同。判断某个 driver 是否存在,永远以 pdal --drivers 的输出为准,不要以文档为准。
20.2.3 只编译需要的插件
构建时间的一半以上花在插件上。只想读核心逻辑或写 filter 时,关掉测试和插件:
cmake .. -DCMAKE_BUILD_TYPE=Release -DWITH_TESTS=OFF -DBUILD_PLUGIN_TILEDB=OFF
需要 PGPointcloud 时再单独开:
cmake .. -DBUILD_PLUGIN_PGPOINTCLOUD=ON
插件依赖缺失时 CMake 会在配置阶段报 Could NOT find ...,但默认不中断——按照警告忽略它,结果就是编译完发现 driver 不存在。养成习惯:配置完成后检查一遍 pdal --drivers,对照预期清单。
20.2.4 安装与查找
安装后得到三样东西:pdal 可执行文件、libpdal_base 等动态库、以及 CMake 配置包(PDALConfig.cmake)。第三方项目用 find_package(PDAL 2.7 REQUIRED CONFIG) 就能链接。
排查”编出来的 pdal 和系统里的混了”这类问题时,看两个值:
pdal --version # 版本号 + git hash
pdal-config --prefix # 安装前缀(若安装了 pdal-config)
pdal --version 里的 git hash 是最可靠的判断依据——它能区分”哪个源码树编出来的”。
20.3 测试体系
20.3.1 单元测试
WITH_TESTS=ON 时用 CTest 跑:
ctest --output-on-failure # 全部
ctest -R LasReader --output-on-failure # 按名称过滤
ctest -j8 # 并行
测试框架是 GoogleTest(vendor/gtest),主入口在 pdal/pdal_test_main.cpp。测试代码分布在各模块的 test/ 子目录,命名一般是 <Stage>Test.cpp。
20.3.2 数据与基准
test/data/ 里是各种格式的小样本文件,测试用例直接引用它们。这类”输入文件 + 期望值”的测试是 PDAL 的主要回归手段:
- 算法类测试:固定输入 → 断言输出维度的统计值或点数量;
- 读写类测试:往返(read → write → read)后断言元数据与点数一致;
- pipeline 测试:
BUILD_PIPELINE_TESTS打开后跑完整 JSON 用例。
BUILD_PIPELINE_TESTS 默认 OFF,跑全量回归时要显式打开。
20.3.3 写测试的惯例
一个最小可用的 filter 测试包含三步:构造输入(用 BufferReader 塞点)、准备并执行、断言。第 13 章的 examples/writing-filter 就是按这个结构组织的。给新 stage 提 PR 时,配套测试与文档是合并的前提。
20.4 扩展点
PDAL 提供三类官方扩展点,都在第 13 章展开过,这里只从架构角度看它们的差异:
| 扩展点 | 产物 | 发现方式 | 适用 |
|---|---|---|---|
| 自定义 Stage | 动态库 libpdal_plugin_filter_xxx |
PDAL_DRIVER_PATH 扫描 |
新算法、新格式 |
| 自定义 Kernel | 动态库 libpdal_plugin_kernel_xxx |
同上 | 新 CLI 子命令 |
| 内建 stage | 编进核心库 | 静态注册 | 无额外依赖、值得进主干的算法 |
判断标准:如果算法只依赖核心库,且对社区有普遍价值,走内建并提 PR;如果依赖特定 SDK 或内部数据,走外部插件。plugins/ 目录里的每个子目录是一个可选插件,它们的 CMakeLists 是写外部插件最好的参考。
20.5 调试
20.5.1 日志
PDAL 的日志分级从 Error 到 Debug5,命令行用 --loglevel 控制:
pdal pipeline pipe.json --loglevel debug # 一般调试
pdal pipeline pipe.json --loglevel debug5 # 最详细,含插件加载与 SRS 解析过程
插件加载、”找不到驱动”、”SRS 解析失败”这类问题只有 debug 以上才会出现在日志里。第 13 章的”Loaded plugin”排查就是靠这个。
20.5.2 崩溃栈
WITH_BACKTRACE=ON(默认)时,未捕获异常会打印调用栈。Linux 上依赖 libunwind/libexecinfo,Windows 上走 dbghelp。编译时同时给 -DCMAKE_BUILD_TYPE=RelWithDebInfo 才能看到符号名。
20.5.3 lasdump
BUILD_TOOLS_LASDUMP=ON 时会编译 lasdump,用于逐字节查看 LAS 头与 VLR:
lasdump -h block.laz # 头信息
lasdump -v block.laz # VLR 列表
排查”别的软件不认 PDAL 写出的文件”这类问题时,第一件事就是用 lasdump 对比两边的 VLR 与 global_encoding 位。
20.6 贡献流程
20.6.1 代码风格
- C++17,缩进 4 空格,类名大驼峰、方法小驼峰;
- 每个 stage 的文件名与类名对应(
SmrfFilter在filters/SMRFilter.cpp,命名不完全一致时以目录约定为准); - 公开 API 在头文件里写 Doxygen 注释;
- 提交前跑
clang-format(仓库根有.clang-format)。
20.6.2 PR 的最小集合
一个可被合并的 stage PR 通常包含:
filters/XxxFilter.cpp+.hpp,含addArgs/addDimensions/run(或processOne);filters/CMakeLists.txt里加源文件;doc/stages/filters.xxx.md文档;test/unit/filters/XxxFilterTest.cpp测试与test/data/样本;doc/stages/filters.md索引里加链接。
缺任何一项,review 都会打回。
20.6.3 本地验证清单
cmake --build . --parallel
ctest -R Xxx --output-on-failure
pdal --options filters.xxx # 参数表是否完整
pdal --drivers | grep xxx # 是否注册成功
20.6.4 发布流程
PDAL 用语义化版本,HOWTORELEASE.md 记录了打 tag、生成文档、发布二进制包的步骤。发行版(conda-forge、OSGeo4W、vcpkg)跟进通常滞后数周,所以”我的代码合了但包里没有”是正常现象。
20.7 排错清单
| 现象 | 原因 | 修法 |
|---|---|---|
| CMake 配置成功但 driver 缺失 | 插件选项默认 OFF 或依赖没找到 | 显式 -DBUILD_PLUGIN_XXX=ON,看配置阶段的 Could NOT find |
| 编译报 C++ 标准错误 | 工具链默认 C++14 | 显式 -DCMAKE_CXX_STANDARD=17 |
| 运行时报找不到动态库 | 安装前缀不在 loader 路径 | ldconfig / 设 PATH / 设 LD_LIBRARY_PATH |
pdal --version 与预期不一致 |
系统里还有另一个安装 | 看 git hash,用绝对路径调用 |
| 测试大面积失败但代码没改 | 测试数据缺失或 GDAL 版本不匹配 | 检查 test/data/;对照 CI 的依赖版本 |
| 插件加载失败 | ABI 不匹配(编译器/标准库版本) | 用同一环境编译 PDAL 与插件 |
20.8 生产建议
自己维护的发行版要锁定插件集合。 团队成员用不同发行版会导致”你的机器有 e57、我的没有”。用统一的容器镜像或 conda 环境文件,把 BUILD_PLUGIN_* 与依赖版本一起固定。
核心库改动最小化。 所有定制走外部插件,PDAL 升级时只需重编插件与回归测试;改核心库会把每次升级变成手工合并。
CI 里跑两层测试。 单元测试(ctest)保证算法正确性,pipeline 级的定长输出对比保证组合行为不漂移。两层都过再合并。
文档与代码同 PR。 doc/stages/*.md 就是发布出去的文档,参数改了文档不更新,下一版线上文档就是错的。
20.9 本章小结
| 主题 | 结论 |
|---|---|
| 仓库结构 | 核心库在 pdal/,内建 stage 在 io/ filters/,CLI 子命令在 kernels/,可选依赖在 plugins/ |
| 依赖 | GDAL/PROJ 是硬依赖,其余全部插件化 |
| 构建 | CMake ≥3.13 + C++17;WITH_TESTS 默认 ON,BUILD_PLUGIN_* 默认 OFF |
| 测试 | GoogleTest + CTest;BUILD_PIPELINE_TESTS 默认 OFF |
| 扩展 | 外部插件(PDAL_DRIVER_PATH)或内建 stage(静态注册) |
| 调试 | --loglevel debug5 看插件/SRS,lasdump 看 LAS 字节,WITH_BACKTRACE 看崩溃栈 |
| 贡献 | 源码 + 文档 + 测试 + 索引四件套 |