7.5 KiB
外部工具调用
适用:任何调用 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-63、reimport-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不用rename:staging 目录可能在另一个文件系统上 (reimport-gpkg.js:72-73) finally里rmSync(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 的开头是一段密集的校验,顺序是刻意的:
- 数值参数先验(
Number.isFinite+ 范围),错的配置立刻死 - 输入文件存在性
- 外部可执行文件存在性
- 全部通过后才
mkdirSync建输出目录
原则:在做任何有副作用的事情之前,把能验的都验完。不要先建目录再发现 QGIS 装错了。
数值校验用 Number.isFinite 而不是 !isNaN——后者对 Infinity 返回 false,
而 Infinity 是个合法的 Number() 结果。
Blender 调用
两种调用姿势
| 阶段 | 参数 | 出处 |
|---|---|---|
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 的堆栈。
子进程失败处理
统一走 runCommand(build-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.error和result.status分别检查:前者是启动失败(ENOENT 等), 后者是运行失败,混在一起会丢信息- 带上
signal:Blender 被 OOM killer 干掉时status是 null,只有signal能说明发生了什么
build-osm2streets-qgis.js / reimport-gpkg.js 内部用 execFileSync(同步、非零
自动抛),也一律 stdio: "inherit"。
反模式
| 反模式 | 后果 |
|---|---|
给 ogr2ogr 加 COORDINATE_PRECISION |
静默丢顶点 |
| 外部工具产物直接写目标目录 | 失败时用 0 字节文件覆盖好数据 |
execFileSync 不带 qgisEnv() |
坐标系静默出错 |
stdio: "pipe" 或吞掉输出 |
失去唯一的调试信息来源 |
只判 status !== 0,不看 result.error / signal |
启动失败和被信号杀死都变成同一句错 |
用 rename 从临时目录搬文件 |
跨文件系统时 EXDEV |
依赖 PATH 里的 ogr2ogr |
抓到系统 GDAL,版本与 QGIS 不匹配 |
| 先建目录/删文件再校验参数 | 配置写错也会破坏已有输出 |