第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 通常包含:

  1. filters/XxxFilter.cpp + .hpp,含 addArgs/addDimensions/run(或 processOne);
  2. filters/CMakeLists.txt 里加源文件;
  3. doc/stages/filters.xxx.md 文档;
  4. test/unit/filters/XxxFilterTest.cpp 测试与 test/data/ 样本;
  5. 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 看崩溃栈
贡献 源码 + 文档 + 测试 + 索引四件套