Skip to content

feat(desktop): migrate packaging to Tauri 2 and Nuitka - #118

Draft
LC044 wants to merge 18 commits into
masterfrom
agent/desktop-packaging-phase-0
Draft

feat(desktop): migrate packaging to Tauri 2 and Nuitka#118
LC044 wants to merge 18 commits into
masterfrom
agent/desktop-packaging-phase-0

Conversation

@LC044

@LC044 LC044 commented Aug 9, 2026

Copy link
Copy Markdown
Owner

描述

将桌面打包链路从 Electron/electron-builder 迁移到 Tauri 2,并将 Server 与 AI Sidecar 的默认冻结工具从 PyInstaller onedir 迁移到 Nuitka standalone。

Tauri Rust 壳负责随机端口分配、FastAPI Sidecar 启动、健康检查、日志重定向和退出时进程树清理。Vue 通过 Tauri command 获取运行时 API 地址并直连本地服务。三平台 GitHub Actions 已改为安装 Rust/Tauri 系统依赖、编译 Nuitka runtime 并上传 Tauri 安装包。

本轮属于阶段 0/0.5。Electron Node 主进程中的 AI 扩展下载、离线导入和 AI Gateway 尚未迁移到 Rust,Tauri 设置页会按既有降级逻辑显示桌面扩展接口不可用;AI Nuitka 构建链路保留,待 Rust 扩展管理器接入。

变更类型

  • ✨ 新功能 (New Feature)
  • 📝 文档更新 (Documentation)
  • ♻️ 代码重构 (Refactor)
  • ⚡️ 性能优化 (Performance)
  • 🔧 构建/工具链修改 (Build/Chore)

相关 Issue

无。

如何测试

  • pnpm buildpackage/website
  • python -m py_compile scripts/build_desktop_runtime.py desktop_entry.py(Server)
  • python -m py_compile scripts/build_desktop_runtime.py scripts/build_desktop_extension.py desktop_entry.py(AI)
  • cargo checkpackage/desktop/src-tauri
  • pwsh .\tests\scripts\run-tests.ps1 -Layer e2e -Level full:276 passed,4 skipped
  • GitHub Actions 将进一步验证 Windows、macOS、Linux 原生 Nuitka/Tauri 安装包构建

检查清单

  • 我已阅读并遵循项目的贡献指南
  • 我的代码遵循项目的代码风格
  • 所有测试均已通过
  • 我已更新了相关文档

I have read and agree to the CLA

@LC044

LC044 commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

多平台安装包 CI 验证通过:

  • Windows installer:通过,Artifact TrailSnap-windows-2(213,460,319 bytes)
  • macOS installer:通过,Artifact TrailSnap-macos-2(235,161,898 bytes)
  • Linux installer:通过,Artifact TrailSnap-linux-2(567,790,113 bytes,包含 AppImage 与 DEB)
  • Workflow run: https://github.com/LC044/TrailSnap/actions/runs/31313793555

本地完整 E2E:274 passed,4 skipped。

I have read and agree to the CLA

@LC044 LC044 changed the title feat(desktop): add phase 0 cross-platform packaging feat(desktop): migrate packaging to Tauri 2 and Nuitka Aug 10, 2026
LC044 and others added 12 commits August 10, 2026 10:27
The desktop build workflow ran `pnpm --dir package/desktop build -- --bundles <b>`.
pnpm 10 forwards the `--` separator verbatim to the script, producing
`tauri build -- --bundles <b>`. That `--` tells Tauri to pass the remaining
args down to `cargo build`, so cargo received `--bundles` and rejected it
(suggesting `--benches`). The build failed on Windows, macOS, and Linux
within seconds of reaching the Tauri step, after the Nuitka sidecar had
already compiled successfully.

Invoke `tauri` directly through `pnpm exec` so `--bundles` is consumed by
the Tauri CLI instead of forwarded to cargo. Verified locally that
`pnpm --dir package/desktop exec tauri build --help` forwards args to tauri
and that `-b, --bundles` is the correct Tauri 2 flag.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nstaller

