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(),