Euler source The code that runs the solver shell and this site, version 4.34.2 Repository on GitHub

Shared system-venv helpers: the root-owned /opt/euler venv every euler-* service runs from.

152 lines of Bash Raw file Open on GitHub

#!/usr/bin/env bash
# Shared system-venv helpers: the root-owned /opt/euler venv every euler-* service runs from.
# ===========================================================================================
#
# Sourced by the app-service kits (auth.sh / user.sh) so the venv location, the
# installed dependency set, and the deploy/clean logic have **one** definition
# instead of drifting copies. Can also be run directly for a manual
# `venv.sh deploy|remove|redeploy|clean|status`.
#
# The venv is deliberately **not** the operator's `.venv`: the services run as locked-down
# euler-* uids that cannot read the operator's home, so the code lives in a
# root-owned tree at /opt/euler, group-readable by euler-web, and the units run
# `/opt/euler/venv/bin/python -m euler_site.<svc>` / `-m euler_webshell.<svc>`.
#
# Author: Vikas Munshi <vikas.munshi@gmail.com>
# Copyright (c) 2026. All rights reserved.
# Licensed under the MIT License.

# The system venv: root-owned, at /opt/euler; the services run its python.
OPT_DIR="/opt/euler"
VENV_DIR="${OPT_DIR}/venv"
VENV_PY="${VENV_DIR}/bin/python"
PYTHON="${PYTHON:-python3.14}"          # the project floor; deadsnakes on 24.04

# Dependency groups installed into the system venv. Beyond `web` (the app services
# themselves), a web shell is the full solver: `ai` so a maintainer's `claude-*`
# work once the Phase-7 broker supplies the key (the anthropic SDK must be present);
# `solutions` so `eval`/`benchmark` can build the sieves/tables real solutions need;
# `dev` because `new`/`lint` shell out to black/isort/autoflake/flake8. `show`
# (matplotlib/PyQt5) is **excluded** — GUI plotting has no place on a headless server.
VENV_EXTRAS="ai,dev,solutions"          # the solver package's extras; site + webshell have none

# Build/refresh the root-owned system venv and (re)install the repo into it so the
# DEPLOYED package always matches the current source (the units run `-m euler_site.*`
# with cwd=/, so a stale venv fails even when the repo has the module). Idempotent —
# every kit calls it, and installing again is a cheap refresh.
#   $1 = the repo root to install from   $2 = the group that owns the venv (euler-web)
deploy_venv() {
    local project_root="$1" web_group="$2"
    if [ ! -x "${VENV_PY}" ]; then
        echo "Creating system venv at ${VENV_DIR} (${PYTHON})..."
        sudo mkdir -p "${OPT_DIR}"
        sudo "${PYTHON}" -m venv "${VENV_DIR}"
    fi
    echo "Installing euler-solver[${VENV_EXTRAS}], euler-site, euler-webshell into ${VENV_DIR} (as root, from ${project_root})..."
    sudo "${VENV_PY}" -m pip install --quiet --upgrade pip
    # Each version is read straight from the package's tracked version.py at build time
    # (setuptools `attr:`, see [tool.setuptools.dynamic] in each pyproject.toml) — no git,
    # so this root build against the operator-owned checkout produces the SAME number
    # a developer's `pip install -e` does, with nothing to special-case here.
    # Three packages (docs/package-guide.md), installed together so their lockstep pins
    # resolve against each other rather than an index. The single `solver` distribution
    # they replace is removed first: it owns the same `solver/` files.
    sudo "${VENV_PY}" -m pip uninstall --quiet --yes solver 2>/dev/null || true
    sudo "${VENV_PY}" -m pip install --quiet "${project_root}/packages/solver[${VENV_EXTRAS}]" \
        "${project_root}/packages/site" "${project_root}/packages/webshell"
    # The `solutions` extra installs the base `primesieve` wheel, which SKIPS the
    # numpy extension (its sdist ships NumPy-1.x-era C++ that won't build under 2.x).
    # `pip install` alone therefore leaves `primesieve.numpy` importable-but-broken in
    # the system venv — every numpy-backed solution fails at runtime in the web tier.
    # Build it from source here so a deployed venv matches a developer's (idempotent:
    # the script no-ops when the extension already imports). Best-effort — a build
    # failure (missing libprimesieve-dev / toolchain) must not abort the whole deploy.
    echo "Building the primesieve.numpy extension into ${VENV_DIR}..."
    if ! sudo VENV="${VENV_DIR}" bash "${project_root}/scripts/setup/primesieve_numpy_extension.sh" install; then
        echo "warn: primesieve.numpy build failed — numpy-backed solutions will break at runtime."
        echo "      Fix the toolchain (dev_env.sh install primesieve c) then re-run 'venv.sh deploy'."
    fi
    deploy_content "${project_root}"
    sudo chown -R root:"${web_group}" "${OPT_DIR}"
    clean_build_artifacts "${project_root}"
}

