From a99a1777a20001d33e6b120c38dfde884d3d2abf Mon Sep 17 00:00:00 2001 From: Bardh Hoxha Date: Tue, 28 Jul 2026 16:55:21 +0100 Subject: [PATCH] fix(docker): repair the container build and drop dead root config The CPU image the README documents has not been buildable. Both Dockerfiles fail on their uv invocation, in different ways: .devcontainer/Dockerfile:43 uv pip install . -> error: No virtual environment found; ... pass `--system` Dockerfile:48 uv pip install --system --no-root . -> error: unexpected argument '--no-root' found `--no-root` belongs to `uv sync`, not `uv pip install`, and uv declines to touch the system interpreter without `--system`. The two files were near-duplicates that had drifted, so consolidate onto the root one and fix it: - install with `uv pip install --system .` - COPY the source before installing. The version is read from src/cbfkit/VERSION via [tool.setuptools.dynamic], and setuptools only warns when that file is missing, so installing from pyproject.toml alone quietly produced an empty cbfkit 0.0.0. - point docker-compose at the surviving Dockerfile, matching how gpu.Dockerfile is already referenced That puts `COPY . .` back on the live path, which makes context weight matter, so tighten .dockerignore: drop generated media, prose directories and local tool state, and fix the `mypy_cache` entry that never matched the real `.mypy_cache` (~200 MB by itself). Context goes from 405 MB to 11 MB. README.md and LICENSE stay, since setuptools reads both for wheel metadata. Remove two files that do nothing: - .gitattributes, empty since 6e08796 two years ago - .devcontainer/devcontainer.json, superseded by cbfkit-container/, which is what the README's dev-container instructions already point at Correct two stale descriptions. conftest.py's docstring claimed it configured mypy, ruff and bytecode caches and XLA memory; it does none of that. .env.example advertised MYPY_CACHE_DIR, RUFF_CACHE_DIR and PYTHONPYCACHEPREFIX, none of which can work from a .env file: the first two are separate processes that never load it, and Python reads the third at interpreter startup, before conftest.py runs. JAX_ENABLE_X64 was the file's only uncommented line and is already set in src/cbfkit/__init__.py. Test suite unchanged: 492 passed, 4 skipped. --- .devcontainer/Dockerfile | 49 -------------------- .devcontainer/devcontainer.json | 39 ---------------- .devcontainer/docker-compose.yml | 2 +- .dockerignore | 34 +++++++++++++- .env.example | 77 +++++++++++++++----------------- .gitattributes | 0 Dockerfile | 19 +++----- conftest.py | 22 +++++---- 8 files changed, 91 insertions(+), 151 deletions(-) delete mode 100644 .devcontainer/Dockerfile delete mode 100644 .devcontainer/devcontainer.json delete mode 100644 .gitattributes diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index 91bfd9ba..00000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,49 +0,0 @@ -FROM ros:humble-ros-base-jammy - -# Set environment variables to avoid interactive prompts during installation -ENV DEBIAN_FRONTEND=noninteractive \ - PYTHONFAULTHANDLER=1 \ - PYTHONUNBUFFERED=1 \ - PYTHONHASHSEED=random \ - YOUR_ENV=default_value \ - PATH="/root/.local/bin:${PATH}" - -# Version pinning as ARGs for easier updates -ARG PYTHON_VERSION=3.10 - -# Update the package list, install necessary packages in one layer -RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends \ - make \ - build-essential \ - software-properties-common \ - curl \ - ssh \ - git \ - libopenblas-dev \ - && add-apt-repository -y ppa:deadsnakes/ppa \ - && apt-get install -y ffmpeg \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# Install uv -RUN curl -LsSf https://astral.sh/uv/install.sh | sh - -# Expose the port that Jupyter Notebook will run on -EXPOSE 8888 - -WORKDIR /home/cbfkit - -# # Copy the project files -COPY pyproject.toml ./ - -# Set the PYTHONPATH to include /home and project directories -ENV PYTHONPATH="/home:/home/cbfkit:/home/cbfkit/src:${PYTHONPATH}" - -# Install dependencies using uv -RUN uv pip install . - -# Source the ROS 2 environment for all users when starting a shell -RUN echo "source /opt/ros/humble/setup.bash" >> /root/.bashrc - -# Set the safe directory to /home/cbfkit -RUN git config --global --add safe.directory /home/cbfkit diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 939181e3..00000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "CBFkit Docker container", - "dockerComposeFile": "docker-compose.yml", - "service": "cbfkit", - "workspaceFolder": "/home/cbfkit", - "customizations": { - "vscode": { - "extensions": [ - "ms-python.python", - "esbenp.prettier-vscode", - "redhat.vscode-yaml", - "aaron-bond.better-comments", - "shardulm94.trailing-spaces", - "ms-python.black-formatter", - "ms-toolsai.jupyter" - ], - "settings": { - "[python]": { - "editor.defaultFormatter": "ms-python.black-formatter", - "python.formatting.provider": "black", - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.organizeImports": true - }, - "python.formatting.blackArgs": [ - "--line-length", - "88", - "--skip-magic-trailing-comma" - ] - }, - "nb_black.black_on_save": true, - "isort.args": [ - "--profile", - "black" - ] - } - } - } -} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 1c8b480b..71083caa 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -5,7 +5,7 @@ services: platform: linux/arm64 build: context: ../ - dockerfile: .devcontainer/Dockerfile + dockerfile: Dockerfile volumes: - ../:/home/cbfkit - ${SSH_KEYS_DIR:-~/.ssh}:/root/.ssh:ro diff --git a/.dockerignore b/.dockerignore index a827fefd..5f8b88f5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,16 @@ +# Keeps the build context small. The Dockerfile does `COPY . .`, so anything +# not excluded here is shipped into the image, and generated animation output +# dwarfs the source tree (examples/ and tutorials/ are ~186 MB on disk but only +# ~736 KB tracked). +# +# README.md and LICENSE must NOT be excluded: setuptools reads both when it +# builds the wheel metadata. + .git .gitignore +.github + +# Python build and cache artefacts **/__pycache__ **/*.pyc **/*.pyo @@ -8,10 +19,31 @@ **/*.dylib **/*.o **/*.swp +**/*.egg-info +build/ +dist/ .venv .env -mypy_cache +.mypy_cache .pytest_cache +.ruff_cache .ipynb_checkpoints + +# Generated figures, animations and rendered notebooks. Safe to drop: src/ +# contains no media, and package-data is limited to codegen/templates/*.j2, +# py.typed and VERSION. +media/ +**/*.gif +**/*.mp4 +**/*.png +**/*.html + +# Prose and local tooling state, not needed to run the library +docs/ +paper/ +blog/ +.omc/ +.playwright-mcp/ + __MACOSX .DS_Store diff --git a/.env.example b/.env.example index 2099f06b..819e02da 100644 --- a/.env.example +++ b/.env.example @@ -1,55 +1,52 @@ -# Environment Configuration for cbfkit -# ===================================== -# Copy this file to .env and uncomment/customize paths for your system. -# If not set, tools will use their default local cache directories. +# Environment configuration for cbfkit +# ==================================== +# Copy this file to .env and uncomment what you need. +# +# Scope: .env is loaded by the root conftest.py, which only pytest imports. +# These variables therefore apply to `pytest` runs and nothing else. A plain +# `python tutorials/....py`, and the mypy/ruff/black processes, never read this +# file -- export those in your shell profile instead. # ============================================ -# JAX / GPU CONFIGURATION +# JAX # ============================================ -# Enable 64-bit float precision (recommended for numerical stability) -# Required for QP solvers (OSQP/JAXopt) and MPPI exponential weighting -JAX_ENABLE_X64=true +# Backend selection. conftest.py already defaults the test session to "cpu", +# so set this only to override (for example to exercise a GPU build). +# JAX_PLATFORM_NAME=gpu -# Force JAX to use CPU or GPU backend -# JAX_PLATFORM_NAME=cpu - -# GPU memory preallocation (default: true, preallocates 75% of GPU memory) -# Set to false to allocate memory on-demand (may cause fragmentation) +# GPU memory preallocation (default: true, preallocates 75% of GPU memory). +# Set to false to allocate on demand; may cause fragmentation. # XLA_PYTHON_CLIENT_PREALLOCATE=false -# Fraction of GPU memory to preallocate (default: 0.75) -# Increase if you get OOM errors at startup, decrease if sharing GPU +# Fraction of GPU memory to preallocate (default: 0.75). +# Raise it if you hit OOM at startup, lower it when sharing a GPU. # XLA_PYTHON_CLIENT_MEM_FRACTION=0.75 -# Memory allocator strategy -# Set to "platform" for minimal memory footprint (slower, good for debugging OOM) +# Allocator strategy. "platform" minimises footprint at a speed cost, which +# helps when debugging OOM. # XLA_PYTHON_CLIENT_ALLOCATOR=platform +# 64-bit precision is enabled in code (src/cbfkit/__init__.py calls +# config.update("jax_enable_x64", True) at import), so JAX_ENABLE_X64 does not +# belong here. + # ============================================ -# CACHE DIRECTORIES (for cloud sync users) +# Cache locations # ============================================ -# Uncomment the paths for your operating system: - -# --- Windows --- -# PYTHONPYCACHEPREFIX=C:\Users\yourname\.cache\pycache -# MYPY_CACHE_DIR=C:\Users\yourname\.cache\mypy -# PYTEST_CACHE_DIR=C:\Users\yourname\.cache\pytest -# RUFF_CACHE_DIR=C:\Users\yourname\.cache\ruff - -# --- macOS --- -# PYTHONPYCACHEPREFIX=/Users/yourname/.cache/pycache -# MYPY_CACHE_DIR=/Users/yourname/.cache/mypy -# PYTEST_CACHE_DIR=/Users/yourname/.cache/pytest -# RUFF_CACHE_DIR=/Users/yourname/.cache/ruff - -# --- Linux (Ubuntu, etc.) --- -# PYTHONPYCACHEPREFIX=/home/yourname/.cache/pycache -# MYPY_CACHE_DIR=/home/yourname/.cache/mypy + +# Moves pytest's cache, which is useful when the checkout lives in a synced +# folder. Applied by conftest.py::pytest_configure. # PYTEST_CACHE_DIR=/home/yourname/.cache/pytest -# RUFF_CACHE_DIR=/home/yourname/.cache/ruff -# Virtual environment location (must be set as system env var, not here) -# Windows: UV_PROJECT_ENVIRONMENT=C:\Users\yourname\.venvs\cbfkit -# macOS: UV_PROJECT_ENVIRONMENT=/Users/yourname/.venvs/cbfkit -# Linux: UV_PROJECT_ENVIRONMENT=/home/yourname/.venvs/cbfkit \ No newline at end of file +# The following CANNOT be set from this file, and earlier versions of it were +# wrong to suggest otherwise: +# +# MYPY_CACHE_DIR, RUFF_CACHE_DIR mypy and ruff are separate processes and +# never load .env +# PYTHONPYCACHEPREFIX Python reads it at interpreter startup, +# long before conftest.py runs +# UV_PROJECT_ENVIRONMENT uv reads it before Python starts +# +# Export those in your shell profile, e.g. +# export MYPY_CACHE_DIR="$HOME/.cache/mypy" diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index e69de29b..00000000 diff --git a/Dockerfile b/Dockerfile index 658629c8..5596f318 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,24 +33,17 @@ EXPOSE 8888 WORKDIR /home/cbfkit -# # Copy the project files -COPY pyproject.toml ./ - # Set the PYTHONPATH to include /home and project directories ENV PYTHONPATH="/home:/home/cbfkit:/home/cbfkit/src:${PYTHONPATH}" -# Copy dependency definitions first to leverage caching -COPY pyproject.toml uv.lock ./ - -# Install dependencies using uv (system-wide) -# We use --no-root to install only dependencies defined in pyproject.toml/uv.lock -# This prevents the command from failing due to missing source code -RUN uv pip install --system --no-root . - -# Copy the project files +# Copy the source before installing. The version is read from +# src/cbfkit/VERSION via [tool.setuptools.dynamic], and setuptools treats a +# missing VERSION file as a warning rather than an error -- so installing from +# pyproject.toml alone quietly produces an empty cbfkit 0.0.0. COPY . . -# Install the project itself +# --system is required: uv installs into a virtualenv by default and aborts +# when it cannot find one, which is always the case in this image. RUN uv pip install --system . # Source the ROS 2 environment for all users when starting a shell diff --git a/conftest.py b/conftest.py index 4f207d07..c0a1e68b 100644 --- a/conftest.py +++ b/conftest.py @@ -1,14 +1,19 @@ """ -Root conftest.py - Loads environment variables from .env file. +Root conftest.py - test-session environment setup. -This module: -1. Uses python-dotenv to load .env file (if present) -2. Sets environment variables for mypy, ruff, pytest, and Python bytecode caches -3. Sets JAX/XLA GPU memory configuration if specified -4. Configures pytest's cache directory dynamically +pytest.ini anchors the rootdir here, so pytest imports this before any test +module. That makes it the earliest available hook for process-wide setup: -Environment variables are only set if not already present in the environment, -allowing system-level overrides. +1. Loads a .env file from the project root via python-dotenv, when both the + file and the package are present. Existing environment variables win. +2. Puts ./src at the front of sys.path, so the checkout is exercised rather + than an unrelated cbfkit sitting in site-packages. +3. Defaults JAX to the CPU backend, which stops sandboxed and CI hosts from + crashing on a Metal/GPU backend that is visible but not usable. +4. Honours PYTEST_CACHE_DIR, for keeping the cache off a synced folder. + +Note the reach of step 1: .env only affects code running inside this process. +mypy, ruff and black are separate processes and never see it. """ import os @@ -23,6 +28,7 @@ def load_dotenv(*_args, **_kwargs): return False + # Load .env file from project root (won't override existing env vars) load_dotenv(Path(__file__).parent / ".env")