Nuitka's Scons C compilation made the desktop CI builds take ~1h on each
of Windows/macOS/Linux, with no measurable cold-start benefit for the
desktop targets. Switch both sidecars back to PyInstaller onedir, which
the codebase was originally designed for: app/core/paths.py anchors
read-only resources via sys._MEIPASS, the path PyInstaller sets in
onedir mode (Nuitka never set it, so it only worked via the SERVER_ROOT
fallback).

Changes:
- package/server/scripts/build_desktop_runtime.py: invoke
  `python -m PyInstaller --noconfirm --clean desktop_server.spec`
  instead of Nuitka; same dist/trailsnap-server output contract.
- package/ai/scripts/build_desktop_runtime.py: same, using
  desktop_ai.spec -> dist/trailsnap-ai.
- package/server/pyproject.toml + package/ai/pyproject.toml: drop
  nuitka>=4.0 from the dev/desktop groups; regenerate both uv.lock
  (only nuitka removed, no other version changes; uv lock --check OK).
- package/ai/scripts/build_desktop_extension.py: genericize the
  'Nuitka runtime not found' message to 'AI runtime not found'.
- doc/desktop_packaging_technical_route.md + package/desktop/README.md:
  document PyInstaller onedir as the default route; Nuitka kept only as
  a evaluated-and-not-adopted comparison.

Verified locally: server sidecar builds via PyInstaller, produces
dist/trailsnap-server with resources under _internal/resources (where
_MEIPASS resolves), and trailsnap-server.exe --help runs (exit 0).
Unit smoke (201 tests) passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Linux installer job failed at the Tauri AppImage bundling step with
`failed to run linuxdeploy`. linuxdeploy and linuxdeploy-plugin-appimage
are themselves AppImages and need libfuse2 to execute; ubuntu-latest
(24.04) no longer ships libfuse2 preinstalled. Add it to the Linux
system-dependency step.

Unrelated to the Nuitka->PyInstaller switch: the prior Nuitka run never
reached AppImage bundling (it was blocked by the --bundles forwarding
bug), so this is the first time the Linux AppImage path ran end-to-end.
macOS and Windows both succeeded in the same run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Installing libfuse2 was not sufficient: the AppImage-based linuxdeploy /
linuxdeploy-plugin-appimage tools still fail to run on ubuntu-latest
because the runner has no usable /dev/fuse device, even with the userspace
library present. Tauri only surfaces this as 'failed to run linuxdeploy'.

Set APPIMAGE_EXTRACT_AND_RUN=1 (Linux only) so those AppImage tools
extract themselves to a temp directory and execute from there instead of
mounting via FUSE. Windows and macOS are unaffected (env var is empty and
unused there).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…stderr

APPIMAGE_EXTRACT_AND_RUN=1 did not fix the Linux AppImage failure.
Tauri still reports only 'failed to run linuxdeploy' and hides the tool's
stderr. Add TAURI_BUNDLER_VERBOSE=1, LINUXDEPLOY_DEBUG=1 (Linux) and
--verbose so the next run prints linuxdeploy's actual error output, which
is needed to identify the real root cause. Diagnostic-only; will be
reverted once the cause is known.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ibgfortran

The Linux AppImage build failed at the linuxdeploy dependency-walking step
with (revealed via TAURI_BUNDLER_VERBOSE):

  Deploying dependencies for ELF file .../server/_dfitpack...so
  ERROR: Could not find dependency: libgfortran-040039e1.so.5.0.0
  ERROR: Failed to deploy dependencies for existing files
  failed to run linuxdeploy

scipy/numpy/scikit-learn ship native Fortran/BLAS libraries (libgfortran,
OpenBLAS) inside versioned <pkg>.libs directories. PyInstaller's default
hooks did not bundle them, so the frozen sidecar referenced a .so that was
absent from the bundle, and linuxdeploy could not resolve it while
assembling the AppImage. Use collect_all for numpy/scipy/sklearn to pull
their submodules, data files, AND .libs binaries into the bundle.

Also drop the temporary TAURI_BUNDLER_VERBOSE / LINUXDEPLOY_DEBUG / --verbose
diagnostics now that the root cause is identified; keep APPIMAGE_EXTRACT_AND_RUN
which is still needed for linuxdeploy to run on the FUSE-less runner.

