Packaging & Distribution

14.1 Build from source (all platforms)

Use this checklist when you want to compile and share binaries with an alpha tester.

Prerequisites: Set up your development environment first (see §2 Development Environment Setup).

  1. Install Rust (rustup) and ensure the active toolchain uses edition 2021 (or later for edition 2024 support).

  2. Set up dependencies (choose one method from §2):

    • Conda (recommended): ./scripts/setup-conda-env.sh, then conda activate orbitron-dev (see §2.3)

    • System packages: Follow platform-specific instructions in §2.2

    • Docker:

      docker-compose -f docker-compose.dev.yml build  # Build image if needed
      docker-compose -f docker-compose.dev.yml run --rm dev cargo build --release

      The container has neither node nor trunk, so the viewer assets in step 3 must be staged on the host (see §2.1).

  3. Stage the WASM viewer assets. ui/shell embeds three generated, gitignored files with include_bytes!, so any release build of the GUI fails without this:

    (cd viewer/wasm/ts && npm ci)
    extensions/python-bridge/scripts/stage_viewer_assets.sh
  4. Build binaries:

    # Build all binaries at once
    cargo build --release --workspace
    
    # Or build individually:
    cargo build --release -p orbitron-ui-shell
    cargo build --release -p orbitron-cli --bin orbitron
    cargo build --release -p orbitron-tui --bin orbitron-tui
  5. Artifacts land in target/release/:

    • GUI: target/release/orbitron-viewer
    • CLI: target/release/orbitron
    • TUI: target/release/orbitron-tui

Note: Linux and macOS builds use the HDF5 sources bundled by hdf5-metno; CMake is required, but no external HDF5 installation or HDF5_DIR is needed. Windows builds use the HDF5 package from the conda environment and set HDF5_DIR to its Library directory.

14.2 macOS desktop bundle

Run this after the build-from-source checklist to produce a signed .app (and optional .dmg).

  1. Install the bundler tooling once:

    cargo install cargo-bundle
  2. Ensure the GUI crate exposes a binary target (ui/shell/src/bin/orbitron-viewer.rs) that launches start_ui.

  3. Confirm bundle metadata in ui/shell/Cargo.toml under [package.metadata.bundle].

  4. Get your signing identity:

    security find-identity -v -p codesigning
  5. Build the .app (and optional .dmg):

    cargo bundle --release -p orbitron-ui-shell --bin orbitron-viewer
    
    cargo bundle --release -p orbitron-ui-shell --bin orbitron-viewer --format dmg

    Output lands in target/release/bundle/osx/.

  6. Sign, notarize, and staple (recommended sequence). Use the bundle name reported by cargo bundle (Orbitron Viewer.app, set via [package.metadata.bundle.bin.orbitron-viewer]):

    # ensure Resources exists (can be empty)
    mkdir -p "target/release/bundle/osx/Orbitron Viewer.app/Contents/Resources"
    
    # sign the app bundle
    codesign --force --deep --options runtime --timestamp \
      --sign "Developer ID Application: YOUR NAME (TEAMID)" \
      "target/release/bundle/osx/Orbitron Viewer.app"
    
    # verify signature
    codesign --verify --deep --strict --verbose=2 \
      "target/release/bundle/osx/Orbitron Viewer.app"
    
    # create a zip for notarization
    ditto -c -k --keepParent \
      "target/release/bundle/osx/Orbitron Viewer.app" \
      orbitron-ui-shell.zip
    
    # submit for notarization (use a keychain profile)
    xcrun notarytool submit orbitron-ui-shell.zip \
      --keychain-profile "notary-orbitron" \
      --wait
    
    # staple the notarization ticket
    xcrun stapler staple "target/release/bundle/osx/Orbitron Viewer.app"
    
    # verify Gatekeeper acceptance
    spctl -a -vv "target/release/bundle/osx/Orbitron Viewer.app"

    If you see source=Unnotarized Developer ID, the app is signed correctly but has not been notarized/stapled yet. Ensure the notarytool submit ... --wait and stapler staple steps succeed, then re-run spctl. You can also check ticket state with:

    xcrun stapler validate "target/release/bundle/osx/Orbitron Viewer.app"

    For a single-command local workflow, use:

    scripts/macos-notarize-app.sh \
      --identity "Developer ID Application: YOUR NAME (TEAMID)" \
      --profile "notary-orbitron" \
      --app "target/release/bundle/osx/Orbitron Viewer.app"
  7. No runtime dylibs need bundling on macOS. All three binaries link only system frameworks — HDF5 is built from source and linked statically, and the ssh2 crate left with the SFTP removal, taking libssh2, libcurl and the Kerberos chain with it. Check before assuming otherwise:

    otool -L target/release/orbitron-viewer | grep -v '/System/\|/usr/lib/'

    That prints only the binary’s own path today. scripts/build-macos-release.sh therefore ships no lib/ directory beside the binaries.

    If a future dependency does introduce one, the fix is to copy the exact versioned dylib from $CONDA_PREFIX/lib into Contents/Frameworks, add an rpath, and re-sign:

    APP="target/release/bundle/osx/Orbitron Viewer.app"
    mkdir -p "$APP/Contents/Frameworks"
    cp "$CONDA_PREFIX/lib/<name>.dylib" "$APP/Contents/Frameworks/"
    
    install_name_tool -add_rpath "@executable_path/../Frameworks" \
      "$APP/Contents/MacOS/orbitron-viewer"
    
    codesign --force --deep --options runtime --timestamp \
      --sign "Developer ID Application: YOUR NAME (TEAMID)" \
      "$APP"
  8. Create the keychain profile once:

    xcrun notarytool store-credentials "notary-orbitron" \
      --apple-id "you@example.com" \
      --team-id "TEAMID" \
      --password "APP_SPECIFIC_PASSWORD"

