Skip to content

Troubleshooting

build:backend fails with ModuleNotFoundError: No module named 'fastapi'

PyInstaller is running against the wrong Python environment — almost certainly anaconda's. Make sure backend-api/venv/ exists and was created with:

cd backend-api
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt pyinstaller

scripts/build-backend.mjs auto-prefers backend-api/venv/bin/python when present. If you see the warning "using bare pyinstaller from PATH" in the build output, the venv wasn't found.

build:backend fails with BUNDLE IMPORT SELF-TEST FAILED

The bundle built, but the packaged binary can't import a module the app needs at runtime. The failing module is named in the output:

[selftest] FAIL sklearn.mixture: ModuleNotFoundError: No module named 'sklearn.mixture'
[selftest] 13/14 imports OK

PyInstaller only bundles what its static analysis can see, so a function-local import inside a rarely-exercised endpoint gets dropped silently. Fix it by declaring the module in scripts/build-backend.mjs:

  • hiddenImports — pure-Python modules with no data files.
  • collectAll — packages with native extensions, data files, or submodules PyInstaller doesn't trace. Note that --collect-all gathers a package's own files but not its external dependencies; those need their own hiddenImports entries.

Then re-run npm run build:backend. Don't ship a bundle that fails this check — it builds and launches fine, and only dies when a user reaches the affected feature.

This check exists because that exact bug shipped: sklearn.mixture was missing from the bundle, so wood/leaf segmentation raised ModuleNotFoundError in the packaged app while dev and pytest — which run against backend-api/venv, where every module is present — passed.

build:backend fails with No such file or directory: …/venv/bin/python3

The venv has stale shebangs (usually from being copied or its parent directory being renamed). Delete and recreate:

cd backend-api && rm -rf venv && python3 -m venv venv && \
  source venv/bin/activate && pip install -r requirements.txt pyinstaller

App launches but shows red "Backend failed to start" banner

In dev:

  • If backend-api/venv exists, npm run dev runs uvicorn itself — check the [dev] lines in the terminal for a uvicorn startup failure.
  • Without a venv, the supervisor falls back to the bundled sidecar, so confirm resources/phytograph_backend/ exists (if not, run npm run build:backend).

In packaged build: open macOS Console.app or Windows Event Viewer and search for [Backend stderr]: lines from the supervisor.

Not every stderr line is an error

Python's logging writes to stderr by default, so INFO and WARNING messages land there too. Read the message before assuming a failure.