Verified locally: spec builds and trailsnap-server.exe --help runs (exit 0).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…erbose

collect_all(numpy/scipy/sklearn) did not fix the AppImage build. Add a
Linux-only diagnostic step that, after staging the sidecar, prints:
- libgfortran files present
- *.libs directories present
- _dfitpack NEEDED libs (objdump) and unresolved libs (ldd)

Restore TAURI_BUNDLER_VERBOSE / --verbose / LINUXDEPLOY_DEBUG as permanent
(noting Tauri hides linuxdeploy stderr by default, which made prior fixes
blind). Goal: determine whether scipy.libs made it into the bundle and what
linuxdeploy now reports. Diagnostic-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The map-syntax resource glob {"../server-dist/trailsnap-server/**/*":
"server/"} flattened the PyInstaller onedir tree to server/<basename>
(Tauri's ResourcePaths Glob branch in crates/tauri-utils/src/resources.rs
joins dest with path.file_name(), keeping only the basename). This broke
the \$ORIGIN/../../scipy.libs/ RUNPATH that scipy's _dfitpack.so uses to
locate libgfortran-<hash>.so, so linuxdeploy aborted the AppImage build
with "Could not find dependency: libgfortran-040039e1.so.5.0.0". It would
also have broken sidecar startup on every platform, since the PyInstaller
bootloader needs _internal/ next to the executable.

Point the resource entry at the directory directly so Tauri walks it
(Walk branch preserves structure via dest.join(strip_prefix(pattern)))
instead of globbing it flat. Also expose every *.libs/ dir via
LD_LIBRARY_PATH on Linux as a fallback resolver for linuxdeploy in case
its \$ORIGIN expansion is shaky.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The previous fix (preserve sidecar dir structure via the directory-form
resource entry) got libgfortran past linuxdeploy, and Windows/macOS
packaging stayed green. But the Linux AppImage build now aborts one lib
further: "Could not find dependency: libmvec-2-06864e43.28.so", a NEEDED
of pillow_heif's libx265-d26213b5.so.215.

The LD_LIBRARY_PATH fallback only covered *.libs/ subdirs, but
PyInstaller flattens many hash-suffixed vendored libs (libgfortran,
libmvec, libx265, libscipy_openblas64_, ...) to the _internal/ top
level — out of reach of both \$ORIGIN (libx265 sits in
pillow_heif.libs/, so its \$ORIGIN points the wrong way) and the
.libs-only LD_LIBRARY_PATH. Extend the fallback to every directory under
_internal/ so linuxdeploy's ldd resolver finds vendored libs regardless
of where PyInstaller placed them.

Also broaden the Linux diagnose step to locate libmvec / libx265 and
list the top-level flattened hash libs, so a genuinely missing lib is
obvious next time instead of a bare "Could not find dependency".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ploy

linuxdeploy resolves ELF deps via `ldd` on the canonical path
(src/core/elf_file.cpp), and ldd expands \$ORIGIN-relative RPATHs.
libx265-d26213b5.so.215 in pillow_heif.libs/ NEEDs libmvec-<hash>.so
which sits in the same dir, yet ldd reports "=> not found" and linuxdeploy
aborts — meaning libx265's RPATH lost its \$ORIGIN during PyInstaller
relocate / UPX (upx=True in desktop_server.spec).

The LD_LIBRARY_PATH fallback added last attempt is useless: linuxdeploy
is an AppImage run via --appimage-extract-and-run, whose runtime overrides
LD_LIBRARY_PATH to its own extracted lib dir, and src/main.cpp confirms
linuxdeploy reads no EXTRA_LD_LIBRARY_PATH (only the --exclude-library CLI
flag, which Tauri does not expose). The only lever ldd honors is the
ELF's own RPATH.

So prefix \$ORIGIN (preserving any existing RPATH) onto every .so inside
*.libs/ dirs before Tauri copies the sidecar into the AppDir. Also broaden
the Linux diagnose step to print libx265's file type / RPATH / ldd output
so a future missing-lib is diagnosed in one pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant