#!/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(){localproject_root="$1"web_group="$2"if[!-x"${VENV_PY}"];thenecho"Creating system venv at ${VENV_DIR} (${PYTHON})..."sudomkdir-p"${OPT_DIR}"sudo"${PYTHON}"-mvenv"${VENV_DIR}"fiecho"Installing euler-solver[${VENV_EXTRAS}], euler-site, euler-webshell into ${VENV_DIR} (as root, from ${project_root})..."sudo"${VENV_PY}"-mpipinstall--quiet--upgradepip# 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}"-mpipuninstall--quiet--yessolver2>/dev/null||truesudo"${VENV_PY}"-mpipinstall--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!sudoVENV="${VENV_DIR}"bash"${project_root}/scripts/setup/primesieve_numpy_extension.sh"install;thenecho"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'."fideploy_content"${project_root}"sudochown-Rroot:"${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(){localproject_root="$1"scratchscratch="$(mktemp-d)"echo"Staging euler-content (the public tree) and installing it into ${VENV_DIR}..."localok=0"${PYTHON}""${project_root}/scripts/content/stage.py""${scratch}/content"\&&sudo"${VENV_PY}"-mpipinstall--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.sudorm-rf"${scratch}"||trueif["${ok}"-ne0];thenecho"Error: euler-content did not install — the public site has nothing to serve.">&2return1fi}# 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(){localproject_root="$1"sudorm-rf"${project_root}/build""${project_root}"/*.egg-info\"${project_root}"/packages/*/build"${project_root}"/packages/*/*.egg-info2>/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}"];thenecho"Removing the system venv at ${OPT_DIR}..."sudorm-rf"${OPT_DIR}"elseecho"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}"];thenset-euopipefail_repo="$(cd"$(dirname"${BASH_SOURCE[0]}")/../.."&&pwd)"case"${1:-status}"indeploy)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)ifvenv_has_modulesolver;thenecho"venv: ✓ solver importable from ${VENV_DIR} ([${VENV_EXTRAS}])"ifvenv_has_moduleeuler_content;thenecho"content: ✓ euler_content importable (the public site's tree)"elseecho"content: ✗ euler_content missing (run 'venv.sh deploy')"fielseecho"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]";exit1;;esacfi