14.3 Windows installer

  • Install WiX tooling and the Cargo helper:

    cargo install cargo-wix
  • Generate an initial installer manifest (runs once to scaffold wix/main.wxs):

    cargo wix init --bin orbitron-viewer --output wix
  • Build the .msi:

    cargo wix --nocapture --release --bin orbitron-viewer

    The resulting installer lands in target/wix/. Sign with signtool.exe before shipping.

14.4 Linux packages

  • Deb:

    cargo install cargo-deb
    cargo deb --package orbitron-ui-shell --no-build
  • RPM (optional):

    cargo install cargo-rpm
    cargo rpm build --release

    Update Cargo.toml metadata (maintainer, license, post-install scripts) as required by the distribution.

14.5 Precompiled archive binaries

  • Build the desired targets with cargo build --release --target <triple>.

  • Package the artifacts alongside README, LICENSE, and sample config:

    tar czf orbitron-viewer-${VERSION}-${TARGET}.tar.gz \
        -C target/${TARGET}/release orbitron-viewer orbitron README.md LICENSE

    Use .zip archives on Windows for ease of extraction.

  • Publish checksum files (e.g., shasum -a 256 <file>).

Automate the above via CI (GitHub Actions/Gitea runners) using a matrix over macos-latest, windows-latest, and ubuntu-latest. Reuse the same pipelines to upload artifacts to the chosen release host.

14.6 Python Wheels

Python wheels provide the Orbitron API for use in Jupyter notebooks and Python scripts.

Build Script

./scripts/build-python-wheels.sh [OPTIONS]

Options: - --skip-widget - Skip Jupyter widget frontend build

That is the only flag the script accepts; anything else prints the usage line and exits 1. The build is always maturin build --release — there is no debug mode.

Output

The local script builds one wheel for the current platform. A tagged release aggregates the three platform jobs into this layout:

dist/python-wheels/
├── orbitron-X.Y.Z-cp310-abi3-manylinux_2_28_x86_64.whl  # Linux
├── orbitron-X.Y.Z-cp310-abi3-macosx_11_0_arm64.whl      # macOS Apple Silicon
├── orbitron-X.Y.Z-cp310-abi3-win_amd64.whl              # Windows
└── README.txt  # Installation instructions
# abi3: one wheel per platform runs on CPython 3.10+. Intel macOS isn't built.

Manual Build

# From the repository root. The PEP 517 backend stages both viewer frontends.
conda run --no-capture-output -n orbitron-dev \
  python -m pip wheel extensions/python-bridge --no-build-isolation \
  --wheel-dir ~/scratch/orbitron-wheels

Platform-Specific Builds

macOS: The release job builds the supported Apple Silicon wheel on its macOS arm64 runner. Orbitron does not currently publish an Intel or universal2 wheel.

Linux (manylinux):

# The tagged-release workflow runs this through PyO3/maturin-action in a
# manylinux_2_28 container. Reproduce it in that container rather than labeling
# a wheel built against the host glibc as manylinux.
maturin build --release --features python --manylinux 2_28

Windows:

# Build on Windows machine or CI
maturin build --release --features python

Testing Python Wheels

./scripts/test-python-wheel.sh [path/to/wheel.whl]

# Or test latest wheel
./scripts/test-python-wheel.sh

Tests performed: 1. Create clean virtual environment 2. Install wheel 3. Run 6 smoke tests: - Import orbitron module - Create Orbitron instance - Load molecule - Measure distance - Serialize to bytes - Access properties

Troubleshooting Python Wheels

Error: “maturin not found”

./scripts/setup-conda-env.sh
conda run -n orbitron-dev maturin --version

Error: “Could not find HDF5 headers”

# Ensure conda environment is active
conda activate orbitron-dev

# Verify HDF5
conda list | grep hdf5

Error: “Rust compiler version mismatch”

# Project uses Rust 1.92.0 (pinned in rust-toolchain.toml)
rustup show  # Should show 1.92.0

# If wrong version
rustup update
cd /path/to/orbitron  # Let rust-toolchain.toml activate

Wheel platform mismatch:

# Check the wheel tag, then build on the matching platform.
ls ~/scratch/orbitron-wheels/

14.7 WASM Web Viewer

The WASM web viewer provides an interactive 3D molecular viewer for embedding in websites.

Prerequisites

./scripts/setup-conda-env.sh
(cd viewer/wasm/ts && npm ci)
conda run -n orbitron-dev trunk --version

The conda environment pins Trunk 0.21.14 and installs the wasm32-unknown-unknown target through the repository setup.

Build Script

conda run --no-capture-output -n orbitron-dev \
  extensions/python-bridge/scripts/stage_viewer_assets.sh

This builds the Rust WASM module and TypeScript custom element, then stages the runtime used by Python, Jupyter, and desktop Web View export. Set ORBITRON_SKIP_VIEWER_BUILD=1 only when the current build products already exist and the purpose is to restage them.

Output

The build products are viewer/wasm/orbitron-viewer.js and viewer/wasm/dist/orbitron-viewer-wasm{,_bg.wasm}. The staged package copy is under extensions/python-bridge/python/orbitron/viewer_assets/.

The documentation site has a separate tracked deployment copy under website/viewer/. After rebuilding the runtime, update all three files and render the site:

cp viewer/wasm/orbitron-viewer.js website/viewer/orbitron-viewer.js
cp viewer/wasm/dist/orbitron-viewer-wasm.js \
  website/viewer/orbitron-viewer-wasm.js
cp viewer/wasm/dist/orbitron-viewer-wasm_bg.wasm \
  website/viewer/orbitron-viewer-wasm_bg.wasm
conda run --no-capture-output -n orbitron-dev quarto render website

Manual Build

conda run --no-capture-output -n orbitron-dev \
  extensions/python-bridge/scripts/stage_viewer_assets.sh
ls -lh viewer/wasm/orbitron-viewer.js viewer/wasm/dist/

Customization

Edit viewer/wasm/Trunk.toml to customize: - Output directory - Asset paths - Build hooks - Optimization level

Testing WASM Viewer

ORBITRON_SKIP_VIEWER_BUILD=1 \
  conda run --no-capture-output -n orbitron-dev ./scripts/dev-smoke.sh
