Files
osmWorkflow/.trellis/spec/pipeline/external-tools.md

9.1 KiB
Raw Permalink Blame History

外部工具调用

适用:任何调用 QGIS / GDAL / Blender 子进程的代码。 这一层是管线里唯一能启动外部进程的地方,也是踩过坑最多的地方——下面每条约束 都对应一次实际的数据损坏或静默错误。


QGIS 工具链

可执行文件路径

一律从配置的 qgisApp 推导,不写死绝对路径、不依赖 PATH

const qgisMacOS = path.join(qgisApp, "Contents", "MacOS");
const qgisPython = path.join(qgisMacOS, "python3.12");   // build-osm2streets-qgis.js:24
const ogr2ogr    = path.join(qgisMacOS, "ogr2ogr");      // :25
const ogrinfo    = path.join(qgisMacOS, "ogrinfo");      // reimport-gpkg.js:33

启动前必须 existsSync 校验并抛出带路径的错误 build-osm2streets-qgis.js:59-63reimport-gpkg.js:37-41)。让它在第一步就失败, 而不是在 execFileSync 里抛一个没有上下文的 ENOENT。

GDAL 环境变量(必须)

function qgisEnv() {                                  // build-osm2streets-qgis.js:231
  return {
    PROJ_LIB:  path.join(qgisApp, "Contents", "Resources", "qgis", "proj"),
    GDAL_DATA: path.join(qgisApp, "Contents", "Resources", "qgis", "gdal"),
  };
}

reimport-gpkg.js:132 有一份等价实现(gdalEnv())。每次 execFileSync 都要带上 写法固定为 env: { ...process.env, ...qgisEnv() }

漏掉不会立刻崩——GDAL 会退回内置的残缺数据,坐标系解析结果静默出错

在 QGIS 的 Python 里跑脚本

normalize-lane-arrows.py 依赖 osgeo.ogr,只能用 QGIS 自带的解释器。除了 qgisEnv() 还要额外注入三项(build-osm2streets-qgis.js:1287-1305

变量 作用
QT_QPA_PLATFORM offscreen 无头环境下不尝试连显示服务
PYTHONHOME <qgisApp>/Contents/Frameworks 指向 QGIS 的 Python 运行时
PYTHONPATH <...>/Resources/python + /plugins 找得到 osgeo 与插件

新增 QGIS-Python 脚本时照抄这套环境,不要只带 qgisEnv()


ogr2ogr 的三个陷阱

1. 不要设 COORDINATE_PRECISION

reimport-gpkg.js:152-156 有一段专门的注释说明:

默认行为已经能完整往返双精度。显式设置反而会触发 GDAL 的精度裁剪那一遍 把在给定分辨率下collapse 的顶点丢掉——实测在 7 个 lane-arrow 多边形上丢了 28 个点。

看到有人"为了输出干净"加上这个参数,删掉它。

2. 导出失败会留下 0 字节文件

ogr2ogr 遇到不存在的图层退出码非零,但已经创建了一个空文件。直接写目标目录 就会用空文件覆盖掉好数据,而且看起来像成功。

所以 reimport 的落盘是三段式(reimport-gpkg.js:11-13, 61-91

1. 全部导出到 mkdtemp 的 staging 目录
2. 逐个 JSON.parse + 校验 type === "FeatureCollection" && Array.isArray(features)
3. 全部通过后,才逐个 copyFileSync 到 outDir

任一图层失败 → 整批不落盘。任何"从外部工具产出文件再覆盖已有数据"的新代码都照这个 模式写。

补充两点:

  • copyFileSync 不用 renamestaging 目录可能在另一个文件系统上 reimport-gpkg.js:72-73
  • finallyrmSync(stagingDir, {recursive: true, force: true}),失败路径也要清理

3. 建包与追加是两组参数

第一个图层创建 GeoPackage其余追加build-osm2streets-qgis.js:108-111

SCENE_LAYERS.forEach((layer, index) => {
  importLayer(gpkgPath, ..., layer.id, index > 0, ogrEnv);   // update = index > 0
});

importLayer:1306)在 update 为真时补 -update -overwrite。重建前先 unlinkSync 掉旧的 gpkg:104-106),不要依赖 -overwrite 清理整个文件。


前置校验的顺序

build-osm2streets-qgis.js:41-70 的开头是一段密集的校验,顺序是刻意的:

  1. 数值参数先验(Number.isFinite + 范围),错的配置立刻死
  2. 输入文件存在性
  3. 外部可执行文件存在性
  4. 全部通过后才 mkdirSync 建输出目录

原则:在做任何有副作用的事情之前,把能验的都验完。不要先建目录再发现 QGIS 装错了。

