Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

Build from source

ScaiFlux ships as a single PyInstaller-bundled executable so end users don't need Python, pip, or a virtualenv on their machine — just a compatible libc and they can run ./scaiflux. This document describes the toolchain that builds that binary from source.

For end users who do have Python, prefer pip install / pipx install of the wheel — it's smaller, faster to install, and easier to upgrade. The standalone binary is for the no-Python case.

TL;DR#

bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# One-time prereq (Debian/Ubuntu — see "Prerequisites" below for
# other distros). Needed because PyInstaller bundles the Python
# shared library and many distros ship Python without it.
sudo apt install libpython3.13

# Build:
./scripts/build_dist.sh

# Output:
ls dist/
#   scaiflux                            ← raw build output
#   scaiflux-0.1.0-linux-x86_64         ← versioned + platform-tagged copy
#   scaiflux-0.1.0-linux-x86_64.sha256  ← integrity check

# Use:
./dist/scaiflux --version
./dist/scaiflux repl

Prerequisites#

  • Python 3.11+ on the build host.

  • The Python shared library (libpython3.X.so.1.0). Distros vary:

    Distro Install command
    Debian / Ubuntu sudo apt install libpython3.13 (substitute your version)
    Fedora / RHEL sudo dnf install python3-libs
    Arch included by default in the python package
    macOS use the python.org installer, or pyenv install <ver> with PYTHON_CONFIGURE_OPTS="--enable-shared"

    The build script detects when this is missing and prints the same hint instead of letting PyInstaller fail 80 seconds in.

  • curl and tar (for one-time patchelf download — see "Portability via staticx" below). Both are present on virtually every Linux install.

  • No other prereqs. The script creates an isolated build venv at .build-venv/, installs ScaiFlux + PyInstaller + staticx into it, caches a static patchelf binary at .build-tools/, and never touches the user's main environment. No sudo, no Docker.

What the build script does#

scripts/build_dist.sh:

  1. Optionally cleans build/, dist/, .build-venv/, .build-tools/ (--clean).
  2. Creates an isolated build venv (skip with --no-venv).
  3. pip installs the project (minus dev extras), PyInstaller, and staticx into the venv.
  4. Checks for the Python shared library and aborts with a clear hint if missing.
  5. One-time: downloads patchelf 0.18+ to .build-tools/ (needed by staticx; the pip-packaged patchelf ships 0.17 which has a bug that fires on modern-glibc PyInstaller binaries).
  6. Runs pyinstaller --clean --noconfirm packaging/scaiflux.spec.
  7. Runs staticx on the PyInstaller output to bundle every shared library dependency (libpython, libc, libm, …) into the binary itself — eliminates glibc-version sensitivity. See Portability via staticx below. Skip with --no-portable if you want a dynamic-only binary for some reason.
  8. Smoke-tests the resulting binary:
    bash
    1
    2
    3
    ./dist/scaiflux --version
    ./dist/scaiflux --help
    ./dist/scaiflux {setup,serve,doctor} --help
    
  9. Stamps a versioned + platform-tagged copy, gzips it, and writes a .sha256 next to both raw and gzipped artifacts.

A typical successful build takes 60–120 seconds on a warm cache, 4–6 minutes from cold. The resulting binary lives in dist/scaiflux-<version>-<os>-<arch> and is in the 25–30 MB range (stripped + statically linked).

Portability via staticx#

Without staticx, a PyInstaller binary built on Debian 13 (glibc 2.41) fails on Debian 12 (glibc 2.36) with:

text
1
2
Failed to load Python shared library '/tmp/_MEI.../libpython3.13.so.1.0':
/lib/x86_64-linux-gnu/libm.so.6: version 'GLIBC_2.38' not found

Symbol versions in glibc go forward, not backward — a binary built against newer glibc can't load on older glibc.

staticx solves this by running over the finished PyInstaller binary and:

  • Reading its dynamic dependencies via ldd.
  • Bundling every .so (including libpython, libm, libc, libpthread, etc.) into a custom archive appended to the executable.
  • Patching the ELF (via patchelf) so the binary uses an embedded loader from staticx's own runtime instead of the system's /lib64/ld-linux-x86-64.so.2.