env -u DYLD_LIBRARY_PATH \
  conda run --no-capture-output -n orbitron-dev \
  python scripts/dev-smoke-browser.py

The first gate checks staging, serving, and self-contained HTML invariants. The second runs 11 Chromium scenarios with pixel-level paint assertions, scene ordering checks, multiviewer isolation, packed trajectory playback, and MRC density rendering. On macOS, unsetting an inherited DYLD_LIBRARY_PATH avoids injecting conda libraries into Chromium.

Troubleshooting WASM Viewer

Error: “trunk not found”

./scripts/setup-conda-env.sh
conda run -n orbitron-dev trunk --version

Error: “wasm32-unknown-unknown not installed”

rustup target add wasm32-unknown-unknown

Blank viewer in browser: - Check browser console (F12) for errors - Check CORS: Serve via HTTP server, not file:// protocol - A null navigator.gpu is not the cause on its own. The renderer feature-detects and pins a single backend per canvas — BROWSER_WEBGPU when a usable WebGPU adapter is found, WebGL2 (Backends::GL) otherwise (core/render/src/renderer/init/mod.rs) — so a browser without WebGPU still renders.

Large .wasm file size: - Ensure using --release flag (optimized build) - Check Cargo.toml for optimization settings - Measure the artifact rather than enforcing an old fixed size. On 2026-08-27, the release WASM was 6,134,411 bytes and the complete self-contained benzene smoke page was 8.08 MB.

14.8 Standalone HTML Viewer

Use the production orbitron.export_html path. It embeds the current custom element, WASM runtime, and one scene or bundle into a single file with no CDN or local-server dependency:

conda run --no-capture-output -n orbitron-dev \
  python -m pip install -e extensions/python-bridge

conda run -n orbitron-dev python -c '
from pathlib import Path
import orbitron

source = "io/pipelines/tests/fixtures/xyz/benzene.xyz"
destination = Path.home() / "scratch" / "orbitron-benzene.html"
scene = orbitron.Orbitron().load(source)
orbitron.export_html(scene, destination)
print(destination)
'

Pass a .orbpack path instead of a Scene when the output must retain trajectory frames, normal modes, volumetric grids, or recorded presentation state. The output size follows the runtime plus the encoded scene; do not pin a 5 MB expectation. The 2026-08-27 smoke artifact was 8.08 MB.

The older scripts/build-wasm-viewer.sh and scripts/build-wasm-standalone.sh wrappers still contain pre-.orbpack instructions and a v0.4.0 fallback. They remain wired into the tagged-release workflow and must be replaced before the next release rather than used as the developer reference. This is tracked in notes/open-items.md.

14.9 Complete Release Build

Tagged releases are assembled by .github/workflows/release.yml. Its native matrix builds CLI, TUI, desktop, and platform wheels; the distribution job builds the manylinux wheel and web artifacts; the final job checks expected files and creates the GitHub release.

For a local candidate, build only the platform you are on with the scripts in this chapter, then run the release checklist. Do not emulate the multi-platform job by relabeling local binaries. The web-distribution step currently carries the legacy-wrapper issue noted in §14.8.

14.10 Distribution Sizes

Recent sizes are evidence, not release gates. Compression, target platform, embedded viewer assets, and the chosen scene all change them.

Distribution Size Notes
Python wheel Inspect the built wheel Varies by platform and bundled native libraries.
Raw WASM runtime 6,134,411 bytes on 2026-08-27 The optimized .wasm, before ZIP or base64 overhead.
Self-contained HTML 8.08 MB on 2026-08-27 Benzene plus the inlined runtime; larger bundles increase it.
Native archives Inspect each platform artifact GUI bundles, symbols, signing, and native libraries differ by OS.

14.11 Build Times

Build time depends on CPU count, Cargo and npm cache state, target architecture, and whether HDF5 and the WASM dependency graph are cold. Record elapsed time and peak memory with the release artifact when it matters; do not use an old M1 laptop estimate as a timeout.

14.12 Release Checklist

Before releasing distributions, verify:

See RELEASE_CHECKLIST.md for complete pre-release verification.