# Install the published content (euler-content) into the venv — what the public site serves
# (docs/package-guide.md § 4). Staged as the operator, from tracked files only and never
# solutions/private (scripts/content/stage.py), into a scratch directory outside the checkout;
# then installed as root like the code. Same version as the code, from the same tree.
deploy_content() {
    local project_root="$1" scratch
    scratch="$(mktemp -d)"
    echo "Staging euler-content (the public tree) and installing it into ${VENV_DIR}..."
    local ok=0
    "${PYTHON}" "${project_root}/scripts/content/stage.py" "${scratch}/content" \
        && sudo "${VENV_PY}" -m pip install --quiet --upgrade "${scratch}/content" || ok=1
    # The install builds in place, as root (build/, *.egg-info), so the scratch dir is
    # partly root-owned: remove it as root, and never let the cleanup fail the deploy.
    sudo rm -rf "${scratch}" || true
    if [ "${ok}" -ne 0 ]; then
        echo "Error: euler-content did not install — the public site has nothing to serve." >&2
        return 1
    fi
}

# Remove the in-tree build artifacts the setuptools backend leaves behind. A source
# install (`pip install <path>[...]`, not a wheel) builds *in place*, dropping `build/`
# and `solver.egg-info/` into the repo — gitignored, but they dirty the tree and the
# egg-info lands root-owned (the install ran under sudo). Neither is needed after the
# install: the deployed venv carries its own dist-info, and the operator's local
# editable install resolves via a PEP 660 `.pth` finder pointing at the source, not the
# egg-info. Remove under sudo to cover the root-owned egg-info; never fail the deploy on it.
clean_build_artifacts() {
    local project_root="$1"
    sudo rm -rf "${project_root}/build" "${project_root}"/*.egg-info \
        "${project_root}"/packages/*/build "${project_root}"/packages/*/*.egg-info 2>/dev/null || true
}

# True when the deployed venv can import *module* (build-order gate). -P ignores cwd, so
# this probes the venv's site-packages, not the repo the caller is standing in.
venv_has_module() {
    [ -x "${VENV_PY}" ] && sudo "${VENV_PY}" -P -c "import $1" 2>/dev/null
}

# Tear the root-owned system venv back out — remove the whole /opt/euler tree (the venv
# plus the primesieve.numpy build products under it). The app-service kits (auth.sh /
# user.sh) manage their own identities, state, and units; this only drops the shared
# venv, so run it when no euler-* service still needs it. Idempotent — a no-op when
# /opt/euler is already gone.
remove_venv() {
    if [ -d "${OPT_DIR}" ]; then
        echo "Removing the system venv at ${OPT_DIR}..."
        sudo rm -rf "${OPT_DIR}"
    else
        echo "No system venv at ${OPT_DIR} — nothing to remove."
    fi
}

# ── standalone use (venv.sh deploy|clean|status) ──────────────────────────────────
# Only when executed directly, not when sourced. Derives the repo root from this
# script's location (scripts/setup/venv.sh → up 2).
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
    set -euo pipefail
    _repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
    case "${1:-status}" in
        deploy) deploy_venv "${_repo}" "euler-web" ;;
        remove) remove_venv ;;
        redeploy) remove_venv; deploy_venv "${_repo}" "euler-web" ;;
        clean)  clean_build_artifacts "${_repo}"; echo "Removed in-tree build/ and *.egg-info." ;;
        status)
            if venv_has_module solver; then
                echo "venv: ✓ solver importable from ${VENV_DIR} ([${VENV_EXTRAS}])"
                if venv_has_module euler_content; then
                    echo "content: ✓ euler_content importable (the public site's tree)"
                else
                    echo "content: ✗ euler_content missing (run 'venv.sh deploy')"
                fi
            else
                echo "venv: ✗ ${VENV_DIR} not deployed (run 'venv.sh deploy' or a kit's deploy)"
            fi ;;
        -h | --help | help) echo "Usage: $0 [deploy|remove|redeploy|clean|status]  (deploy/remove/redeploy need sudo)" ;;
        *) echo "Unknown action: ${1}"; echo "Usage: $0 [deploy|remove|redeploy|clean|status]"; exit 1 ;;
    esac
fi