From 73601bb13df44474d1f8f147e37ee97cfbda83b6 Mon Sep 17 00:00:00 2001 From: Brian Date: Sun, 20 Sep 2026 06:25:34 -0700 Subject: [PATCH] packaging: stop exact-pinning build backends Exact setuptools/wheel pins block platform-specific backend patches and make isolated pip installs fail on Android/Termux Python. Keep a setuptools lower bound only, drop the unused wheel backend pin, keep requirements.txt aligned with core package deps, and install the Spaces extra in Docker instead of pulling bitsandbytes by default. Android/Termux is still not a supported runtime; the new platform note documents host wheels, ANDROID_API_LEVEL, and --no-build-isolation so pip does not isolate a second setuptools or compile manylinux-only extensions. --- Dockerfile | 8 ++-- README.md | 4 +- ci/pr-test-policy.json | 3 ++ docs/platforms/android.md | 56 ++++++++++++++++++++++++++++ pyproject.toml | 4 +- requirements.txt | 4 +- tests/test_ci_policy.py | 22 +++++++++++ tests/test_jetson_support_tooling.py | 20 ++++++++++ 8 files changed, 114 insertions(+), 7 deletions(-) create mode 100644 docs/platforms/android.md diff --git a/Dockerfile b/Dockerfile index 723ce0d..e0002fa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,15 +10,17 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ WORKDIR /app -# Install Python deps first (cache layer) +# Install Python deps first (cache layer). requirements.txt is the core +# package graph; the Spaces extra adds Gradio for app.py without pulling +# the quantization extra. COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Copy project COPY . . -# Install the package itself (for obliteratus imports) -RUN pip install --no-cache-dir . +# Install the package itself (for obliteratus imports) plus the UI extra. +RUN pip install --no-cache-dir ".[spaces]" # Run as non-root user for security RUN useradd -m appuser diff --git a/README.md b/README.md index 6bcd9b5..8516422 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,9 @@ and rollback requirements. Install `.[spaces,quantization]` instead when the UI must load supported bitsandbytes 8-bit or 4-bit models. Jetson users must follow the dedicated [Jetson bootstrap](docs/platforms/jetson.md); its bitsandbytes path is not yet -supported. +supported. Android/Termux is not a supported runtime; if you install there +anyway, follow the [Android install notes](docs/platforms/android.md) so pip +does not isolate a second setuptools or replace host wheels. ### 3. Google Colab (free GPU) diff --git a/ci/pr-test-policy.json b/ci/pr-test-policy.json index 48184e6..edcff09 100644 --- a/ci/pr-test-policy.json +++ b/ci/pr-test-policy.json @@ -20,6 +20,9 @@ "scripts/select_pr_tests.py", "scripts/setup_jetson.py", "pyproject.toml", + "requirements.txt", + "Dockerfile", + "docs/platforms/**", "uv.lock", "tests/fixtures/distributed_checkpoints/**" ], diff --git a/docs/platforms/android.md b/docs/platforms/android.md new file mode 100644 index 0000000..ef24604 --- /dev/null +++ b/docs/platforms/android.md @@ -0,0 +1,56 @@ +# Android / Termux install notes + +Android is not a supported OBLITERATUS runtime. These notes exist so a +Termux (or other Android Python) install does not fight host wheels or +download a second setuptools behind PEP 517 isolation. + +`sys.platform` on current Android CPython is `android`, not `linux`. The +locked CPU PyTorch index in `pyproject.toml` is intentionally limited to +`linux` and `win32` manylinux/Windows wheels. Do not point Android at that +index. + +## Layout + +1. Install the platform's own `python`, `setuptools`, and scientific + wheels first (`torch`, `numpy`, and whatever Termux already packages). + Those builds carry the Android patches that PyPI wheels do not. + `safetensors` and `tokenizers` are Rust extensions; rustup does not + host `aarch64-linux-android`, so install Termux `rust` before pip + and export the API level Python was built against (24 on current + Termux): + + ```bash + pkg install rust + export ANDROID_API_LEVEL=24 + ``` + +2. Isolated pip builds of meson packages (pandas, matplotlib) will try + to compile `cmake`/`ninja` from source and can hang under proot. + Install those binaries from Termux first (`pkg install ninja cmake + python-matplotlib python-scipy python-pillow python-psutil + python-pyarrow python-greenlet python-brotli python-cryptography + python-rpds-py python-msgpack libraqm harfbuzz`). PyPI `psutil` + sdists reject `sys.platform == "android"`; the Termux package is + patched. Termux matplotlib needs `libraqm`. PyPI `tokenizers` abi3 + wheels fail on CPython 3.14 (`PyBaseObject_Type`); rebuild without + the `abi3` cargo feature so the extension is `cp314` instead of + `abi3`. Then install OBLITERATUS without build isolation: + + ```bash + python -m pip install --no-build-isolation -e ".[spaces]" + ``` + +3. If pip still tries to compile `torch` or `numpy` from source, install + the project over the host packages instead: + + ```bash + python -m pip install --no-build-isolation --no-deps -e . + python -m pip install --no-build-isolation "gradio>=6.7,<7.0" + ``` + +Do not use `uv sync --locked` on Android. The lock resolves Linux/Windows +CPU PyTorch and does not contain `android_*` wheels for torch, numpy, +pyarrow, tokenizers, or bitsandbytes. + +The `quantization` and `qwen-hybrid` extras are CUDA-oriented and are not +an Android install path. diff --git a/pyproject.toml b/pyproject.toml index cb394da..02b1071 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,7 @@ [build-system] -requires = ["setuptools==80.9.0", "wheel==0.45.1"] +# Lower bound only. Exact backend pins block platform-specific setuptools patches. +# A separate wheel package is not required for the PEP 517 backend. +requires = ["setuptools>=80.9.0"] build-backend = "setuptools.build_meta" [project] diff --git a/requirements.txt b/requirements.txt index d985dd9..2ebfff7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ -gradio>=6.7,<7.0 +# Mirrors [project].dependencies in pyproject.toml. +# Optional extras (spaces, quantization, qwen-hybrid, dev) are not included. torch>=2.0 transformers>=4.40 datasets>=2.14 @@ -13,4 +14,3 @@ numpy>=1.24 scikit-learn>=1.3 tqdm>=4.64 optuna>=4.9,<5 -bitsandbytes>=0.46.1 diff --git a/tests/test_ci_policy.py b/tests/test_ci_policy.py index 1034eb4..1864846 100644 --- a/tests/test_ci_policy.py +++ b/tests/test_ci_policy.py @@ -225,6 +225,28 @@ def test_ci_evidence_names_the_pull_request_head_not_the_synthetic_merge_commit( assert '--head-sha "$GITHUB_SHA"' not in workflow +def test_build_backend_requirements_use_lower_bounds_not_exact_pins(): + pyproject = (ROOT / "pyproject.toml").read_text(encoding="utf-8") + match = re.search( + r"(?ms)^\[build-system\]\n(?:#[^\n]*\n)*requires = \[([^\]]+)\]", + pyproject, + ) + assert match is not None + requires = re.findall(r'"([^"]+)"', match.group(1)) + assert requires + for requirement in requires: + spec = requirement.split(";", 1)[0] + name = re.split(r"[<>=!~]", spec, maxsplit=1)[0].strip() + assert name != "wheel", requirement + assert "==" not in spec, requirement + assert ">=" in spec, requirement + lower = re.findall(r">=([^,]+)", spec) + upper = re.findall(r"<=([^,]+)", spec) + assert not ( + lower and upper and lower[0] == upper[0] + ), f"collapsed exact pin written as a range: {requirement}" + + def test_ci_runs_checkpoint_portability_contracts_on_windows(): workflow = WORKFLOW.read_text(encoding="utf-8") checkpoint_job = workflow.split(" checkpoint-windows:\n", maxsplit=1)[1].split( diff --git a/tests/test_jetson_support_tooling.py b/tests/test_jetson_support_tooling.py index 84e6f7e..3f89f84 100644 --- a/tests/test_jetson_support_tooling.py +++ b/tests/test_jetson_support_tooling.py @@ -217,6 +217,26 @@ def test_bitsandbytes_is_opt_in_for_quantization_only(): assert extras["quantization"] == ["bitsandbytes>=0.46.1"] +def test_requirements_txt_mirrors_core_pyproject_dependencies(): + metadata = tomllib.loads((ROOT / "pyproject.toml").read_text()) + requirements = [ + line.strip() + for line in (ROOT / "requirements.txt").read_text().splitlines() + if line.strip() and not line.startswith("#") + ] + + assert requirements == metadata["project"]["dependencies"] + assert not any(value.startswith("bitsandbytes") for value in requirements) + assert not any(value.startswith("gradio") for value in requirements) + + +def test_dockerfile_installs_spaces_extra_not_quantization(): + dockerfile = (ROOT / "Dockerfile").read_text() + + assert 'pip install --no-cache-dir ".[spaces]"' in dockerfile + assert "bitsandbytes" not in dockerfile + + def test_jetson_issue_form_requires_reproducible_sanitized_hardware_evidence(): form = yaml.safe_load( (ROOT / ".github" / "ISSUE_TEMPLATE" / "jetson-runtime.yml").read_text(),