Common causes:

  • Quarantine bit on a fresh macOS install:
    xattr -dr com.apple.quarantine /Applications/Phytograph.app
    
  • A Windows application control policy refusing a bundled library — the log shows ImportError: DLL load failed while importing …: An Application Control policy has blocked this file. Restarting cannot fix it; see Native libraries are signed separately.
  • First-launch cold start (~30s with onedir, longer on slower disks)
  • An orphaned backend from a previous session (it won't block the new instance, which picks its own port, but it does consume memory):
    pkill -f phytograph_backend      # packaged bundle
    pkill -f 'uvicorn main:app'      # dev
    

"Cannot remove quarantine" on macOS

The signed app's CodeSignature seal makes xattrs immutable. Either install via Finder drag (which clears quarantine on first launch via Gatekeeper approval), or use a signed+notarized CI build where this never comes up.

Renderer can't reach the backend

The renderer does not hardcode the port. initBackendUrl() in src/renderer/utils/backendApi.ts fetches the real port from the main process over the backend.getInfo IPC before the first render, and getBackendUrl() returns that cached value. If requests are going to the wrong place, the resolution step is what to inspect — don't edit getBackendUrl(). To pin a specific port for both ends, set PHYTOGRAPH_BACKEND_PORT.

A tool fails with "… timed out. The backend did not respond within …"

Every request the renderer makes carries a client-side deadline (2 min for an export, 5 min for a session op or a plant generate, 10 min for an import). When one expires, abortOnTimeout() in src/renderer/utils/backendApi.ts aborts the fetch with a BackendTimeoutError naming the endpoint and the budget, and that message is what the toast shows.

The backend logs any request over 5 s, so start there:

[slow] POST /api/pointcloud/export took 143.2s, 2 other request(s) already in flight

Lower the threshold with PHYTOGRAPH_SLOW_REQUEST_SECONDS (in dev the line goes to the npm run dev terminal alongside uvicorn's access log). If the request isn't in the log at all it never arrived; if it's there and never finished, the operation is stuck — restart the backend.

The trailing in-flight count should be uncorrelated with slowness. It wasn't always: every handler used to be async def doing blocking work inline on the single event loop, so one long operation starved every other request behind it — a 7 M-point LAZ export (≈1–2 s of real work) died on its 2-minute deadline while 60 ms preview calls timed out at 60 s. Blocking handlers are now def and run in the worker threadpool. If you see slow requests correlating with a high in-flight count again, something has regressed — see Concurrency model.

A user cancel is deliberately different: it aborts with no reason, stays a plain AbortError, and callers swallow it silently. Don't collapse the two, or a real timeout goes back to being invisible.

No suitable compiler found (gcc or clang) when building PyHelios

Almost always means xcode-select points at a broken Xcode, not that a compiler is missing. A macOS update can leave stale Xcode support frameworks in /Library/Developer/PrivateFrameworks/, after which the active developer directory's xcrun/xcodebuild shim fails to load:

Symbol not found: _XPCTypeBool
  Referenced from: /Library/Developer/PrivateFrameworks/CoreDevice.framework
  Expected in:     /Library/Apple/System/Library/PrivateFrameworks/Mercury.framework

/usr/bin/cc, /usr/bin/clang and /usr/bin/c++ are all shims through that directory, so every one of them fails. PyHelios's build script probes gcc --version then clang --version, sees both fail, and reports the missing compiler — which is why the message is misleading.

Diagnose it in under a second:

npm run check:toolchain

Two symptoms confirm this specific failure:

  • C compiles, C++ does not. With no working shim, clang resolves no sysroot and therefore searches no SDK. The libc++ headers live only in the SDK, so the build gets as far as ~78% (freetype, glfw, glew, libjpeg are all C) and dies on the first C++ file with fatal error: 'set' file not found.
  • The Command Line Tools installer keeps reopening. When the broken shim can't resolve a tool it calls xcode-select: Failed to locate 'clang++', requesting installation of command line developer tools. Every failed compile re-triggers it.

Reinstalling the Command Line Tools cannot fix this — the pointer is wrong, not the payload. Repoint it instead:

sudo xcode-select -s /Library/Developer/CommandLineTools

The Command Line Tools alone are sufficient to build Phytograph; a working Xcode is not required. To repair Xcode itself, launch Xcode.app once so it reinstalls its components.

Builds are not blocked while the machine is in this state: scripts/build-pyhelios.mjs compiles a C++ sentinel before starting, and if the active directory is unusable but the Command Line Tools work, it builds with DEVELOPER_DIR pointed at them and prints a warning every build until you run the command above. That override is per-process and changes nothing on the system.

Don't trust pkgutil or the installer to confirm a healthy toolchain here — both reported success throughout an outage where nothing could compile C++. Only compiling proves it:

clang++ --version && echo '#include <set>' | clang++ -x c++ -fsyntax-only -

Plant generation / Helios features fail in dev only

PyHelios is built from the source submodule, not a pip wheel. Its native lib (libhelios.dylib) lives at pyhelios/pyhelios_build/build/lib/ and must be importable from the active Python env. If you replaced or recreated the venv, or the submodule isn't initialized, rebuild from source:

git submodule update --init --recursive   # if pyhelios/ is empty
source backend-api/venv/bin/activate
node scripts/build-pyhelios.mjs            # compiles libhelios + editable install

The backend also auto-rebuilds libhelios on startup when the C++ source is newer than the compiled lib, so a stale lib usually fixes itself on the next backend restart. A clean rebuild: node scripts/build-pyhelios.mjs --clean.

"Cloud session not found … Re-import the cloud and try again"

A tool (export, triangulate, LAD, spacing check) refuses to run and names a session id. The backend restarted since that cloud was imported — in dev, the usual cause is uvicorn's --reload firing on a backend-api/*.py edit, which replaces the process and drops every in-memory CloudSession. The renderer still holds the old session id, and the on-disk octree keeps rendering, so nothing looks wrong until a tool needs the actual points.

Fix: re-import the cloud.

Importing a 9th cloud is no longer a cause of this

It used to be. Only PHYTOGRAPH_MAX_CLOUD_SESSIONS (default 8) sessions are held in RAM, and an evicted one used to be gone — so on a scene of twenty scans, everything but the eight most recent answered this error. Eviction now spills the session to disk and reads it back on demand (Session eviction), so the remaining causes are a backend restart, an explicit delete, a spill that could not be written (disk full — the log says Could not spill cloud session) or read back (corrupt file; Could not restore cloud session), or one trimmed away under PHYTOGRAPH_SESSION_SPILL_MAX_BYTES. The first use of a paged-out cloud is slower by one disk read.

This is deliberately a hard error rather than a fall back to the file the cloud was imported from. Once a file is imported, Phytograph treats it as if it no longer exists. The session arrays are the complete source of truth: they carry every edit (deletions, translation, filtering, segmentation labels) and every choice made in the import wizard (column roles, dropped columns, global shift, ASCII layout). Re-reading the file would not restore the cloud — it would substitute a different one and report success, which is worse than an error.

Some source paths are not readable point clouds at all. A RIEGL project "source path" is the project directory, kept only as provenance; the older fall-through surfaced it as the misleading Scan file not found: /…/2018-02-23.002.riproject when the real problem was the dropped session.

The allow_file_source flag on PointSource / HeliosScanEntry / ScanExportEntry is the deliberate opt-in for files that are genuinely not live clouds (unit tests driving the loaders, external files). The app never sets it.

Stale backend processes

Since ports are resolved per instance, a leftover backend no longer blocks the next launch — it lands on a different free port. (The supervisor only kills a process on its own resolved port, and only when /version mismatches, so a running dev backend is never killed by a test run or a second instance.)

To clean up orphans:

pkill -f phytograph_backend      # packaged bundle
pkill -f 'uvicorn main:app'      # dev

To see what holds a specific port: lsof -ti :<port>.