数值校验用 Number.isFinite 而不是 !isNaN——后者对 Infinity 返回 falseInfinity 是个合法的 Number() 结果。


Blender 调用

macOS Blender 4.5 的 Metal 启动兼容

1. Scope / Trigger

export_cesium.py 在 macOS 的 Blender 4.5.12 后台启动时,可能在 Python 脚本加载前的 Metal 扩展探测中崩溃;这不是场景或道路数据错误。

2. Signatures

Cesium 阶段的调用参数必须包含:

--background --factory-startup --debug-gpu-force-workarounds --python blender/export_cesium.py -- ...

3. Contracts

--debug-gpu-force-workarounds 是 Blender 的官方 CLI 参数。它只约束导出进程的 GPU 扩展探测,不改变 .blend、GeoJSON 或导出脚本的输入输出契约。

4. Validation & Error Matrix

情况 结果
缺少该参数且启动时崩在 Metal 初始化 不应归因于道路数据;补齐参数后重跑 Cesium 阶段
参数存在且 CESIUM_EXPORT_DONE / stage manifest 写出 继续 GLB digest 与预览验证

5. Good / Base / Bad Cases

  • Good: 保留 --factory-startup,并在 Cesium 导出加入 workaround。
  • Base: Blender 场景阶段未受影响时,不额外改变其启动参数。
  • Bad: 为绕过启动崩溃删除 --factory-startup,这会重新引入本机偏好和 addon 的不确定性。

6. Tests Required

  • npm run test:build-stages 断言导出参数仍包含 workaround。
  • 对目标区域运行 --stages blender,cesium,preview,并用 glb-digest.js 解析输出。

7. Wrong vs Correct

Wrong:

--background --python blender/export_cesium.py

Correct:

--background --factory-startup --debug-gpu-force-workarounds --python blender/export_cesium.py

两种调用姿势

阶段 参数 出处
blender --background --factory-startup --python generate_scene.py -- build-area.js:242-247
cesium --background --factory-startup --python export_cesium.py -- build-area.js:276-280

两个后台阶段都使用 --factory-startup:它屏蔽用户的 preferences 和 addon 保证场景生成与导出不受本机 Blender 配置影响。export 阶段仍会显式读取已经建好的 .blend

-- 之后才是脚本自己的参数Blender 不解析它们。脚本侧用 sys.argv[sys.argv.index("--") + 1:] 取。

可执行文件路径同样从配置推导: path.join(area.blenderApp, "Contents", "MacOS", "Blender")build-area.js:291)。

调用前的资源校验

ensureFile()build-area.js:295)在每个阶段开头把依赖逐个验一遍,带 label

ensureFile(blenderExecutable(area), "Blender executable");
ensureFile(area.outputs.blend, "Blend scene");          // cesium 阶段依赖上一阶段产物
ensureFile(path.join(repoRoot, "blender", "export_cesium.py"), "Cesium exporter");

阶段间依赖靠这个显式表达,不靠隐式的执行顺序--stages cesium 单跑时,缺 .blend 会得到一句人话错误而不是 Blender 的堆栈。


子进程失败处理

统一走 runCommandbuild-area.js:301-311

const result = spawnSync(command, commandArgs, { stdio: "inherit" });
if (result.error) throw result.error;
if (result.status !== 0) {
  const signal = result.signal ? ` signal=${result.signal}` : "";
  throw new Error(`Stage '${stage}' failed with status=${result.status}${signal}`);
}

三个要点:

  • stdio: "inherit":外部工具的输出直接透传,不缓冲、不吞。这条管线的调试 高度依赖 QGIS/Blender 自己打的日志
  • result.errorresult.status 分别检查前者是启动失败ENOENT 等), 后者是运行失败,混在一起会丢信息
  • 带上 signalBlender 被 OOM killer 干掉时 status 是 null只有 signal 能说明发生了什么

build-osm2streets-qgis.js / reimport-gpkg.js 内部用 execFileSync(同步、非零 自动抛),也一律 stdio: "inherit"


反模式

反模式 后果
ogr2ogrCOORDINATE_PRECISION 静默丢顶点
外部工具产物直接写目标目录 失败时用 0 字节文件覆盖好数据
execFileSync 不带 qgisEnv() 坐标系静默出错
stdio: "pipe" 或吞掉输出 失去唯一的调试信息来源
只判 status !== 0,不看 result.error / signal 启动失败和被信号杀死都变成同一句错
rename 从临时目录搬文件 跨文件系统时 EXDEV
依赖 PATH 里的 ogr2ogr 抓到系统 GDAL版本与 QGIS 不匹配
先建目录/删文件再校验参数 配置写错也会破坏已有输出

相关