The result is verified via ldd dist/scaiflux reporting not a dynamic executable. The binary runs on any Linux x86_64 regardless of distro vintage.

Cost: about 1 MB of binary growth and a few seconds of build time.

If for some reason you want the dynamic build (smaller, but host-glibc-locked):

bash
1
./scripts/build_dist.sh --no-portable

The raw PyInstaller output is preserved as dist/scaiflux.dynamic regardless of --no-portable, so you can always compare.

What's in the spec file#

packaging/scaiflux.spec deserves a quick tour because PyInstaller isn't drop-in for projects that use dynamic imports.

  • Entry script: packaging/scaiflux_entrypoint.py — a tiny shim that calls scaiflux.cli:main(). PyInstaller can't analyse a [project.scripts] entry point declaration, so we hand it a real .py file.

  • hiddenimports: ScaiFlux loads tools, slash commands, and CLI subcommands by name. PyInstaller's static analyser can't follow those, so each module is enumerated explicitly. The spec uses collect_submodules() for big trees (api, auth, runtime, telemetry, plugins, agents, skills) and a hand list for tools / commands / subcommands (small enough to keep visible; safer than catching too much).

  • datas: bundles the two non-Python resources ScaiFlux reads at runtime — scaiflux/schemas/scaiflux.schema.json (the .scaiflux.json JSON-schema) and scaiflux/cli/working_verbs.txt (the indicator's verb pool). Both loaders resolve adjacent to __file__, which PyInstaller arranges correctly inside the bundle.

  • collect_all for pydantic_core, h2, hpack, hyperframe, httpcore — these have C extensions or non-Python data files that PyInstaller needs help finding.

  • excludes: mcp, fastapi, uvicorn, pytest, etc. — optional / dev-only deps that would otherwise bloat the binary meaningfully. Users who need the MCP integration can install the wheel instead; the standalone binary's tool catalog is the built-in surface only.

  • onefile=True: produces a single executable rather than a directory. Costs a few hundred ms of self-extraction on first run per process; almost always worth it for distribution simplicity.

  • upx=False: UPX shaves ~30 % off the binary size at the cost of measurably slower startup. For an interactive CLI we'd rather pay the disk than the perceived latency.

Limits of the bundled binary#

The frozen binary is not a one-to-one substitute for pip install scaiflux in every dimension:

  • No entry-point plugin discovery. Plugins registered via [project.entry-points."scaiflux.tools"] (or commands / plugins) on the target machine can't be picked up — there's no pip install path inside the binary. The frozen surface is whatever was compiled into scaiflux.spec. Plugin authors should ship the wheel instead.

  • No MCP client. mcp is excluded to keep the binary slim; if you need MCP, install the wheel with pip install scaiflux[mcp].

  • OS / arch lock-in. A Linux x86_64 binary won't run on ARM64, macOS, or Windows. Build per-arch / per-OS. (Within Linux x86_64 the staticx wrapping above makes the build host's glibc irrelevant.)

  • First-run extraction. PyInstaller's one-file mode extracts the bundle to $TMPDIR on first invocation, which adds 200–500 ms to startup. Subsequent invocations reuse the extraction.

Re-building after source changes#

The build script doesn't watch for changes — re-run it after edits. Use --clean to wipe caches if PyInstaller misbehaves; otherwise incremental builds are fine and meaningfully faster (~30 s vs 90 s).

CI considerations#

To build in CI:

yaml
1
2
3
4
5
6
7
# Example for GitHub Actions / Linux x86_64
- run: sudo apt-get update && sudo apt-get install -y libpython3.13
- run: ./scripts/build_dist.sh
- uses: actions/upload-artifact@v4
  with:
    name: scaiflux-${{ runner.os }}-${{ runner.arch }}
    path: dist/scaiflux-*

For cross-OS distribution, run the script in a matrix job — one per target OS. PyInstaller cannot cross-compile; macOS binaries must be built on macOS, Windows on Windows, etc. Note: staticx is Linux-only — the macOS / Windows paths produce a regular dynamic binary and may need their own portability story (macOS binaries are generally less version-sensitive thanks to system-wide framework loading; Windows binaries usually need MSVC redistributables on the target).

Updated 2026-05-18 14:34:13 View source (.md) rev 3