From 8e5659a23ea2e0bdc892e66075f782abd182e2dc Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Mon, 6 Jul 2026 11:55:44 -0400 Subject: [PATCH 01/30] refactor Dockerfile for efficiency, multi-layer caching, and versioning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Switch builder base to tensorflow/tensorflow:2.21.0 (Python 3.11) so TF is pre-installed, avoiding the multi-GB re-download and disk pressure - Single-stage build to eliminate the large COPY --from layer commit that exhausted Podman's /var/tmp on constrained local disks - Install CPU-only PyTorch before ufish/cellpose to prevent the NVIDIA CUDA stack from being pulled in as a transitive dependency - Layer ordering: most-stable deps first (ufish → cellpose → extras → core) so requirements.txt changes don't invalidate the heavy cached layers - Strip tensorflow from the uv install pass (already in base image) to avoid re-downloading the 545 MB wheel - ARG TORCH_COMPUTE=cpu: override with --build-arg TORCH_COMPUTE=cu124 etc. for GPU-enabled deployments - ARG TF_VERSION / PYTHON_VERSION: single place to update the TF base image - Patch dist-info METADATA + _version.py after install so setuptools_scm does not overwrite the version with 0.0.0 when .git is absent - .dockerignore: add !scallops/_version.py so the pre-generated version file is always included in the build context; expand exclusions for IDE dirs, build artifacts, and coverage reports --- .dockerignore | 37 ++++++++++++++++++ Dockerfile | 101 ++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 123 insertions(+), 15 deletions(-) diff --git a/.dockerignore b/.dockerignore index 6ab18dc..ac73d0d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,39 @@ +# Version control +.git +.github + +# IDE / editor +.idea +.vscode +*.swp +*.swo + +# Python build artifacts +__pycache__ +*.pyc +*.pyo +*.pyd +*.egg-info +dist +build +.tox +.nox + +# Tests and coverage scallops/tests +.pytest_cache +.coverage +.coverage.* +htmlcov + +# Documentation (not needed for install) docs +requirements.doc.txt + +# macOS +.DS_Store + +# _version.py is gitignored but must be generated before docker build +# (run `python -m setuptools_scm` or `pip install -e .` first). +# The ! forces inclusion even if a future pattern would otherwise exclude it. +!scallops/_version.py diff --git a/Dockerfile b/Dockerfile index 6d8bbd5..1a49448 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,89 @@ -FROM python:3.13-slim +# syntax=docker/dockerfile:1 +# PYTHON_VERSION must match the Python inside tensorflow/tensorflow:${TF_VERSION} +# (currently 3.11); changing TF_VERSION may require updating PYTHON_VERSION. +ARG TF_VERSION=2.21.0 +ARG PYTHON_VERSION=3.11 +# Override at build time for GPU: --build-arg TORCH_COMPUTE=cu124 (or cu126, cu128, etc.) +ARG TORCH_COMPUTE=cpu -ENV AWS_RETRY_MODE=adaptive \ - AWS_MAX_ATTEMPTS=10 +FROM --platform=linux/amd64 tensorflow/tensorflow:${TF_VERSION} +ARG PYTHON_VERSION +ARG TORCH_COMPUTE + +COPY --from=docker.io/astral/uv:latest /uv /uvx /bin/ + +# build-essential: needed for mahotas/centrosome (requirements.txt) and the +# Cython extension; git: needed to clone ufish from its pinned tag RUN apt-get update -qq && \ - apt-get install -qq --no-install-recommends -y \ + DEBIAN_FRONTEND=noninteractive apt-get install -qq --no-install-recommends -y \ build-essential \ - git \ - ca-certificates -WORKDIR /app -COPY requirements.txt requirements.ufish.txt ./ -RUN pip install -q --no-cache-dir --upgrade pip && pip install -q --no-cache-dir -r requirements.txt -RUN pip install -q --no-cache-dir -r requirements.ufish.txt -COPY . ./ -RUN pip install . -RUN apt-get remove -qq -y build-essential git && \ - apt-get autoremove -qq -y && apt-get clean -qq && \ - rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /app/ + git && \ + rm -rf /var/lib/apt/lists/* + +ENV UV_SYSTEM_PYTHON=1 \ + UV_NO_CACHE=1 \ + UV_HTTP_TIMEOUT=300 + +WORKDIR /build + +# Each file is its own layer ordered most→least stable for cache efficiency. + +# Install CPU-only PyTorch before ufish/cellpose to prevent the multi-GB NVIDIA +# CUDA stack from being pulled in as a transitive dep. +# --allow-insecure-host: download-r2.pytorch.org (PyTorch CDN) is intercepted +# by SSL inspection proxies; proper fix is to inject your proxy CA cert. +RUN uv pip install torch torchvision \ + --index-url https://download.pytorch.org/whl/${TORCH_COMPUTE} \ + --allow-insecure-host download.pytorch.org \ + --allow-insecure-host download-r2.pytorch.org + +# ufish: git-pinned tag, rarely bumped +COPY requirements.ufish.txt ./ +RUN uv pip install -r requirements.ufish.txt + +# cellpose 3.x: declares numpy<2.1 but is runtime-compatible with numpy 2.x; +# separate step so uv resolves against already-installed numpy +COPY requirements.cellpose.txt ./ +RUN uv pip install -r requirements.cellpose.txt + +# extra optional deps: only change when this Dockerfile changes +RUN uv pip install pysam napari napari_ome_zarr dask-ml miniwdl pytest pytest-xdist + +# core deps: TF already in base image so strip it to avoid re-downloading +COPY requirements.txt ./ +RUN grep -v '^tensorflow' requirements.txt | uv pip install -r /dev/stdin + +# Compile Cython extension; --no-deps skips re-resolving all dependencies +# (already installed above) which would otherwise re-download tensorflow. +# || true on compileall: torch ships py312_intrinsics.py with PEP 695 syntax +# that Python 3.11 cannot parse — that file is never imported on 3.11. +# Stash _version.py to /tmp before the broad COPY so it survives rm -rf /build. +# Fail here with a clear Docker message if it hasn't been generated yet +# (run `python -m setuptools_scm` or `pip install -e .` first). +COPY scallops/_version.py /tmp/scm_version.py +COPY . . +# --no-deps: all deps already installed above; avoids re-downloading tensorflow. +# setuptools_scm falls back to 0.0.0 without .git, so after install we patch +# both the installed _version.py and the dist-info METADATA with the real +# version read from the pre-generated /tmp/scm_version.py. +# || true on compileall: torch ships py312_intrinsics.py (PEP 695 syntax) that +# Python 3.11 cannot parse — that file is never imported on 3.11. +RUN uv pip install --no-build-isolation --no-deps . && \ + SCM_VERSION=$(python3 -c "import re; print(re.search(r\"version = '([^']+)'\", open('/tmp/scm_version.py').read()).group(1))") && \ + DIST_PKGS=/usr/local/lib/python${PYTHON_VERSION}/dist-packages && \ + cp /tmp/scm_version.py ${DIST_PKGS}/scallops/_version.py && \ + sed -i "s/^Version: .*/Version: ${SCM_VERSION}/" ${DIST_PKGS}/scallops-*.dist-info/METADATA && \ + python -m compileall -q /usr/local/lib/python${PYTHON_VERSION} 2>&1 | \ + grep -v 'py312_intrinsics' || true && \ + rm -rf /build /tmp/scm_version.py + +# fontconfig required by napari/vispy at import time (font rendering) +RUN apt-get update -qq && \ + DEBIAN_FRONTEND=noninteractive apt-get install -qq --no-install-recommends -y \ + libfontconfig1 && \ + rm -rf /var/lib/apt/lists/* + +ENV AWS_RETRY_MODE=adaptive \ + AWS_MAX_ATTEMPTS=10 \ + TF_CPP_MIN_LOG_LEVEL=2 \ + PYTHONUNBUFFERED=1 From 288db6ad7f663d678e78fa43cb158f7bf8160135 Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Mon, 6 Jul 2026 12:29:31 -0400 Subject: [PATCH 02/30] yadd docker.mk preflight and fix version injection via SCM_VERSION build-arg setuptools_scm falls back to 0.0.0 when .git is absent from the build context. Fix it at every level: Dockerfile: accept SCM_VERSION as a build arg (default 0.0.0+unknown), passed by CI or docker.mk. Patch both _version.py and dist-info METADATA after install. Remove the fragile COPY scallops/_version.py that broke on fresh clones. docker.mk: local mirror of the CI workflow. Runs python -m setuptools_scm as a preflight, attaches OCI labels matching docker/metadata-action output, tags with semver + short SHA, uses docker buildx to match CI. Includes docker-push target for manual GHCR pushes. docker.yml: add fetch-depth=0 so setuptools_scm can walk git history to find tags; generate SCM_VERSION and pass it as a build-arg to build-push-action. .dockerignore: exclude _version.py from build context (version now injected via build-arg, not file copy). --- .dockerignore | 7 ++-- .github/workflows/docker.yml | 9 +++++ Dockerfile | 33 ++++++++-------- docker.mk | 75 ++++++++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 19 deletions(-) create mode 100644 docker.mk diff --git a/.dockerignore b/.dockerignore index ac73d0d..5f64a64 100644 --- a/.dockerignore +++ b/.dockerignore @@ -33,7 +33,6 @@ requirements.doc.txt # macOS .DS_Store -# _version.py is gitignored but must be generated before docker build -# (run `python -m setuptools_scm` or `pip install -e .` first). -# The ! forces inclusion even if a future pattern would otherwise exclude it. -!scallops/_version.py +# _version.py is injected via --build-arg SCM_VERSION by the Makefile (make docker) +# so it does not need to be in the build context +scallops/_version.py diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 386e20b..6304aed 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -18,6 +18,14 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v6 + with: + fetch-depth: 0 # full history so setuptools_scm can resolve the version from tags + + - name: Generate package version + id: scm + run: | + pip install --quiet setuptools_scm + echo "version=$(python -m setuptools_scm)" >> "$GITHUB_OUTPUT" - name: Log in to the GitHub Container Registry uses: docker/login-action@v3 @@ -51,5 +59,6 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + build-args: SCM_VERSION=${{ steps.scm.outputs.version }} cache-from: type=gha cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile index 1a49448..f5f40be 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,10 +5,16 @@ ARG TF_VERSION=2.21.0 ARG PYTHON_VERSION=3.11 # Override at build time for GPU: --build-arg TORCH_COMPUTE=cu124 (or cu126, cu128, etc.) ARG TORCH_COMPUTE=cpu +# SCM_VERSION is passed by: +# - CI: .github/workflows/docker.yml (resolved via python -m setuptools_scm) +# - locally: docker.mk (make -f docker.mk docker) +# Falls back to 0.0.0+unknown when building directly with docker/podman build . +ARG SCM_VERSION=0.0.0+unknown FROM --platform=linux/amd64 tensorflow/tensorflow:${TF_VERSION} ARG PYTHON_VERSION ARG TORCH_COMPUTE +ARG SCM_VERSION COPY --from=docker.io/astral/uv:latest /uv /uvx /bin/ @@ -53,29 +59,26 @@ RUN uv pip install pysam napari napari_ome_zarr dask-ml miniwdl pytest pytest-xd COPY requirements.txt ./ RUN grep -v '^tensorflow' requirements.txt | uv pip install -r /dev/stdin -# Compile Cython extension; --no-deps skips re-resolving all dependencies -# (already installed above) which would otherwise re-download tensorflow. -# || true on compileall: torch ships py312_intrinsics.py with PEP 695 syntax -# that Python 3.11 cannot parse — that file is never imported on 3.11. -# Stash _version.py to /tmp before the broad COPY so it survives rm -rf /build. -# Fail here with a clear Docker message if it hasn't been generated yet -# (run `python -m setuptools_scm` or `pip install -e .` first). -COPY scallops/_version.py /tmp/scm_version.py COPY . . # --no-deps: all deps already installed above; avoids re-downloading tensorflow. -# setuptools_scm falls back to 0.0.0 without .git, so after install we patch -# both the installed _version.py and the dist-info METADATA with the real -# version read from the pre-generated /tmp/scm_version.py. +# SCM_VERSION is injected by the Makefile; patch both the installed _version.py +# and the dist-info METADATA so every version surface reports the real version. # || true on compileall: torch ships py312_intrinsics.py (PEP 695 syntax) that # Python 3.11 cannot parse — that file is never imported on 3.11. -RUN uv pip install --no-build-isolation --no-deps . && \ - SCM_VERSION=$(python3 -c "import re; print(re.search(r\"version = '([^']+)'\", open('/tmp/scm_version.py').read()).group(1))") && \ +RUN SETUPTOOLS_SCM_PRETEND_VERSION=${SCM_VERSION} uv pip install --no-build-isolation --no-deps . && \ DIST_PKGS=/usr/local/lib/python${PYTHON_VERSION}/dist-packages && \ - cp /tmp/scm_version.py ${DIST_PKGS}/scallops/_version.py && \ + python3 -c " +v = '${SCM_VERSION}' +txt = open('${DIST_PKGS}/scallops/_version.py').read() +import re +txt = re.sub(r\"version = '[^']*'\", f\"version = '{v}'\", txt) +txt = re.sub(r\"__version__ = version = '[^']*'\", f\"__version__ = version = '{v}'\", txt) +open('${DIST_PKGS}/scallops/_version.py', 'w').write(txt) +" && \ sed -i "s/^Version: .*/Version: ${SCM_VERSION}/" ${DIST_PKGS}/scallops-*.dist-info/METADATA && \ python -m compileall -q /usr/local/lib/python${PYTHON_VERSION} 2>&1 | \ grep -v 'py312_intrinsics' || true && \ - rm -rf /build /tmp/scm_version.py + rm -rf /build # fontconfig required by napari/vispy at import time (font rendering) RUN apt-get update -qq && \ diff --git a/docker.mk b/docker.mk new file mode 100644 index 0000000..efe3f9e --- /dev/null +++ b/docker.mk @@ -0,0 +1,75 @@ +# docker.mk — local mirror of the CI/CD Docker workflow (.github/workflows/docker.yml) +# +# The CI workflow (docker/build-push-action) does the following that this file replicates: +# 1. Resolves the package version via setuptools_scm and passes it as SCM_VERSION build-arg +# 2. Attaches OCI standard labels (revision, source, version, created, …) +# 3. Tags the image with the semver version AND a short git-SHA tag +# 4. Uses docker buildx (not plain docker build) for consistency with CI +# +# What CI does that this file intentionally does NOT replicate: +# - GitHub Actions layer cache (type=gha) — use a local registry cache if needed +# - Automatic push to ghcr.io — use `make -f docker.mk docker-push` explicitly +# +# Usage: +# make -f docker.mk docker # CPU image (default) +# make -f docker.mk docker-gpu TORCH_COMPUTE=cu124 # GPU image +# make -f docker.mk docker-push # build CPU + push to GHCR + +# ── configurable knobs ──────────────────────────────────────────────────────── +REGISTRY ?= ghcr.io/genentech +IMAGE ?= scallops +TF_VERSION ?= 2.21.0 +TORCH_COMPUTE ?= cpu + +# ── derived values (match what docker/metadata-action computes in CI) ───────── +SCM_VERSION := $(shell python -m setuptools_scm 2>/dev/null || echo "0.0.0+unknown") +GIT_SHA := $(shell git rev-parse HEAD) +GIT_SHA_SHORT := $(shell git rev-parse --short HEAD) +GIT_SOURCE := $(shell git remote get-url origin 2>/dev/null \ + | sed 's|git@github.com:|https://github.com/|; s|\.git$$||') +BUILD_DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ) + +FULL_IMAGE := $(REGISTRY)/$(IMAGE) + +# OCI labels — mirrors what docker/metadata-action generates in CI +LABELS := \ + --label org.opencontainers.image.created=$(BUILD_DATE) \ + --label org.opencontainers.image.revision=$(GIT_SHA) \ + --label org.opencontainers.image.source=$(GIT_SOURCE) \ + --label org.opencontainers.image.version=$(SCM_VERSION) \ + --label org.opencontainers.image.title=$(IMAGE) \ + --label org.opencontainers.image.url=$(GIT_SOURCE) + +BUILD_ARGS := \ + --build-arg SCM_VERSION=$(SCM_VERSION) \ + --build-arg TF_VERSION=$(TF_VERSION) \ + --build-arg TORCH_COMPUTE=$(TORCH_COMPUTE) + +# Tags: version + short-SHA (mirrors CI's semver + sha tags) +TAGS := \ + -t $(FULL_IMAGE):$(SCM_VERSION) \ + -t $(FULL_IMAGE):sha-$(GIT_SHA_SHORT) \ + -t $(FULL_IMAGE):latest + +.PHONY: docker docker-gpu docker-push + +## Build the CPU image locally (default) +docker: + docker buildx build \ + $(BUILD_ARGS) \ + $(LABELS) \ + $(TAGS) \ + . + +## Build a GPU image locally; override TORCH_COMPUTE if needed (e.g. cu126) +docker-gpu: + $(MAKE) -f docker.mk docker TORCH_COMPUTE=$(or $(TORCH_COMPUTE),cu124) + +## Build and push the CPU image to GHCR (requires `docker login ghcr.io` first) +docker-push: + docker buildx build \ + $(BUILD_ARGS) \ + $(LABELS) \ + $(TAGS) \ + --push \ + . From 27e1b68dc3202bf217aff7abb368541e227f28cc Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Mon, 6 Jul 2026 12:40:35 -0400 Subject: [PATCH 03/30] docker.mk: auto-install setuptools_scm if missing, check buildx prereq --- docker.mk | 49 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/docker.mk b/docker.mk index efe3f9e..41cf3e8 100644 --- a/docker.mk +++ b/docker.mk @@ -22,43 +22,59 @@ TF_VERSION ?= 2.21.0 TORCH_COMPUTE ?= cpu # ── derived values (match what docker/metadata-action computes in CI) ───────── -SCM_VERSION := $(shell python -m setuptools_scm 2>/dev/null || echo "0.0.0+unknown") -GIT_SHA := $(shell git rev-parse HEAD) +# NOTE: SCM_VERSION is intentionally left as a lazy variable (=) so that it is +# evaluated *after* the _check_deps recipe has installed setuptools_scm. +GIT_SHA := $(shell git rev-parse HEAD) GIT_SHA_SHORT := $(shell git rev-parse --short HEAD) -GIT_SOURCE := $(shell git remote get-url origin 2>/dev/null \ - | sed 's|git@github.com:|https://github.com/|; s|\.git$$||') -BUILD_DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ) +GIT_SOURCE := $(shell git remote get-url origin 2>/dev/null \ + | sed 's|git@github.com:|https://github.com/|; s|\.git$$||') +BUILD_DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ) +FULL_IMAGE := $(REGISTRY)/$(IMAGE) -FULL_IMAGE := $(REGISTRY)/$(IMAGE) +# ── prerequisite check ──────────────────────────────────────────────────────── +# Installs missing Python deps then recomputes SCM_VERSION inside the recipe +# (top-level $(shell) runs before any target so we cannot rely on it here). +define check_prereqs + @python -m setuptools_scm --version >/dev/null 2>&1 \ + || (echo "[docker.mk] setuptools_scm not found — installing..." \ + && python -m pip install -q setuptools_scm) + @docker buildx version >/dev/null 2>&1 \ + || (echo "[docker.mk] ERROR: docker buildx required (Docker 20.10+ or the buildx plugin)" \ + && exit 1) + $(eval SCM_VERSION := $(shell python -m setuptools_scm)) +endef # OCI labels — mirrors what docker/metadata-action generates in CI -LABELS := \ +define oci_labels --label org.opencontainers.image.created=$(BUILD_DATE) \ --label org.opencontainers.image.revision=$(GIT_SHA) \ --label org.opencontainers.image.source=$(GIT_SOURCE) \ --label org.opencontainers.image.version=$(SCM_VERSION) \ --label org.opencontainers.image.title=$(IMAGE) \ --label org.opencontainers.image.url=$(GIT_SOURCE) +endef -BUILD_ARGS := \ +define build_args --build-arg SCM_VERSION=$(SCM_VERSION) \ --build-arg TF_VERSION=$(TF_VERSION) \ --build-arg TORCH_COMPUTE=$(TORCH_COMPUTE) +endef -# Tags: version + short-SHA (mirrors CI's semver + sha tags) -TAGS := \ +define image_tags -t $(FULL_IMAGE):$(SCM_VERSION) \ -t $(FULL_IMAGE):sha-$(GIT_SHA_SHORT) \ -t $(FULL_IMAGE):latest +endef .PHONY: docker docker-gpu docker-push ## Build the CPU image locally (default) docker: + $(call check_prereqs) docker buildx build \ - $(BUILD_ARGS) \ - $(LABELS) \ - $(TAGS) \ + $(call build_args) \ + $(call oci_labels) \ + $(call image_tags) \ . ## Build a GPU image locally; override TORCH_COMPUTE if needed (e.g. cu126) @@ -67,9 +83,10 @@ docker-gpu: ## Build and push the CPU image to GHCR (requires `docker login ghcr.io` first) docker-push: + $(call check_prereqs) docker buildx build \ - $(BUILD_ARGS) \ - $(LABELS) \ - $(TAGS) \ + $(call build_args) \ + $(call oci_labels) \ + $(call image_tags) \ --push \ . From 11cc66bf974460eb83c3da8012cdcff5b6336108 Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Mon, 6 Jul 2026 12:59:46 -0400 Subject: [PATCH 04/30] docker.mk: use uv for setuptools_scm (auto-install uv if missing, --no-project to avoid TF resolution) --- docker.mk | 97 ++++++++++++++++++++++++++----------------------------- 1 file changed, 46 insertions(+), 51 deletions(-) diff --git a/docker.mk b/docker.mk index 41cf3e8..1cd00c6 100644 --- a/docker.mk +++ b/docker.mk @@ -11,9 +11,9 @@ # - Automatic push to ghcr.io — use `make -f docker.mk docker-push` explicitly # # Usage: -# make -f docker.mk docker # CPU image (default) -# make -f docker.mk docker-gpu TORCH_COMPUTE=cu124 # GPU image -# make -f docker.mk docker-push # build CPU + push to GHCR +# make -f docker.mk docker # CPU image (default) +# make -f docker.mk docker-gpu TORCH_COMPUTE=cu124 # GPU image +# make -f docker.mk docker-push # build CPU + push to GHCR # ── configurable knobs ──────────────────────────────────────────────────────── REGISTRY ?= ghcr.io/genentech @@ -21,72 +21,67 @@ IMAGE ?= scallops TF_VERSION ?= 2.21.0 TORCH_COMPUTE ?= cpu -# ── derived values (match what docker/metadata-action computes in CI) ───────── -# NOTE: SCM_VERSION is intentionally left as a lazy variable (=) so that it is -# evaluated *after* the _check_deps recipe has installed setuptools_scm. -GIT_SHA := $(shell git rev-parse HEAD) -GIT_SHA_SHORT := $(shell git rev-parse --short HEAD) +# ── uv: prefer PATH, fall back to the default install location ──────────────── +# If uv is not found, _check_prereqs installs it there automatically. +UV := $(shell which uv 2>/dev/null || echo "$(HOME)/.local/bin/uv") + +# ── values computed at parse time (none depend on setuptools_scm) ───────────── +GIT_SHA := $(shell git rev-parse HEAD 2>/dev/null || echo "unknown") +GIT_SHA_SHORT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") GIT_SOURCE := $(shell git remote get-url origin 2>/dev/null \ | sed 's|git@github.com:|https://github.com/|; s|\.git$$||') BUILD_DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ) FULL_IMAGE := $(REGISTRY)/$(IMAGE) -# ── prerequisite check ──────────────────────────────────────────────────────── -# Installs missing Python deps then recomputes SCM_VERSION inside the recipe -# (top-level $(shell) runs before any target so we cannot rely on it here). -define check_prereqs - @python -m setuptools_scm --version >/dev/null 2>&1 \ - || (echo "[docker.mk] setuptools_scm not found — installing..." \ - && python -m pip install -q setuptools_scm) +# ── internal helpers ────────────────────────────────────────────────────────── + +# Installs missing prerequisites at shell execution time (not Make parse time). +# uv is used instead of pip to avoid PEP-668 "externally managed environment" +# errors on Homebrew/system Python installs. +define _check_prereqs + @"$(UV)" --version >/dev/null 2>&1 \ + || (echo "[docker.mk] uv not found — installing via official installer..." \ + && curl -LsSf https://astral.sh/uv/install.sh | sh) @docker buildx version >/dev/null 2>&1 \ || (echo "[docker.mk] ERROR: docker buildx required (Docker 20.10+ or the buildx plugin)" \ && exit 1) - $(eval SCM_VERSION := $(shell python -m setuptools_scm)) -endef - -# OCI labels — mirrors what docker/metadata-action generates in CI -define oci_labels - --label org.opencontainers.image.created=$(BUILD_DATE) \ - --label org.opencontainers.image.revision=$(GIT_SHA) \ - --label org.opencontainers.image.source=$(GIT_SOURCE) \ - --label org.opencontainers.image.version=$(SCM_VERSION) \ - --label org.opencontainers.image.title=$(IMAGE) \ - --label org.opencontainers.image.url=$(GIT_SOURCE) -endef - -define build_args - --build-arg SCM_VERSION=$(SCM_VERSION) \ - --build-arg TF_VERSION=$(TF_VERSION) \ - --build-arg TORCH_COMPUTE=$(TORCH_COMPUTE) endef -define image_tags - -t $(FULL_IMAGE):$(SCM_VERSION) \ - -t $(FULL_IMAGE):sha-$(GIT_SHA_SHORT) \ - -t $(FULL_IMAGE):latest +# SCM_VERSION is a shell variable ($$) so it is evaluated at shell execution +# time, after _check_prereqs has ensured uv is present. +# `uv run --with setuptools_scm` creates a throwaway venv — no system Python +# pollution, no pip permission issues. +define _build + @SCM_VERSION=$$("$(UV)" run --with setuptools_scm --no-project --quiet \ + python3 -m setuptools_scm) \ + && echo "[docker.mk] Building $(FULL_IMAGE):$$SCM_VERSION" \ + && docker buildx build \ + --build-arg SCM_VERSION=$$SCM_VERSION \ + --build-arg TF_VERSION=$(TF_VERSION) \ + --build-arg TORCH_COMPUTE=$(TORCH_COMPUTE) \ + --label "org.opencontainers.image.created=$(BUILD_DATE)" \ + --label "org.opencontainers.image.revision=$(GIT_SHA)" \ + --label "org.opencontainers.image.source=$(GIT_SOURCE)" \ + --label "org.opencontainers.image.version=$$SCM_VERSION" \ + --label "org.opencontainers.image.title=$(IMAGE)" \ + --label "org.opencontainers.image.url=$(GIT_SOURCE)" \ + -t "$(FULL_IMAGE):$$SCM_VERSION" \ + -t "$(FULL_IMAGE):sha-$(GIT_SHA_SHORT)" \ + -t "$(FULL_IMAGE):latest" endef .PHONY: docker docker-gpu docker-push ## Build the CPU image locally (default) docker: - $(call check_prereqs) - docker buildx build \ - $(call build_args) \ - $(call oci_labels) \ - $(call image_tags) \ - . + $(call _check_prereqs) + $(call _build) . -## Build a GPU image locally; override TORCH_COMPUTE if needed (e.g. cu126) +## Build a GPU image; override TORCH_COMPUTE if needed (e.g. cu126) docker-gpu: $(MAKE) -f docker.mk docker TORCH_COMPUTE=$(or $(TORCH_COMPUTE),cu124) -## Build and push the CPU image to GHCR (requires `docker login ghcr.io` first) +## Build and push to GHCR (requires `docker login ghcr.io` first) docker-push: - $(call check_prereqs) - docker buildx build \ - $(call build_args) \ - $(call oci_labels) \ - $(call image_tags) \ - --push \ - . + $(call _check_prereqs) + $(call _build) --push . From d2ebc21987c0989e0794a1b023128aa892a9903c Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Mon, 6 Jul 2026 13:02:18 -0400 Subject: [PATCH 05/30] docker.mk: sanitize Docker tag by replacing + with - (PEP 440 local versions are invalid in Docker tag format) --- docker.mk | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docker.mk b/docker.mk index 1cd00c6..e0004bc 100644 --- a/docker.mk +++ b/docker.mk @@ -54,7 +54,8 @@ endef define _build @SCM_VERSION=$$("$(UV)" run --with setuptools_scm --no-project --quiet \ python3 -m setuptools_scm) \ - && echo "[docker.mk] Building $(FULL_IMAGE):$$SCM_VERSION" \ + && DOCKER_TAG=$$(echo "$$SCM_VERSION" | tr '+' '-') \ + && echo "[docker.mk] Building $(FULL_IMAGE):$$DOCKER_TAG (package version $$SCM_VERSION)" \ && docker buildx build \ --build-arg SCM_VERSION=$$SCM_VERSION \ --build-arg TF_VERSION=$(TF_VERSION) \ @@ -65,7 +66,7 @@ define _build --label "org.opencontainers.image.version=$$SCM_VERSION" \ --label "org.opencontainers.image.title=$(IMAGE)" \ --label "org.opencontainers.image.url=$(GIT_SOURCE)" \ - -t "$(FULL_IMAGE):$$SCM_VERSION" \ + -t "$(FULL_IMAGE):$$DOCKER_TAG" \ -t "$(FULL_IMAGE):sha-$(GIT_SHA_SHORT)" \ -t "$(FULL_IMAGE):latest" endef From 614bb4a693a88f17b379656901d0b83175eae01a Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Mon, 6 Jul 2026 13:08:45 -0400 Subject: [PATCH 06/30] Dockerfile: replace multiline python3 -c block with sed (fixed Dockerfile parse error on bare lines) --- Dockerfile | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index f5f40be..3408b4d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -67,15 +67,10 @@ COPY . . # Python 3.11 cannot parse — that file is never imported on 3.11. RUN SETUPTOOLS_SCM_PRETEND_VERSION=${SCM_VERSION} uv pip install --no-build-isolation --no-deps . && \ DIST_PKGS=/usr/local/lib/python${PYTHON_VERSION}/dist-packages && \ - python3 -c " -v = '${SCM_VERSION}' -txt = open('${DIST_PKGS}/scallops/_version.py').read() -import re -txt = re.sub(r\"version = '[^']*'\", f\"version = '{v}'\", txt) -txt = re.sub(r\"__version__ = version = '[^']*'\", f\"__version__ = version = '{v}'\", txt) -open('${DIST_PKGS}/scallops/_version.py', 'w').write(txt) -" && \ - sed -i "s/^Version: .*/Version: ${SCM_VERSION}/" ${DIST_PKGS}/scallops-*.dist-info/METADATA && \ + sed -i "s|version = '[^']*'|version = '${SCM_VERSION}'|g" \ + ${DIST_PKGS}/scallops/_version.py && \ + sed -i "s/^Version: .*/Version: ${SCM_VERSION}/" \ + ${DIST_PKGS}/scallops-*.dist-info/METADATA && \ python -m compileall -q /usr/local/lib/python${PYTHON_VERSION} 2>&1 | \ grep -v 'py312_intrinsics' || true && \ rm -rf /build From 03fefc98f22706adf06e11290dd05c8d3656c614 Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Mon, 6 Jul 2026 14:00:24 -0400 Subject: [PATCH 07/30] fix version injection in Docker image; add --version flag to CLI Dockerfile: replace fragile sed glob with a Python one-liner that uses importlib.metadata.distribution() to find the exact dist-info path and patches both METADATA (so importlib.metadata.version() returns the right value) and _version.py (so scallops.__version__ is consistent). __main__.py: add --version argparse action so `scallops --version` works; previously the version was only visible buried in --help description text. --- Dockerfile | 6 +----- scallops/__main__.py | 3 +++ 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3408b4d..1e27788 100644 --- a/Dockerfile +++ b/Dockerfile @@ -66,11 +66,7 @@ COPY . . # || true on compileall: torch ships py312_intrinsics.py (PEP 695 syntax) that # Python 3.11 cannot parse — that file is never imported on 3.11. RUN SETUPTOOLS_SCM_PRETEND_VERSION=${SCM_VERSION} uv pip install --no-build-isolation --no-deps . && \ - DIST_PKGS=/usr/local/lib/python${PYTHON_VERSION}/dist-packages && \ - sed -i "s|version = '[^']*'|version = '${SCM_VERSION}'|g" \ - ${DIST_PKGS}/scallops/_version.py && \ - sed -i "s/^Version: .*/Version: ${SCM_VERSION}/" \ - ${DIST_PKGS}/scallops-*.dist-info/METADATA && \ + python3 -c "import importlib.metadata as m,re,pathlib; v='${SCM_VERSION}'; d=m.distribution('scallops'); p=pathlib.Path(d._path); md=p/'METADATA'; md.write_text(re.sub(r'^Version:.*','Version: '+v,md.read_text(),flags=re.M)); vf=p.parent/'scallops'/'_version.py'; vf.exists() and vf.write_text(re.sub(r\"version = '[^']*'\",\"version = '\"+v+\"'\",vf.read_text()))" && \ python -m compileall -q /usr/local/lib/python${PYTHON_VERSION} 2>&1 | \ grep -v 'py312_intrinsics' || true && \ rm -rf /build diff --git a/scallops/__main__.py b/scallops/__main__.py index 83f924e..6147be4 100644 --- a/scallops/__main__.py +++ b/scallops/__main__.py @@ -56,6 +56,9 @@ def create_parsers(default_help: bool = False) -> argparse.ArgumentParser: root_parser = ScallopsArgumentParser( prog="scallops", description=f"Version {version('scallops')}" ) + root_parser.add_argument( + "--version", action="version", version=f"%(prog)s {version('scallops')}" + ) subparsers = root_parser.add_subparsers(help="Command help") pooled_if_sbs_main._create_parser(subparsers, default_help) features_main._create_parser(subparsers, default_help) From 173f6d614eebe1cc622fb30fecdf203ee86fcdac Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Mon, 6 Jul 2026 14:09:07 -0400 Subject: [PATCH 08/30] fix torch SSL HandshakeFailure on Linux; robust docker.mk version detection Dockerfile: switch torch/torchvision install from uv to pip. uv uses rustls which fails with HandshakeFailure on networks with SSL inspection proxies; pip uses OpenSSL which negotiates TLS more permissively. docker.mk: use `uv run --with setuptools_scm` to resolve the package version. This creates a throwaway venv, avoiding PEP-668 externally-managed-environment errors on Homebrew/system Python without needing pip --user or pipx. Docker tag sanitizes the PEP-440 local separator (+) to (-) for compatibility. --- Dockerfile | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1e27788..b6858d7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,14 +34,16 @@ WORKDIR /build # Each file is its own layer ordered most→least stable for cache efficiency. -# Install CPU-only PyTorch before ufish/cellpose to prevent the multi-GB NVIDIA -# CUDA stack from being pulled in as a transitive dep. -# --allow-insecure-host: download-r2.pytorch.org (PyTorch CDN) is intercepted -# by SSL inspection proxies; proper fix is to inject your proxy CA cert. -RUN uv pip install torch torchvision \ +# Install PyTorch before ufish/cellpose to prevent them pulling in a second +# (CUDA-enabled) copy of torch as a transitive dep. +# pip is used instead of uv here: uv's rustls TLS stack fails with +# HandshakeFailure on networks that use SSL inspection proxies, while pip's +# OpenSSL is more permissive. --trusted-host bypasses cert verification for +# the PyTorch CDN; the proper fix is to inject your proxy CA cert. +RUN pip install --no-cache-dir torch torchvision \ --index-url https://download.pytorch.org/whl/${TORCH_COMPUTE} \ - --allow-insecure-host download.pytorch.org \ - --allow-insecure-host download-r2.pytorch.org + --trusted-host download.pytorch.org \ + --trusted-host download-r2.pytorch.org # ufish: git-pinned tag, rarely bumped COPY requirements.ufish.txt ./ From 71e5cf2f28298d9408fd0d596f4c30d88add8c09 Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Mon, 6 Jul 2026 14:16:55 -0400 Subject: [PATCH 09/30] remove separate torch install step that broke SSL on restricted networks The PyTorch CDN (download-r2.pytorch.org) is blocked by SSL inspection on some networks. The separate pip/uv torch install step was introduced to get CPU-only wheels but caused HandshakeFailure errors on Linux. Removing the step entirely: torch is now installed as a transitive dep of ufish/cellpose from PyPI, which is CUDA-capable by default and works with both CPU and GPU at runtime (--gpus all). Image is larger but builds everywhere without network-specific SSL workarounds. Also removes the unused TORCH_COMPUTE build arg. --- Dockerfile | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/Dockerfile b/Dockerfile index b6858d7..84e37a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,6 @@ # (currently 3.11); changing TF_VERSION may require updating PYTHON_VERSION. ARG TF_VERSION=2.21.0 ARG PYTHON_VERSION=3.11 -# Override at build time for GPU: --build-arg TORCH_COMPUTE=cu124 (or cu126, cu128, etc.) -ARG TORCH_COMPUTE=cpu # SCM_VERSION is passed by: # - CI: .github/workflows/docker.yml (resolved via python -m setuptools_scm) # - locally: docker.mk (make -f docker.mk docker) @@ -34,17 +32,6 @@ WORKDIR /build # Each file is its own layer ordered most→least stable for cache efficiency. -# Install PyTorch before ufish/cellpose to prevent them pulling in a second -# (CUDA-enabled) copy of torch as a transitive dep. -# pip is used instead of uv here: uv's rustls TLS stack fails with -# HandshakeFailure on networks that use SSL inspection proxies, while pip's -# OpenSSL is more permissive. --trusted-host bypasses cert verification for -# the PyTorch CDN; the proper fix is to inject your proxy CA cert. -RUN pip install --no-cache-dir torch torchvision \ - --index-url https://download.pytorch.org/whl/${TORCH_COMPUTE} \ - --trusted-host download.pytorch.org \ - --trusted-host download-r2.pytorch.org - # ufish: git-pinned tag, rarely bumped COPY requirements.ufish.txt ./ RUN uv pip install -r requirements.ufish.txt From 6cf4efe73c515aea497a7dfdf605bd7fe14594ff Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Mon, 6 Jul 2026 14:30:59 -0400 Subject: [PATCH 10/30] fix version display: replace fragile python3 one-liner with sed importlib.metadata d._path is a private attribute that fails silently on Ubuntu's dist-packages layout. Use sed with | delimiters (safe for the + and . in PEP 440 versions) to patch both METADATA and _version.py directly. --- Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 84e37a1..8f37c79 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,7 +55,9 @@ COPY . . # || true on compileall: torch ships py312_intrinsics.py (PEP 695 syntax) that # Python 3.11 cannot parse — that file is never imported on 3.11. RUN SETUPTOOLS_SCM_PRETEND_VERSION=${SCM_VERSION} uv pip install --no-build-isolation --no-deps . && \ - python3 -c "import importlib.metadata as m,re,pathlib; v='${SCM_VERSION}'; d=m.distribution('scallops'); p=pathlib.Path(d._path); md=p/'METADATA'; md.write_text(re.sub(r'^Version:.*','Version: '+v,md.read_text(),flags=re.M)); vf=p.parent/'scallops'/'_version.py'; vf.exists() and vf.write_text(re.sub(r\"version = '[^']*'\",\"version = '\"+v+\"'\",vf.read_text()))" && \ + DIST_PKGS=/usr/local/lib/python${PYTHON_VERSION}/dist-packages && \ + sed -i "s|^Version:.*|Version: ${SCM_VERSION}|" ${DIST_PKGS}/scallops-*.dist-info/METADATA && \ + sed -i "s|version = '[^']*'|version = '${SCM_VERSION}'|g" ${DIST_PKGS}/scallops/_version.py && \ python -m compileall -q /usr/local/lib/python${PYTHON_VERSION} 2>&1 | \ grep -v 'py312_intrinsics' || true && \ rm -rf /build From cbeb22fde291b72ce578ba7e77eb6a174a0dc75e Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Mon, 6 Jul 2026 11:55:44 -0400 Subject: [PATCH 11/30] refactor Dockerfile for efficiency, multi-layer caching, and versioning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Switch builder base to tensorflow/tensorflow:2.21.0 (Python 3.11) so TF is pre-installed, avoiding the multi-GB re-download and disk pressure - Single-stage build to eliminate the large COPY --from layer commit that exhausted Podman's /var/tmp on constrained local disks - Install CPU-only PyTorch before ufish/cellpose to prevent the NVIDIA CUDA stack from being pulled in as a transitive dependency - Layer ordering: most-stable deps first (ufish → cellpose → extras → core) so requirements.txt changes don't invalidate the heavy cached layers - Strip tensorflow from the uv install pass (already in base image) to avoid re-downloading the 545 MB wheel - ARG TORCH_COMPUTE=cpu: override with --build-arg TORCH_COMPUTE=cu124 etc. for GPU-enabled deployments - ARG TF_VERSION / PYTHON_VERSION: single place to update the TF base image - Patch dist-info METADATA + _version.py after install so setuptools_scm does not overwrite the version with 0.0.0 when .git is absent - .dockerignore: add !scallops/_version.py so the pre-generated version file is always included in the build context; expand exclusions for IDE dirs, build artifacts, and coverage reports --- .dockerignore | 37 ++++++++++++++++++ Dockerfile | 101 ++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 123 insertions(+), 15 deletions(-) diff --git a/.dockerignore b/.dockerignore index 6ab18dc..ac73d0d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,39 @@ +# Version control +.git +.github + +# IDE / editor +.idea +.vscode +*.swp +*.swo + +# Python build artifacts +__pycache__ +*.pyc +*.pyo +*.pyd +*.egg-info +dist +build +.tox +.nox + +# Tests and coverage scallops/tests +.pytest_cache +.coverage +.coverage.* +htmlcov + +# Documentation (not needed for install) docs +requirements.doc.txt + +# macOS +.DS_Store + +# _version.py is gitignored but must be generated before docker build +# (run `python -m setuptools_scm` or `pip install -e .` first). +# The ! forces inclusion even if a future pattern would otherwise exclude it. +!scallops/_version.py diff --git a/Dockerfile b/Dockerfile index 6d8bbd5..1a49448 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,89 @@ -FROM python:3.13-slim +# syntax=docker/dockerfile:1 +# PYTHON_VERSION must match the Python inside tensorflow/tensorflow:${TF_VERSION} +# (currently 3.11); changing TF_VERSION may require updating PYTHON_VERSION. +ARG TF_VERSION=2.21.0 +ARG PYTHON_VERSION=3.11 +# Override at build time for GPU: --build-arg TORCH_COMPUTE=cu124 (or cu126, cu128, etc.) +ARG TORCH_COMPUTE=cpu -ENV AWS_RETRY_MODE=adaptive \ - AWS_MAX_ATTEMPTS=10 +FROM --platform=linux/amd64 tensorflow/tensorflow:${TF_VERSION} +ARG PYTHON_VERSION +ARG TORCH_COMPUTE + +COPY --from=docker.io/astral/uv:latest /uv /uvx /bin/ + +# build-essential: needed for mahotas/centrosome (requirements.txt) and the +# Cython extension; git: needed to clone ufish from its pinned tag RUN apt-get update -qq && \ - apt-get install -qq --no-install-recommends -y \ + DEBIAN_FRONTEND=noninteractive apt-get install -qq --no-install-recommends -y \ build-essential \ - git \ - ca-certificates -WORKDIR /app -COPY requirements.txt requirements.ufish.txt ./ -RUN pip install -q --no-cache-dir --upgrade pip && pip install -q --no-cache-dir -r requirements.txt -RUN pip install -q --no-cache-dir -r requirements.ufish.txt -COPY . ./ -RUN pip install . -RUN apt-get remove -qq -y build-essential git && \ - apt-get autoremove -qq -y && apt-get clean -qq && \ - rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /app/ + git && \ + rm -rf /var/lib/apt/lists/* + +ENV UV_SYSTEM_PYTHON=1 \ + UV_NO_CACHE=1 \ + UV_HTTP_TIMEOUT=300 + +WORKDIR /build + +# Each file is its own layer ordered most→least stable for cache efficiency. + +# Install CPU-only PyTorch before ufish/cellpose to prevent the multi-GB NVIDIA +# CUDA stack from being pulled in as a transitive dep. +# --allow-insecure-host: download-r2.pytorch.org (PyTorch CDN) is intercepted +# by SSL inspection proxies; proper fix is to inject your proxy CA cert. +RUN uv pip install torch torchvision \ + --index-url https://download.pytorch.org/whl/${TORCH_COMPUTE} \ + --allow-insecure-host download.pytorch.org \ + --allow-insecure-host download-r2.pytorch.org + +# ufish: git-pinned tag, rarely bumped +COPY requirements.ufish.txt ./ +RUN uv pip install -r requirements.ufish.txt + +# cellpose 3.x: declares numpy<2.1 but is runtime-compatible with numpy 2.x; +# separate step so uv resolves against already-installed numpy +COPY requirements.cellpose.txt ./ +RUN uv pip install -r requirements.cellpose.txt + +# extra optional deps: only change when this Dockerfile changes +RUN uv pip install pysam napari napari_ome_zarr dask-ml miniwdl pytest pytest-xdist + +# core deps: TF already in base image so strip it to avoid re-downloading +COPY requirements.txt ./ +RUN grep -v '^tensorflow' requirements.txt | uv pip install -r /dev/stdin + +# Compile Cython extension; --no-deps skips re-resolving all dependencies +# (already installed above) which would otherwise re-download tensorflow. +# || true on compileall: torch ships py312_intrinsics.py with PEP 695 syntax +# that Python 3.11 cannot parse — that file is never imported on 3.11. +# Stash _version.py to /tmp before the broad COPY so it survives rm -rf /build. +# Fail here with a clear Docker message if it hasn't been generated yet +# (run `python -m setuptools_scm` or `pip install -e .` first). +COPY scallops/_version.py /tmp/scm_version.py +COPY . . +# --no-deps: all deps already installed above; avoids re-downloading tensorflow. +# setuptools_scm falls back to 0.0.0 without .git, so after install we patch +# both the installed _version.py and the dist-info METADATA with the real +# version read from the pre-generated /tmp/scm_version.py. +# || true on compileall: torch ships py312_intrinsics.py (PEP 695 syntax) that +# Python 3.11 cannot parse — that file is never imported on 3.11. +RUN uv pip install --no-build-isolation --no-deps . && \ + SCM_VERSION=$(python3 -c "import re; print(re.search(r\"version = '([^']+)'\", open('/tmp/scm_version.py').read()).group(1))") && \ + DIST_PKGS=/usr/local/lib/python${PYTHON_VERSION}/dist-packages && \ + cp /tmp/scm_version.py ${DIST_PKGS}/scallops/_version.py && \ + sed -i "s/^Version: .*/Version: ${SCM_VERSION}/" ${DIST_PKGS}/scallops-*.dist-info/METADATA && \ + python -m compileall -q /usr/local/lib/python${PYTHON_VERSION} 2>&1 | \ + grep -v 'py312_intrinsics' || true && \ + rm -rf /build /tmp/scm_version.py + +# fontconfig required by napari/vispy at import time (font rendering) +RUN apt-get update -qq && \ + DEBIAN_FRONTEND=noninteractive apt-get install -qq --no-install-recommends -y \ + libfontconfig1 && \ + rm -rf /var/lib/apt/lists/* + +ENV AWS_RETRY_MODE=adaptive \ + AWS_MAX_ATTEMPTS=10 \ + TF_CPP_MIN_LOG_LEVEL=2 \ + PYTHONUNBUFFERED=1 From 1a641689589278171dc072601aa81c68464bda74 Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Tue, 7 Jul 2026 11:15:14 -0400 Subject: [PATCH 12/30] drop napari/pytest, fix version injection, remove NVIDIA apt repos - Remove napari, napari_ome_zarr, pytest, pytest-xdist and libfontconfig1: napari is a GUI tool useless headless; test deps have no place in production - Fix version: set SETUPTOOLS_SCM_PRETEND_VERSION via Docker ENV so it propagates into the PEP 517 subprocess that uv spawns; tested via minimal container that confirmed dist-info version matches SCM_VERSION build-arg - Remove NVIDIA apt repos before apt-get to prevent SSL failures on restricted networks (TF base image ships these repos; they fail with cert errors) - Infer Python version dynamically from interpreter instead of hardcoded ARG; print it in build logs; remove stale PYTHON_VERSION and TORCH_COMPUTE ARGs - Version flows entirely from SETUPTOOLS_SCM_PRETEND_VERSION through setuptools_scm's own write_to; no manual _version.py writes --- Dockerfile | 42 ++++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8f37c79..11adc39 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,12 @@ # syntax=docker/dockerfile:1 -# PYTHON_VERSION must match the Python inside tensorflow/tensorflow:${TF_VERSION} -# (currently 3.11); changing TF_VERSION may require updating PYTHON_VERSION. -ARG TF_VERSION=2.21.0 -ARG PYTHON_VERSION=3.11 # SCM_VERSION is passed by: # - CI: .github/workflows/docker.yml (resolved via python -m setuptools_scm) # - locally: docker.mk (make -f docker.mk docker) # Falls back to 0.0.0+unknown when building directly with docker/podman build . +ARG TF_VERSION=2.21.0 ARG SCM_VERSION=0.0.0+unknown FROM --platform=linux/amd64 tensorflow/tensorflow:${TF_VERSION} -ARG PYTHON_VERSION -ARG TORCH_COMPUTE ARG SCM_VERSION COPY --from=docker.io/astral/uv:latest /uv /uvx /bin/ @@ -24,12 +19,19 @@ RUN apt-get update -qq && \ git && \ rm -rf /var/lib/apt/lists/* +# SETUPTOOLS_SCM_PRETEND_VERSION set as Docker ENV so it propagates into the +# PEP 517 build subprocess that uv spawns (inline shell variables do not +# reliably cross that boundary). Tested: ENV propagation works, inline does not. ENV UV_SYSTEM_PYTHON=1 \ UV_NO_CACHE=1 \ - UV_HTTP_TIMEOUT=300 + UV_HTTP_TIMEOUT=300 \ + SETUPTOOLS_SCM_PRETEND_VERSION=${SCM_VERSION} WORKDIR /build +# Print the Python version in use so it's visible in build logs +RUN python3 --version + # Each file is its own layer ordered most→least stable for cache efficiency. # ufish: git-pinned tag, rarely bumped @@ -42,32 +44,28 @@ COPY requirements.cellpose.txt ./ RUN uv pip install -r requirements.cellpose.txt # extra optional deps: only change when this Dockerfile changes -RUN uv pip install pysam napari napari_ome_zarr dask-ml miniwdl pytest pytest-xdist +# napari/pytest excluded — napari is a GUI tool (useless headless) and test +# deps have no place in a production image +RUN uv pip install pysam dask-ml miniwdl # core deps: TF already in base image so strip it to avoid re-downloading COPY requirements.txt ./ RUN grep -v '^tensorflow' requirements.txt | uv pip install -r /dev/stdin -COPY . . # --no-deps: all deps already installed above; avoids re-downloading tensorflow. -# SCM_VERSION is injected by the Makefile; patch both the installed _version.py -# and the dist-info METADATA so every version surface reports the real version. +# SETUPTOOLS_SCM_PRETEND_VERSION (set via ENV above) ensures setuptools_scm +# writes the correct version into _version.py and the wheel metadata. +# PYTHON_VERSION is inferred dynamically from the interpreter so it always +# matches whatever Python the TF base image ships — no ARG to keep in sync. # || true on compileall: torch ships py312_intrinsics.py (PEP 695 syntax) that # Python 3.11 cannot parse — that file is never imported on 3.11. -RUN SETUPTOOLS_SCM_PRETEND_VERSION=${SCM_VERSION} uv pip install --no-build-isolation --no-deps . && \ - DIST_PKGS=/usr/local/lib/python${PYTHON_VERSION}/dist-packages && \ - sed -i "s|^Version:.*|Version: ${SCM_VERSION}|" ${DIST_PKGS}/scallops-*.dist-info/METADATA && \ - sed -i "s|version = '[^']*'|version = '${SCM_VERSION}'|g" ${DIST_PKGS}/scallops/_version.py && \ - python -m compileall -q /usr/local/lib/python${PYTHON_VERSION} 2>&1 | \ +COPY . . +RUN uv pip install --no-build-isolation --no-deps . && \ + PYVER=$(python3 -c "import sys; v=sys.version_info; print(f'{v.major}.{v.minor}')") && \ + python3 -m compileall -q /usr/local/lib/python${PYVER} 2>&1 | \ grep -v 'py312_intrinsics' || true && \ rm -rf /build -# fontconfig required by napari/vispy at import time (font rendering) -RUN apt-get update -qq && \ - DEBIAN_FRONTEND=noninteractive apt-get install -qq --no-install-recommends -y \ - libfontconfig1 && \ - rm -rf /var/lib/apt/lists/* - ENV AWS_RETRY_MODE=adaptive \ AWS_MAX_ATTEMPTS=10 \ TF_CPP_MIN_LOG_LEVEL=2 \ From 1d9566705642d6e9465ca93c9099f98c518c71a7 Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Mon, 6 Jul 2026 12:29:31 -0400 Subject: [PATCH 13/30] add docker.mk preflight and fix version injection via SCM_VERSION build-arg setuptools_scm falls back to 0.0.0 when .git is absent from the build context. Fix it at every level: Dockerfile: accept SCM_VERSION as a build arg (default 0.0.0+unknown), passed by CI or docker.mk. Patch both _version.py and dist-info METADATA after install. Remove the fragile COPY scallops/_version.py that broke on fresh clones. docker.mk: local mirror of the CI workflow. Runs python -m setuptools_scm as a preflight, attaches OCI labels matching docker/metadata-action output, tags with semver + short SHA, uses docker buildx to match CI. Includes docker-push target for manual GHCR pushes. docker.yml: add fetch-depth=0 so setuptools_scm can walk git history to find tags; generate SCM_VERSION and pass it as a build-arg to build-push-action. .dockerignore: exclude _version.py from build context (version now injected via build-arg, not file copy). --- .dockerignore | 7 ++-- .github/workflows/docker.yml | 9 +++++ Dockerfile | 33 ++++++++-------- docker.mk | 75 ++++++++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 19 deletions(-) create mode 100644 docker.mk diff --git a/.dockerignore b/.dockerignore index ac73d0d..5f64a64 100644 --- a/.dockerignore +++ b/.dockerignore @@ -33,7 +33,6 @@ requirements.doc.txt # macOS .DS_Store -# _version.py is gitignored but must be generated before docker build -# (run `python -m setuptools_scm` or `pip install -e .` first). -# The ! forces inclusion even if a future pattern would otherwise exclude it. -!scallops/_version.py +# _version.py is injected via --build-arg SCM_VERSION by the Makefile (make docker) +# so it does not need to be in the build context +scallops/_version.py diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 386e20b..6304aed 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -18,6 +18,14 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v6 + with: + fetch-depth: 0 # full history so setuptools_scm can resolve the version from tags + + - name: Generate package version + id: scm + run: | + pip install --quiet setuptools_scm + echo "version=$(python -m setuptools_scm)" >> "$GITHUB_OUTPUT" - name: Log in to the GitHub Container Registry uses: docker/login-action@v3 @@ -51,5 +59,6 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + build-args: SCM_VERSION=${{ steps.scm.outputs.version }} cache-from: type=gha cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile index 1a49448..f5f40be 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,10 +5,16 @@ ARG TF_VERSION=2.21.0 ARG PYTHON_VERSION=3.11 # Override at build time for GPU: --build-arg TORCH_COMPUTE=cu124 (or cu126, cu128, etc.) ARG TORCH_COMPUTE=cpu +# SCM_VERSION is passed by: +# - CI: .github/workflows/docker.yml (resolved via python -m setuptools_scm) +# - locally: docker.mk (make -f docker.mk docker) +# Falls back to 0.0.0+unknown when building directly with docker/podman build . +ARG SCM_VERSION=0.0.0+unknown FROM --platform=linux/amd64 tensorflow/tensorflow:${TF_VERSION} ARG PYTHON_VERSION ARG TORCH_COMPUTE +ARG SCM_VERSION COPY --from=docker.io/astral/uv:latest /uv /uvx /bin/ @@ -53,29 +59,26 @@ RUN uv pip install pysam napari napari_ome_zarr dask-ml miniwdl pytest pytest-xd COPY requirements.txt ./ RUN grep -v '^tensorflow' requirements.txt | uv pip install -r /dev/stdin -# Compile Cython extension; --no-deps skips re-resolving all dependencies -# (already installed above) which would otherwise re-download tensorflow. -# || true on compileall: torch ships py312_intrinsics.py with PEP 695 syntax -# that Python 3.11 cannot parse — that file is never imported on 3.11. -# Stash _version.py to /tmp before the broad COPY so it survives rm -rf /build. -# Fail here with a clear Docker message if it hasn't been generated yet -# (run `python -m setuptools_scm` or `pip install -e .` first). -COPY scallops/_version.py /tmp/scm_version.py COPY . . # --no-deps: all deps already installed above; avoids re-downloading tensorflow. -# setuptools_scm falls back to 0.0.0 without .git, so after install we patch -# both the installed _version.py and the dist-info METADATA with the real -# version read from the pre-generated /tmp/scm_version.py. +# SCM_VERSION is injected by the Makefile; patch both the installed _version.py +# and the dist-info METADATA so every version surface reports the real version. # || true on compileall: torch ships py312_intrinsics.py (PEP 695 syntax) that # Python 3.11 cannot parse — that file is never imported on 3.11. -RUN uv pip install --no-build-isolation --no-deps . && \ - SCM_VERSION=$(python3 -c "import re; print(re.search(r\"version = '([^']+)'\", open('/tmp/scm_version.py').read()).group(1))") && \ +RUN SETUPTOOLS_SCM_PRETEND_VERSION=${SCM_VERSION} uv pip install --no-build-isolation --no-deps . && \ DIST_PKGS=/usr/local/lib/python${PYTHON_VERSION}/dist-packages && \ - cp /tmp/scm_version.py ${DIST_PKGS}/scallops/_version.py && \ + python3 -c " +v = '${SCM_VERSION}' +txt = open('${DIST_PKGS}/scallops/_version.py').read() +import re +txt = re.sub(r\"version = '[^']*'\", f\"version = '{v}'\", txt) +txt = re.sub(r\"__version__ = version = '[^']*'\", f\"__version__ = version = '{v}'\", txt) +open('${DIST_PKGS}/scallops/_version.py', 'w').write(txt) +" && \ sed -i "s/^Version: .*/Version: ${SCM_VERSION}/" ${DIST_PKGS}/scallops-*.dist-info/METADATA && \ python -m compileall -q /usr/local/lib/python${PYTHON_VERSION} 2>&1 | \ grep -v 'py312_intrinsics' || true && \ - rm -rf /build /tmp/scm_version.py + rm -rf /build # fontconfig required by napari/vispy at import time (font rendering) RUN apt-get update -qq && \ diff --git a/docker.mk b/docker.mk new file mode 100644 index 0000000..efe3f9e --- /dev/null +++ b/docker.mk @@ -0,0 +1,75 @@ +# docker.mk — local mirror of the CI/CD Docker workflow (.github/workflows/docker.yml) +# +# The CI workflow (docker/build-push-action) does the following that this file replicates: +# 1. Resolves the package version via setuptools_scm and passes it as SCM_VERSION build-arg +# 2. Attaches OCI standard labels (revision, source, version, created, …) +# 3. Tags the image with the semver version AND a short git-SHA tag +# 4. Uses docker buildx (not plain docker build) for consistency with CI +# +# What CI does that this file intentionally does NOT replicate: +# - GitHub Actions layer cache (type=gha) — use a local registry cache if needed +# - Automatic push to ghcr.io — use `make -f docker.mk docker-push` explicitly +# +# Usage: +# make -f docker.mk docker # CPU image (default) +# make -f docker.mk docker-gpu TORCH_COMPUTE=cu124 # GPU image +# make -f docker.mk docker-push # build CPU + push to GHCR + +# ── configurable knobs ──────────────────────────────────────────────────────── +REGISTRY ?= ghcr.io/genentech +IMAGE ?= scallops +TF_VERSION ?= 2.21.0 +TORCH_COMPUTE ?= cpu + +# ── derived values (match what docker/metadata-action computes in CI) ───────── +SCM_VERSION := $(shell python -m setuptools_scm 2>/dev/null || echo "0.0.0+unknown") +GIT_SHA := $(shell git rev-parse HEAD) +GIT_SHA_SHORT := $(shell git rev-parse --short HEAD) +GIT_SOURCE := $(shell git remote get-url origin 2>/dev/null \ + | sed 's|git@github.com:|https://github.com/|; s|\.git$$||') +BUILD_DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ) + +FULL_IMAGE := $(REGISTRY)/$(IMAGE) + +# OCI labels — mirrors what docker/metadata-action generates in CI +LABELS := \ + --label org.opencontainers.image.created=$(BUILD_DATE) \ + --label org.opencontainers.image.revision=$(GIT_SHA) \ + --label org.opencontainers.image.source=$(GIT_SOURCE) \ + --label org.opencontainers.image.version=$(SCM_VERSION) \ + --label org.opencontainers.image.title=$(IMAGE) \ + --label org.opencontainers.image.url=$(GIT_SOURCE) + +BUILD_ARGS := \ + --build-arg SCM_VERSION=$(SCM_VERSION) \ + --build-arg TF_VERSION=$(TF_VERSION) \ + --build-arg TORCH_COMPUTE=$(TORCH_COMPUTE) + +# Tags: version + short-SHA (mirrors CI's semver + sha tags) +TAGS := \ + -t $(FULL_IMAGE):$(SCM_VERSION) \ + -t $(FULL_IMAGE):sha-$(GIT_SHA_SHORT) \ + -t $(FULL_IMAGE):latest + +.PHONY: docker docker-gpu docker-push + +## Build the CPU image locally (default) +docker: + docker buildx build \ + $(BUILD_ARGS) \ + $(LABELS) \ + $(TAGS) \ + . + +## Build a GPU image locally; override TORCH_COMPUTE if needed (e.g. cu126) +docker-gpu: + $(MAKE) -f docker.mk docker TORCH_COMPUTE=$(or $(TORCH_COMPUTE),cu124) + +## Build and push the CPU image to GHCR (requires `docker login ghcr.io` first) +docker-push: + docker buildx build \ + $(BUILD_ARGS) \ + $(LABELS) \ + $(TAGS) \ + --push \ + . From d28939ecff81d35b334600f5d523978240569d29 Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Mon, 6 Jul 2026 12:40:35 -0400 Subject: [PATCH 14/30] docker.mk: auto-install setuptools_scm if missing, check buildx prereq --- docker.mk | 49 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/docker.mk b/docker.mk index efe3f9e..41cf3e8 100644 --- a/docker.mk +++ b/docker.mk @@ -22,43 +22,59 @@ TF_VERSION ?= 2.21.0 TORCH_COMPUTE ?= cpu # ── derived values (match what docker/metadata-action computes in CI) ───────── -SCM_VERSION := $(shell python -m setuptools_scm 2>/dev/null || echo "0.0.0+unknown") -GIT_SHA := $(shell git rev-parse HEAD) +# NOTE: SCM_VERSION is intentionally left as a lazy variable (=) so that it is +# evaluated *after* the _check_deps recipe has installed setuptools_scm. +GIT_SHA := $(shell git rev-parse HEAD) GIT_SHA_SHORT := $(shell git rev-parse --short HEAD) -GIT_SOURCE := $(shell git remote get-url origin 2>/dev/null \ - | sed 's|git@github.com:|https://github.com/|; s|\.git$$||') -BUILD_DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ) +GIT_SOURCE := $(shell git remote get-url origin 2>/dev/null \ + | sed 's|git@github.com:|https://github.com/|; s|\.git$$||') +BUILD_DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ) +FULL_IMAGE := $(REGISTRY)/$(IMAGE) -FULL_IMAGE := $(REGISTRY)/$(IMAGE) +# ── prerequisite check ──────────────────────────────────────────────────────── +# Installs missing Python deps then recomputes SCM_VERSION inside the recipe +# (top-level $(shell) runs before any target so we cannot rely on it here). +define check_prereqs + @python -m setuptools_scm --version >/dev/null 2>&1 \ + || (echo "[docker.mk] setuptools_scm not found — installing..." \ + && python -m pip install -q setuptools_scm) + @docker buildx version >/dev/null 2>&1 \ + || (echo "[docker.mk] ERROR: docker buildx required (Docker 20.10+ or the buildx plugin)" \ + && exit 1) + $(eval SCM_VERSION := $(shell python -m setuptools_scm)) +endef # OCI labels — mirrors what docker/metadata-action generates in CI -LABELS := \ +define oci_labels --label org.opencontainers.image.created=$(BUILD_DATE) \ --label org.opencontainers.image.revision=$(GIT_SHA) \ --label org.opencontainers.image.source=$(GIT_SOURCE) \ --label org.opencontainers.image.version=$(SCM_VERSION) \ --label org.opencontainers.image.title=$(IMAGE) \ --label org.opencontainers.image.url=$(GIT_SOURCE) +endef -BUILD_ARGS := \ +define build_args --build-arg SCM_VERSION=$(SCM_VERSION) \ --build-arg TF_VERSION=$(TF_VERSION) \ --build-arg TORCH_COMPUTE=$(TORCH_COMPUTE) +endef -# Tags: version + short-SHA (mirrors CI's semver + sha tags) -TAGS := \ +define image_tags -t $(FULL_IMAGE):$(SCM_VERSION) \ -t $(FULL_IMAGE):sha-$(GIT_SHA_SHORT) \ -t $(FULL_IMAGE):latest +endef .PHONY: docker docker-gpu docker-push ## Build the CPU image locally (default) docker: + $(call check_prereqs) docker buildx build \ - $(BUILD_ARGS) \ - $(LABELS) \ - $(TAGS) \ + $(call build_args) \ + $(call oci_labels) \ + $(call image_tags) \ . ## Build a GPU image locally; override TORCH_COMPUTE if needed (e.g. cu126) @@ -67,9 +83,10 @@ docker-gpu: ## Build and push the CPU image to GHCR (requires `docker login ghcr.io` first) docker-push: + $(call check_prereqs) docker buildx build \ - $(BUILD_ARGS) \ - $(LABELS) \ - $(TAGS) \ + $(call build_args) \ + $(call oci_labels) \ + $(call image_tags) \ --push \ . From 871b6212e3396802d333d36ab311caf578217f58 Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Mon, 6 Jul 2026 12:59:46 -0400 Subject: [PATCH 15/30] docker.mk: use uv for setuptools_scm (auto-install uv if missing, --no-project to avoid TF resolution) --- docker.mk | 97 ++++++++++++++++++++++++++----------------------------- 1 file changed, 46 insertions(+), 51 deletions(-) diff --git a/docker.mk b/docker.mk index 41cf3e8..1cd00c6 100644 --- a/docker.mk +++ b/docker.mk @@ -11,9 +11,9 @@ # - Automatic push to ghcr.io — use `make -f docker.mk docker-push` explicitly # # Usage: -# make -f docker.mk docker # CPU image (default) -# make -f docker.mk docker-gpu TORCH_COMPUTE=cu124 # GPU image -# make -f docker.mk docker-push # build CPU + push to GHCR +# make -f docker.mk docker # CPU image (default) +# make -f docker.mk docker-gpu TORCH_COMPUTE=cu124 # GPU image +# make -f docker.mk docker-push # build CPU + push to GHCR # ── configurable knobs ──────────────────────────────────────────────────────── REGISTRY ?= ghcr.io/genentech @@ -21,72 +21,67 @@ IMAGE ?= scallops TF_VERSION ?= 2.21.0 TORCH_COMPUTE ?= cpu -# ── derived values (match what docker/metadata-action computes in CI) ───────── -# NOTE: SCM_VERSION is intentionally left as a lazy variable (=) so that it is -# evaluated *after* the _check_deps recipe has installed setuptools_scm. -GIT_SHA := $(shell git rev-parse HEAD) -GIT_SHA_SHORT := $(shell git rev-parse --short HEAD) +# ── uv: prefer PATH, fall back to the default install location ──────────────── +# If uv is not found, _check_prereqs installs it there automatically. +UV := $(shell which uv 2>/dev/null || echo "$(HOME)/.local/bin/uv") + +# ── values computed at parse time (none depend on setuptools_scm) ───────────── +GIT_SHA := $(shell git rev-parse HEAD 2>/dev/null || echo "unknown") +GIT_SHA_SHORT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") GIT_SOURCE := $(shell git remote get-url origin 2>/dev/null \ | sed 's|git@github.com:|https://github.com/|; s|\.git$$||') BUILD_DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ) FULL_IMAGE := $(REGISTRY)/$(IMAGE) -# ── prerequisite check ──────────────────────────────────────────────────────── -# Installs missing Python deps then recomputes SCM_VERSION inside the recipe -# (top-level $(shell) runs before any target so we cannot rely on it here). -define check_prereqs - @python -m setuptools_scm --version >/dev/null 2>&1 \ - || (echo "[docker.mk] setuptools_scm not found — installing..." \ - && python -m pip install -q setuptools_scm) +# ── internal helpers ────────────────────────────────────────────────────────── + +# Installs missing prerequisites at shell execution time (not Make parse time). +# uv is used instead of pip to avoid PEP-668 "externally managed environment" +# errors on Homebrew/system Python installs. +define _check_prereqs + @"$(UV)" --version >/dev/null 2>&1 \ + || (echo "[docker.mk] uv not found — installing via official installer..." \ + && curl -LsSf https://astral.sh/uv/install.sh | sh) @docker buildx version >/dev/null 2>&1 \ || (echo "[docker.mk] ERROR: docker buildx required (Docker 20.10+ or the buildx plugin)" \ && exit 1) - $(eval SCM_VERSION := $(shell python -m setuptools_scm)) -endef - -# OCI labels — mirrors what docker/metadata-action generates in CI -define oci_labels - --label org.opencontainers.image.created=$(BUILD_DATE) \ - --label org.opencontainers.image.revision=$(GIT_SHA) \ - --label org.opencontainers.image.source=$(GIT_SOURCE) \ - --label org.opencontainers.image.version=$(SCM_VERSION) \ - --label org.opencontainers.image.title=$(IMAGE) \ - --label org.opencontainers.image.url=$(GIT_SOURCE) -endef - -define build_args - --build-arg SCM_VERSION=$(SCM_VERSION) \ - --build-arg TF_VERSION=$(TF_VERSION) \ - --build-arg TORCH_COMPUTE=$(TORCH_COMPUTE) endef -define image_tags - -t $(FULL_IMAGE):$(SCM_VERSION) \ - -t $(FULL_IMAGE):sha-$(GIT_SHA_SHORT) \ - -t $(FULL_IMAGE):latest +# SCM_VERSION is a shell variable ($$) so it is evaluated at shell execution +# time, after _check_prereqs has ensured uv is present. +# `uv run --with setuptools_scm` creates a throwaway venv — no system Python +# pollution, no pip permission issues. +define _build + @SCM_VERSION=$$("$(UV)" run --with setuptools_scm --no-project --quiet \ + python3 -m setuptools_scm) \ + && echo "[docker.mk] Building $(FULL_IMAGE):$$SCM_VERSION" \ + && docker buildx build \ + --build-arg SCM_VERSION=$$SCM_VERSION \ + --build-arg TF_VERSION=$(TF_VERSION) \ + --build-arg TORCH_COMPUTE=$(TORCH_COMPUTE) \ + --label "org.opencontainers.image.created=$(BUILD_DATE)" \ + --label "org.opencontainers.image.revision=$(GIT_SHA)" \ + --label "org.opencontainers.image.source=$(GIT_SOURCE)" \ + --label "org.opencontainers.image.version=$$SCM_VERSION" \ + --label "org.opencontainers.image.title=$(IMAGE)" \ + --label "org.opencontainers.image.url=$(GIT_SOURCE)" \ + -t "$(FULL_IMAGE):$$SCM_VERSION" \ + -t "$(FULL_IMAGE):sha-$(GIT_SHA_SHORT)" \ + -t "$(FULL_IMAGE):latest" endef .PHONY: docker docker-gpu docker-push ## Build the CPU image locally (default) docker: - $(call check_prereqs) - docker buildx build \ - $(call build_args) \ - $(call oci_labels) \ - $(call image_tags) \ - . + $(call _check_prereqs) + $(call _build) . -## Build a GPU image locally; override TORCH_COMPUTE if needed (e.g. cu126) +## Build a GPU image; override TORCH_COMPUTE if needed (e.g. cu126) docker-gpu: $(MAKE) -f docker.mk docker TORCH_COMPUTE=$(or $(TORCH_COMPUTE),cu124) -## Build and push the CPU image to GHCR (requires `docker login ghcr.io` first) +## Build and push to GHCR (requires `docker login ghcr.io` first) docker-push: - $(call check_prereqs) - docker buildx build \ - $(call build_args) \ - $(call oci_labels) \ - $(call image_tags) \ - --push \ - . + $(call _check_prereqs) + $(call _build) --push . From 3437f65d589637bd831e7bc96036a80881067aff Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Mon, 6 Jul 2026 13:02:18 -0400 Subject: [PATCH 16/30] docker.mk: sanitize Docker tag by replacing + with - (PEP 440 local versions are invalid in Docker tag format) --- docker.mk | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docker.mk b/docker.mk index 1cd00c6..e0004bc 100644 --- a/docker.mk +++ b/docker.mk @@ -54,7 +54,8 @@ endef define _build @SCM_VERSION=$$("$(UV)" run --with setuptools_scm --no-project --quiet \ python3 -m setuptools_scm) \ - && echo "[docker.mk] Building $(FULL_IMAGE):$$SCM_VERSION" \ + && DOCKER_TAG=$$(echo "$$SCM_VERSION" | tr '+' '-') \ + && echo "[docker.mk] Building $(FULL_IMAGE):$$DOCKER_TAG (package version $$SCM_VERSION)" \ && docker buildx build \ --build-arg SCM_VERSION=$$SCM_VERSION \ --build-arg TF_VERSION=$(TF_VERSION) \ @@ -65,7 +66,7 @@ define _build --label "org.opencontainers.image.version=$$SCM_VERSION" \ --label "org.opencontainers.image.title=$(IMAGE)" \ --label "org.opencontainers.image.url=$(GIT_SOURCE)" \ - -t "$(FULL_IMAGE):$$SCM_VERSION" \ + -t "$(FULL_IMAGE):$$DOCKER_TAG" \ -t "$(FULL_IMAGE):sha-$(GIT_SHA_SHORT)" \ -t "$(FULL_IMAGE):latest" endef From c84825545ee077694e7dd18c4b904ec87c435f12 Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Mon, 6 Jul 2026 13:08:45 -0400 Subject: [PATCH 17/30] Dockerfile: replace multiline python3 -c block with sed (fixed Dockerfile parse error on bare lines) --- Dockerfile | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index f5f40be..3408b4d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -67,15 +67,10 @@ COPY . . # Python 3.11 cannot parse — that file is never imported on 3.11. RUN SETUPTOOLS_SCM_PRETEND_VERSION=${SCM_VERSION} uv pip install --no-build-isolation --no-deps . && \ DIST_PKGS=/usr/local/lib/python${PYTHON_VERSION}/dist-packages && \ - python3 -c " -v = '${SCM_VERSION}' -txt = open('${DIST_PKGS}/scallops/_version.py').read() -import re -txt = re.sub(r\"version = '[^']*'\", f\"version = '{v}'\", txt) -txt = re.sub(r\"__version__ = version = '[^']*'\", f\"__version__ = version = '{v}'\", txt) -open('${DIST_PKGS}/scallops/_version.py', 'w').write(txt) -" && \ - sed -i "s/^Version: .*/Version: ${SCM_VERSION}/" ${DIST_PKGS}/scallops-*.dist-info/METADATA && \ + sed -i "s|version = '[^']*'|version = '${SCM_VERSION}'|g" \ + ${DIST_PKGS}/scallops/_version.py && \ + sed -i "s/^Version: .*/Version: ${SCM_VERSION}/" \ + ${DIST_PKGS}/scallops-*.dist-info/METADATA && \ python -m compileall -q /usr/local/lib/python${PYTHON_VERSION} 2>&1 | \ grep -v 'py312_intrinsics' || true && \ rm -rf /build From 237bcc0a55b05d3c6a808fd3474eb0e544c15d51 Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Mon, 6 Jul 2026 14:00:24 -0400 Subject: [PATCH 18/30] fix version injection in Docker image; add --version flag to CLI Dockerfile: replace fragile sed glob with a Python one-liner that uses importlib.metadata.distribution() to find the exact dist-info path and patches both METADATA (so importlib.metadata.version() returns the right value) and _version.py (so scallops.__version__ is consistent). __main__.py: add --version argparse action so `scallops --version` works; previously the version was only visible buried in --help description text. --- Dockerfile | 6 +----- scallops/__main__.py | 3 +++ 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3408b4d..1e27788 100644 --- a/Dockerfile +++ b/Dockerfile @@ -66,11 +66,7 @@ COPY . . # || true on compileall: torch ships py312_intrinsics.py (PEP 695 syntax) that # Python 3.11 cannot parse — that file is never imported on 3.11. RUN SETUPTOOLS_SCM_PRETEND_VERSION=${SCM_VERSION} uv pip install --no-build-isolation --no-deps . && \ - DIST_PKGS=/usr/local/lib/python${PYTHON_VERSION}/dist-packages && \ - sed -i "s|version = '[^']*'|version = '${SCM_VERSION}'|g" \ - ${DIST_PKGS}/scallops/_version.py && \ - sed -i "s/^Version: .*/Version: ${SCM_VERSION}/" \ - ${DIST_PKGS}/scallops-*.dist-info/METADATA && \ + python3 -c "import importlib.metadata as m,re,pathlib; v='${SCM_VERSION}'; d=m.distribution('scallops'); p=pathlib.Path(d._path); md=p/'METADATA'; md.write_text(re.sub(r'^Version:.*','Version: '+v,md.read_text(),flags=re.M)); vf=p.parent/'scallops'/'_version.py'; vf.exists() and vf.write_text(re.sub(r\"version = '[^']*'\",\"version = '\"+v+\"'\",vf.read_text()))" && \ python -m compileall -q /usr/local/lib/python${PYTHON_VERSION} 2>&1 | \ grep -v 'py312_intrinsics' || true && \ rm -rf /build diff --git a/scallops/__main__.py b/scallops/__main__.py index 83f924e..6147be4 100644 --- a/scallops/__main__.py +++ b/scallops/__main__.py @@ -56,6 +56,9 @@ def create_parsers(default_help: bool = False) -> argparse.ArgumentParser: root_parser = ScallopsArgumentParser( prog="scallops", description=f"Version {version('scallops')}" ) + root_parser.add_argument( + "--version", action="version", version=f"%(prog)s {version('scallops')}" + ) subparsers = root_parser.add_subparsers(help="Command help") pooled_if_sbs_main._create_parser(subparsers, default_help) features_main._create_parser(subparsers, default_help) From 17b23e1bed3bd03a941acf925dc23144c9392450 Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Mon, 6 Jul 2026 14:09:07 -0400 Subject: [PATCH 19/30] fix torch SSL HandshakeFailure on Linux; robust docker.mk version detection Dockerfile: switch torch/torchvision install from uv to pip. uv uses rustls which fails with HandshakeFailure on networks with SSL inspection proxies; pip uses OpenSSL which negotiates TLS more permissively. docker.mk: use `uv run --with setuptools_scm` to resolve the package version. This creates a throwaway venv, avoiding PEP-668 externally-managed-environment errors on Homebrew/system Python without needing pip --user or pipx. Docker tag sanitizes the PEP-440 local separator (+) to (-) for compatibility. --- Dockerfile | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1e27788..b6858d7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,14 +34,16 @@ WORKDIR /build # Each file is its own layer ordered most→least stable for cache efficiency. -# Install CPU-only PyTorch before ufish/cellpose to prevent the multi-GB NVIDIA -# CUDA stack from being pulled in as a transitive dep. -# --allow-insecure-host: download-r2.pytorch.org (PyTorch CDN) is intercepted -# by SSL inspection proxies; proper fix is to inject your proxy CA cert. -RUN uv pip install torch torchvision \ +# Install PyTorch before ufish/cellpose to prevent them pulling in a second +# (CUDA-enabled) copy of torch as a transitive dep. +# pip is used instead of uv here: uv's rustls TLS stack fails with +# HandshakeFailure on networks that use SSL inspection proxies, while pip's +# OpenSSL is more permissive. --trusted-host bypasses cert verification for +# the PyTorch CDN; the proper fix is to inject your proxy CA cert. +RUN pip install --no-cache-dir torch torchvision \ --index-url https://download.pytorch.org/whl/${TORCH_COMPUTE} \ - --allow-insecure-host download.pytorch.org \ - --allow-insecure-host download-r2.pytorch.org + --trusted-host download.pytorch.org \ + --trusted-host download-r2.pytorch.org # ufish: git-pinned tag, rarely bumped COPY requirements.ufish.txt ./ From 31b147a30401f3575faa2e7f2709a2704a2be94f Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Mon, 6 Jul 2026 14:16:55 -0400 Subject: [PATCH 20/30] remove separate torch install step that broke SSL on restricted networks The PyTorch CDN (download-r2.pytorch.org) is blocked by SSL inspection on some networks. The separate pip/uv torch install step was introduced to get CPU-only wheels but caused HandshakeFailure errors on Linux. Removing the step entirely: torch is now installed as a transitive dep of ufish/cellpose from PyPI, which is CUDA-capable by default and works with both CPU and GPU at runtime (--gpus all). Image is larger but builds everywhere without network-specific SSL workarounds. Also removes the unused TORCH_COMPUTE build arg. --- Dockerfile | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/Dockerfile b/Dockerfile index b6858d7..84e37a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,6 @@ # (currently 3.11); changing TF_VERSION may require updating PYTHON_VERSION. ARG TF_VERSION=2.21.0 ARG PYTHON_VERSION=3.11 -# Override at build time for GPU: --build-arg TORCH_COMPUTE=cu124 (or cu126, cu128, etc.) -ARG TORCH_COMPUTE=cpu # SCM_VERSION is passed by: # - CI: .github/workflows/docker.yml (resolved via python -m setuptools_scm) # - locally: docker.mk (make -f docker.mk docker) @@ -34,17 +32,6 @@ WORKDIR /build # Each file is its own layer ordered most→least stable for cache efficiency. -# Install PyTorch before ufish/cellpose to prevent them pulling in a second -# (CUDA-enabled) copy of torch as a transitive dep. -# pip is used instead of uv here: uv's rustls TLS stack fails with -# HandshakeFailure on networks that use SSL inspection proxies, while pip's -# OpenSSL is more permissive. --trusted-host bypasses cert verification for -# the PyTorch CDN; the proper fix is to inject your proxy CA cert. -RUN pip install --no-cache-dir torch torchvision \ - --index-url https://download.pytorch.org/whl/${TORCH_COMPUTE} \ - --trusted-host download.pytorch.org \ - --trusted-host download-r2.pytorch.org - # ufish: git-pinned tag, rarely bumped COPY requirements.ufish.txt ./ RUN uv pip install -r requirements.ufish.txt From 02ed90a8a65e23a823611b68908380aaeac4d2f6 Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Mon, 6 Jul 2026 14:30:59 -0400 Subject: [PATCH 21/30] fix version display: replace fragile python3 one-liner with sed importlib.metadata d._path is a private attribute that fails silently on Ubuntu's dist-packages layout. Use sed with | delimiters (safe for the + and . in PEP 440 versions) to patch both METADATA and _version.py directly. --- Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 84e37a1..8f37c79 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,7 +55,9 @@ COPY . . # || true on compileall: torch ships py312_intrinsics.py (PEP 695 syntax) that # Python 3.11 cannot parse — that file is never imported on 3.11. RUN SETUPTOOLS_SCM_PRETEND_VERSION=${SCM_VERSION} uv pip install --no-build-isolation --no-deps . && \ - python3 -c "import importlib.metadata as m,re,pathlib; v='${SCM_VERSION}'; d=m.distribution('scallops'); p=pathlib.Path(d._path); md=p/'METADATA'; md.write_text(re.sub(r'^Version:.*','Version: '+v,md.read_text(),flags=re.M)); vf=p.parent/'scallops'/'_version.py'; vf.exists() and vf.write_text(re.sub(r\"version = '[^']*'\",\"version = '\"+v+\"'\",vf.read_text()))" && \ + DIST_PKGS=/usr/local/lib/python${PYTHON_VERSION}/dist-packages && \ + sed -i "s|^Version:.*|Version: ${SCM_VERSION}|" ${DIST_PKGS}/scallops-*.dist-info/METADATA && \ + sed -i "s|version = '[^']*'|version = '${SCM_VERSION}'|g" ${DIST_PKGS}/scallops/_version.py && \ python -m compileall -q /usr/local/lib/python${PYTHON_VERSION} 2>&1 | \ grep -v 'py312_intrinsics' || true && \ rm -rf /build From f8a28946553eb42997009848248000ef8cb1c9a1 Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Tue, 7 Jul 2026 11:54:41 -0400 Subject: [PATCH 22/30] adding back isolation for scm to work --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 11adc39..93ea8fd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ ARG TF_VERSION=2.21.0 ARG SCM_VERSION=0.0.0+unknown -FROM --platform=linux/amd64 tensorflow/tensorflow:${TF_VERSION} +FROM tensorflow/tensorflow:${TF_VERSION} ARG SCM_VERSION COPY --from=docker.io/astral/uv:latest /uv /uvx /bin/ @@ -60,7 +60,7 @@ RUN grep -v '^tensorflow' requirements.txt | uv pip install -r /dev/stdin # || true on compileall: torch ships py312_intrinsics.py (PEP 695 syntax) that # Python 3.11 cannot parse — that file is never imported on 3.11. COPY . . -RUN uv pip install --no-build-isolation --no-deps . && \ +RUN uv pip install --no-deps . && \ PYVER=$(python3 -c "import sys; v=sys.version_info; print(f'{v.major}.{v.minor}')") && \ python3 -m compileall -q /usr/local/lib/python${PYVER} 2>&1 | \ grep -v 'py312_intrinsics' || true && \ From 7ee040dd7532d690981a583c852a9140009b8d47 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 15:56:08 +0000 Subject: [PATCH 23/30] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci From 9db972b9958a0bc17e7dfa5f921a726cfac4b639 Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Wed, 8 Jul 2026 09:49:55 -0400 Subject: [PATCH 24/30] fix apt SSL errors: remove unused NVIDIA and deadsnakes PPA repos The TF base image ships NVIDIA and deadsnakes apt sources that fail SSL verification on networks with certificate inspection. These repos are unused: CUDA comes from self-contained PyPI wheels (nvidia-*-cu13), not apt packages, and the Python version is already pinned in the base image. Removing them before any apt-get update eliminates the SSL warnings for all users. --- Dockerfile | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 93ea8fd..9894c70 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,9 +11,16 @@ ARG SCM_VERSION COPY --from=docker.io/astral/uv:latest /uv /uvx /bin/ -# build-essential: needed for mahotas/centrosome (requirements.txt) and the -# Cython extension; git: needed to clone ufish from its pinned tag -RUN apt-get update -qq && \ +# Remove NVIDIA and deadsnakes PPA repos shipped by the TF base image before +# any apt-get update — they fail SSL verification on networks with certificate +# inspection and are not needed (CUDA comes from PyPI wheels, not apt packages). +# build-essential: needed for mahotas/centrosome (requirements.txt) and Cython. +# git: needed to clone ufish from its pinned tag. +RUN rm -f /etc/apt/sources.list.d/cuda.list \ + /etc/apt/sources.list.d/nvidia-ml.list \ + /etc/apt/trusted.gpg.d/cuda-keyring.gpg \ + /etc/apt/sources.list.d/deadsnakes*.list && \ + apt-get update -qq && \ DEBIAN_FRONTEND=noninteractive apt-get install -qq --no-install-recommends -y \ build-essential \ git && \ From 50e348deeab57dbb8da557212e1f627c45a0aaed Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Wed, 8 Jul 2026 09:56:19 -0400 Subject: [PATCH 25/30] docs: add Docker and GPU usage section to install.rst --- docs/install.rst | 52 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/docs/install.rst b/docs/install.rst index 6220c25..7e9e78b 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -37,5 +37,57 @@ Developer Instructions pip install -r requirements.txt -e . +Docker +====== + +Pre-built images are published to the GitHub Container Registry after every +push to ``main`` and on every release tag:: + + docker pull ghcr.io/genentech/scallops:latest + +Run a command:: + + docker run --rm ghcr.io/genentech/scallops:latest scallops --help + +Mount local data into the container:: + + docker run --rm -v /path/to/data:/data ghcr.io/genentech/scallops:latest \ + scallops ... + +Building locally +---------------- + +A ``docker.mk`` file is provided that acts as the local equivalent of the CI +workflow. It runs ``setuptools_scm`` as a preflight step to stamp the correct +version into the image, attaches OCI labels, and tags the image with the +current version:: + + make -f docker.mk docker + +GPU support +----------- + +The default image uses ``tensorflow/tensorflow:2.21.0`` (CPU variant) as its +base. **PyTorch** (used by cellpose and U-FISH) is installed from PyPI and +ships CUDA-capable wheels — it will automatically use an NVIDIA GPU at runtime +if one is available. **TensorFlow** runs on CPU only in the default image. + +To enable GPU acceleration for TensorFlow as well, build with the GPU base +image:: + + make -f docker.mk docker TF_VERSION=2.21.0-gpu + +At runtime, pass ``--gpus all`` (Docker) or the equivalent Podman flag and +ensure the `NVIDIA Container Toolkit`_ is installed on the host:: + + docker run --gpus all --rm ghcr.io/genentech/scallops:latest scallops ... + +.. note:: + + GPU containers require NVIDIA drivers on the host machine. The image + itself does not need to change — the same image works on CPU-only and + GPU-equipped hosts; PyTorch selects the appropriate backend automatically. + .. _Mamba: https://mamba.readthedocs.io/en/latest/installation.html .. _Conda: https://docs.conda.io/projects/conda/en/latest/user-guide/install/index.html +.. _NVIDIA Container Toolkit: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html From 080798be643677b38faaf60e7707e184b1426e70 Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Wed, 8 Jul 2026 10:04:11 -0400 Subject: [PATCH 26/30] docs: clarify TF GPU affects segment command (StarDist backend) --- docs/install.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/install.rst b/docs/install.rst index 7e9e78b..2ca44e5 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -70,7 +70,9 @@ GPU support The default image uses ``tensorflow/tensorflow:2.21.0`` (CPU variant) as its base. **PyTorch** (used by cellpose and U-FISH) is installed from PyPI and ships CUDA-capable wheels — it will automatically use an NVIDIA GPU at runtime -if one is available. **TensorFlow** runs on CPU only in the default image. +if one is available. **TensorFlow** runs on CPU only in the default image; +this affects the ``segment`` command when using the StarDist backend, which +relies on TensorFlow for model inference. To enable GPU acceleration for TensorFlow as well, build with the GPU base image:: From 133e285bd42c0ad7c847114521d828e679ac89ae Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Wed, 8 Jul 2026 14:31:14 -0400 Subject: [PATCH 27/30] suppress compileall SyntaxError noise; warn when SCM_VERSION not set - Replace grep filter (only caught first line of multi-line error) with 2>/dev/null so the py312_intrinsics SyntaxError block is fully silenced - Print a clear WARNING in build logs when SCM_VERSION is the default 0.0.0+unknown so users know to use make -f docker.mk docker --- Dockerfile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9894c70..78c17d4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -67,10 +67,11 @@ RUN grep -v '^tensorflow' requirements.txt | uv pip install -r /dev/stdin # || true on compileall: torch ships py312_intrinsics.py (PEP 695 syntax) that # Python 3.11 cannot parse — that file is never imported on 3.11. COPY . . -RUN uv pip install --no-deps . && \ +RUN [ "${SCM_VERSION}" = "0.0.0+unknown" ] && \ + echo "WARNING: SCM_VERSION not set — version will be 0.0.0+unknown. Use 'make -f docker.mk docker' to stamp the correct version." || true && \ + uv pip install --no-deps . && \ PYVER=$(python3 -c "import sys; v=sys.version_info; print(f'{v.major}.{v.minor}')") && \ - python3 -m compileall -q /usr/local/lib/python${PYVER} 2>&1 | \ - grep -v 'py312_intrinsics' || true && \ + python3 -m compileall -q /usr/local/lib/python${PYVER} 2>/dev/null || true && \ rm -rf /build ENV AWS_RETRY_MODE=adaptive \ From 00b610bde6a6fc366a82cf0e77e2c81917625381 Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Wed, 8 Jul 2026 16:27:02 -0400 Subject: [PATCH 28/30] fix compileall: redirect both stdout and stderr to silence py312 error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit compileall prints '*** Error compiling...' to stdout and the SyntaxError details to stderr — 2>/dev/null only suppressed the latter. Use >/dev/null 2>&1 to silence both streams completely. --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 78c17d4..b19aeb3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -71,7 +71,7 @@ RUN [ "${SCM_VERSION}" = "0.0.0+unknown" ] && \ echo "WARNING: SCM_VERSION not set — version will be 0.0.0+unknown. Use 'make -f docker.mk docker' to stamp the correct version." || true && \ uv pip install --no-deps . && \ PYVER=$(python3 -c "import sys; v=sys.version_info; print(f'{v.major}.{v.minor}')") && \ - python3 -m compileall -q /usr/local/lib/python${PYVER} 2>/dev/null || true && \ + python3 -m compileall -q /usr/local/lib/python${PYVER} >/dev/null 2>&1 || true && \ rm -rf /build ENV AWS_RETRY_MODE=adaptive \ From 34e8c429b178b2c188ac899bca4c9b966e64b054 Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Mon, 13 Jul 2026 09:39:04 -0400 Subject: [PATCH 29/30] add IS_GPU env var, RAPIDS support, robust uv fallback in docker.mk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dockerfile: - ARG IS_GPU (default 0) lifted into ENV IS_GPU so it persists across all RUN steps and is inspectable in running containers - IS_GPU controls NVIDIA apt repo retention (kept for GPU, removed for CPU) and RAPIDS installation (GPU only) - RAPIDS (cuDF, cuML, dask-cudf) installed from pypi.nvidia.com when IS_GPU=1; build fails with a clear error if RAPIDS_VERSION is not set for GPU builds - ARG/ENV ordering cleaned up: IS_GPU declared after FROM (not needed before) docker.mk: - IS_GPU derived automatically from TF_VERSION via findstring — users never need to pass it explicitly - docker-gpu target validates RAPIDS_VERSION before starting the build - _check_prereqs: after failed uv install attempt, verify success and print a clear error with the manual install URL if everything fails - RAPIDS_VERSION, RAPIDS_CUDA, IS_GPU forwarded as build-args in _build --- Dockerfile | 55 ++++++++++++++++++++++++++++++++++++++++++++---------- docker.mk | 49 ++++++++++++++++++++++++++++++++++++------------ 2 files changed, 82 insertions(+), 22 deletions(-) diff --git a/Dockerfile b/Dockerfile index b19aeb3..4b455eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,46 @@ # syntax=docker/dockerfile:1 # SCM_VERSION is passed by: # - CI: .github/workflows/docker.yml (resolved via python -m setuptools_scm) -# - locally: docker.mk (make -f docker.mk docker) +# - locally: docker.mk (make -f docker.mk docker / docker-gpu) # Falls back to 0.0.0+unknown when building directly with docker/podman build . +# +# GPU build (TF-GPU + RAPIDS): +# make -f docker.mk docker-gpu RAPIDS_VERSION=25.06 +# CPU build (default): +# make -f docker.mk docker ARG TF_VERSION=2.21.0 ARG SCM_VERSION=0.0.0+unknown +# RAPIDS: required when IS_GPU=1, ignored when IS_GPU=0. +# See https://docs.rapids.ai/install for current release versions. +ARG RAPIDS_VERSION= +ARG RAPIDS_CUDA=cu12 FROM tensorflow/tensorflow:${TF_VERSION} ARG SCM_VERSION +ARG RAPIDS_VERSION +ARG RAPIDS_CUDA +# IS_GPU is derived from TF_VERSION by docker.mk (1 when TF_VERSION contains +# -gpu, 0 otherwise). Docker ENV cannot evaluate shell expressions, so ARG is +# the necessary bridge: docker.mk computes it, ARG captures it, ENV persists it. +# Default 0 (CPU) when building directly without docker.mk. +ARG IS_GPU=0 +ENV IS_GPU=${IS_GPU} COPY --from=docker.io/astral/uv:latest /uv /uvx /bin/ -# Remove NVIDIA and deadsnakes PPA repos shipped by the TF base image before -# any apt-get update — they fail SSL verification on networks with certificate -# inspection and are not needed (CUDA comes from PyPI wheels, not apt packages). +# CPU builds: remove NVIDIA and deadsnakes apt repos — they fail SSL +# verification on restricted networks and are unused (CUDA comes from PyPI +# wheels, not apt). +# GPU builds: keep the NVIDIA repos (the GPU base image uses them) but still +# remove deadsnakes which is never needed. # build-essential: needed for mahotas/centrosome (requirements.txt) and Cython. # git: needed to clone ufish from its pinned tag. -RUN rm -f /etc/apt/sources.list.d/cuda.list \ - /etc/apt/sources.list.d/nvidia-ml.list \ - /etc/apt/trusted.gpg.d/cuda-keyring.gpg \ - /etc/apt/sources.list.d/deadsnakes*.list && \ +RUN if [ "$IS_GPU" = "0" ]; then \ + rm -f /etc/apt/sources.list.d/cuda.list \ + /etc/apt/sources.list.d/nvidia-ml.list \ + /etc/apt/trusted.gpg.d/cuda-keyring.gpg; \ + fi && \ + rm -f /etc/apt/sources.list.d/deadsnakes*.list && \ apt-get update -qq && \ DEBIAN_FRONTEND=noninteractive apt-get install -qq --no-install-recommends -y \ build-essential \ @@ -59,13 +80,27 @@ RUN uv pip install pysam dask-ml miniwdl COPY requirements.txt ./ RUN grep -v '^tensorflow' requirements.txt | uv pip install -r /dev/stdin +# RAPIDS: GPU-accelerated drop-in replacements for pandas (cuDF), scikit-learn +# (cuML), and dask (dask-cudf). Only installed when IS_GPU=1. +# For CPU builds this step is a no-op. +RUN if [ "$IS_GPU" = "1" ]; then \ + if [ -z "${RAPIDS_VERSION}" ]; then \ + echo "ERROR: RAPIDS_VERSION is required for GPU builds." \ + "Pass --build-arg RAPIDS_VERSION=" \ + "(see https://docs.rapids.ai/install)" && exit 1; \ + fi && \ + pip install --no-cache-dir \ + --extra-index-url https://pypi.nvidia.com \ + cudf-${RAPIDS_CUDA}==${RAPIDS_VERSION} \ + cuml-${RAPIDS_CUDA}==${RAPIDS_VERSION} \ + dask-cudf-${RAPIDS_CUDA}==${RAPIDS_VERSION}; \ + fi + # --no-deps: all deps already installed above; avoids re-downloading tensorflow. # SETUPTOOLS_SCM_PRETEND_VERSION (set via ENV above) ensures setuptools_scm # writes the correct version into _version.py and the wheel metadata. # PYTHON_VERSION is inferred dynamically from the interpreter so it always # matches whatever Python the TF base image ships — no ARG to keep in sync. -# || true on compileall: torch ships py312_intrinsics.py (PEP 695 syntax) that -# Python 3.11 cannot parse — that file is never imported on 3.11. COPY . . RUN [ "${SCM_VERSION}" = "0.0.0+unknown" ] && \ echo "WARNING: SCM_VERSION not set — version will be 0.0.0+unknown. Use 'make -f docker.mk docker' to stamp the correct version." || true && \ diff --git a/docker.mk b/docker.mk index e0004bc..32955f6 100644 --- a/docker.mk +++ b/docker.mk @@ -11,18 +11,30 @@ # - Automatic push to ghcr.io — use `make -f docker.mk docker-push` explicitly # # Usage: -# make -f docker.mk docker # CPU image (default) -# make -f docker.mk docker-gpu TORCH_COMPUTE=cu124 # GPU image -# make -f docker.mk docker-push # build CPU + push to GHCR +# make -f docker.mk docker # CPU image (default) +# make -f docker.mk docker-gpu RAPIDS_VERSION=25.06 # GPU image + RAPIDS +# make -f docker.mk docker-push # build CPU + push to GHCR +# make -f docker.mk docker-push TF_VERSION=2.21.0-gpu \ +# RAPIDS_VERSION=25.06 # build GPU + push # ── configurable knobs ──────────────────────────────────────────────────────── REGISTRY ?= ghcr.io/genentech IMAGE ?= scallops TF_VERSION ?= 2.21.0 -TORCH_COMPUTE ?= cpu +# RAPIDS_VERSION: required for GPU builds, ignored for CPU builds. +# See https://docs.rapids.ai/install for current release versions. +RAPIDS_VERSION ?= +RAPIDS_CUDA ?= cu12 + +# ── derived flags ───────────────────────────────────────────────────────────── +# IS_GPU is computed from TF_VERSION so the Dockerfile never needs it passed +# explicitly by the user. Docker ENV cannot evaluate shell expressions, so +# docker.mk is the place to derive it. +IS_GPU := $(if $(findstring -gpu,$(TF_VERSION)),1,0) + +FULL_IMAGE := $(REGISTRY)/$(IMAGE) # ── uv: prefer PATH, fall back to the default install location ──────────────── -# If uv is not found, _check_prereqs installs it there automatically. UV := $(shell which uv 2>/dev/null || echo "$(HOME)/.local/bin/uv") # ── values computed at parse time (none depend on setuptools_scm) ───────────── @@ -31,7 +43,6 @@ GIT_SHA_SHORT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown GIT_SOURCE := $(shell git remote get-url origin 2>/dev/null \ | sed 's|git@github.com:|https://github.com/|; s|\.git$$||') BUILD_DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ) -FULL_IMAGE := $(REGISTRY)/$(IMAGE) # ── internal helpers ────────────────────────────────────────────────────────── @@ -40,8 +51,12 @@ FULL_IMAGE := $(REGISTRY)/$(IMAGE) # errors on Homebrew/system Python installs. define _check_prereqs @"$(UV)" --version >/dev/null 2>&1 \ - || (echo "[docker.mk] uv not found — installing via official installer..." \ - && curl -LsSf https://astral.sh/uv/install.sh | sh) + || (echo "[docker.mk] uv not found at $(UV) — installing via official installer..." \ + && curl -LsSf https://astral.sh/uv/install.sh | sh \ + && "$(UV)" --version >/dev/null 2>&1) \ + || (echo "[docker.mk] ERROR: uv not found and installation failed." \ + "Install manually: https://docs.astral.sh/uv/getting-started/installation/" \ + && exit 1) @docker buildx version >/dev/null 2>&1 \ || (echo "[docker.mk] ERROR: docker buildx required (Docker 20.10+ or the buildx plugin)" \ && exit 1) @@ -51,15 +66,19 @@ endef # time, after _check_prereqs has ensured uv is present. # `uv run --with setuptools_scm` creates a throwaway venv — no system Python # pollution, no pip permission issues. +# IS_GPU and RAPIDS args are always passed; the Dockerfile ignores RAPIDS when +# IS_GPU=0 so there is no penalty for always forwarding them. define _build @SCM_VERSION=$$("$(UV)" run --with setuptools_scm --no-project --quiet \ python3 -m setuptools_scm) \ && DOCKER_TAG=$$(echo "$$SCM_VERSION" | tr '+' '-') \ - && echo "[docker.mk] Building $(FULL_IMAGE):$$DOCKER_TAG (package version $$SCM_VERSION)" \ + && echo "[docker.mk] Building $(FULL_IMAGE):$$DOCKER_TAG (IS_GPU=$(IS_GPU), package version $$SCM_VERSION)" \ && docker buildx build \ --build-arg SCM_VERSION=$$SCM_VERSION \ --build-arg TF_VERSION=$(TF_VERSION) \ - --build-arg TORCH_COMPUTE=$(TORCH_COMPUTE) \ + --build-arg IS_GPU=$(IS_GPU) \ + --build-arg RAPIDS_VERSION=$(RAPIDS_VERSION) \ + --build-arg RAPIDS_CUDA=$(RAPIDS_CUDA) \ --label "org.opencontainers.image.created=$(BUILD_DATE)" \ --label "org.opencontainers.image.revision=$(GIT_SHA)" \ --label "org.opencontainers.image.source=$(GIT_SOURCE)" \ @@ -78,9 +97,15 @@ docker: $(call _check_prereqs) $(call _build) . -## Build a GPU image; override TORCH_COMPUTE if needed (e.g. cu126) +## Build the GPU image (TF-GPU base + RAPIDS). RAPIDS_VERSION is required. +## Example: make -f docker.mk docker-gpu RAPIDS_VERSION=25.06 docker-gpu: - $(MAKE) -f docker.mk docker TORCH_COMPUTE=$(or $(TORCH_COMPUTE),cu124) + @[ -n "$(RAPIDS_VERSION)" ] \ + || (echo "[docker.mk] ERROR: RAPIDS_VERSION is required for GPU builds." \ + "Example: make -f docker.mk docker-gpu RAPIDS_VERSION=25.06" \ + "(see https://docs.rapids.ai/install)" && exit 1) + $(call _check_prereqs) + $(call _build) . ## Build and push to GHCR (requires `docker login ghcr.io` first) docker-push: From a3139bc130e57b2a5e344ff81a604d03e6c45e3d Mon Sep 17 00:00:00 2001 From: Jose Sergio Hleap Date: Mon, 13 Jul 2026 11:36:55 -0400 Subject: [PATCH 30/30] docs: update Docker/GPU section with RAPIDS, IS_GPU, and affected commands - Replace prose GPU description with a table showing which library uses GPU, which commands are affected, and whether the default or GPU image is needed - Document docker-gpu target and required RAPIDS_VERSION arg - Explain IS_GPU env var and how to inspect it at runtime - Add RAPIDS and current RAPIDS release links --- docs/install.rst | 56 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/docs/install.rst b/docs/install.rst index 2ca44e5..36d6e34 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -68,28 +68,56 @@ GPU support ----------- The default image uses ``tensorflow/tensorflow:2.21.0`` (CPU variant) as its -base. **PyTorch** (used by cellpose and U-FISH) is installed from PyPI and -ships CUDA-capable wheels — it will automatically use an NVIDIA GPU at runtime -if one is available. **TensorFlow** runs on CPU only in the default image; -this affects the ``segment`` command when using the StarDist backend, which -relies on TensorFlow for model inference. - -To enable GPU acceleration for TensorFlow as well, build with the GPU base -image:: - - make -f docker.mk docker TF_VERSION=2.21.0-gpu +base. The table below summarises which libraries use the GPU and which +commands are affected: + +.. list-table:: + :header-rows: 1 + :widths: 20 20 35 25 + + * - Library + - GPU in default image + - Affected commands + - Notes + * - PyTorch + - **Yes** (auto-detected) + - ``segment`` (cellpose backend), ``dialout`` (U-FISH) + - PyPI wheels are CUDA-capable; GPU is used automatically when available + * - TensorFlow + - No (CPU only) + - ``segment`` (StarDist backend) + - Requires the GPU image (see below) + * - RAPIDS + - No (CPU only) + - All commands using pandas / scikit-learn / dask internally + - Requires the GPU image (see below) + +**GPU image** — enables TensorFlow GPU and installs `RAPIDS`_ (cuDF, cuML, +dask-cudf), which accelerate the data-science operations used throughout the +pipeline. ``RAPIDS_VERSION`` must match a `current RAPIDS release`_:: + + make -f docker.mk docker-gpu RAPIDS_VERSION=25.06 + +The build automatically sets the ``IS_GPU`` environment variable to ``1`` +inside the container. You can inspect it at runtime to confirm GPU support +was compiled in:: + + docker run --rm ghcr.io/genentech/scallops:latest printenv IS_GPU At runtime, pass ``--gpus all`` (Docker) or the equivalent Podman flag and ensure the `NVIDIA Container Toolkit`_ is installed on the host:: - docker run --gpus all --rm ghcr.io/genentech/scallops:latest scallops ... + docker run --gpus all --rm scallops segment ... .. note:: - GPU containers require NVIDIA drivers on the host machine. The image - itself does not need to change — the same image works on CPU-only and - GPU-equipped hosts; PyTorch selects the appropriate backend automatically. + NVIDIA drivers must be installed on the host — the container image itself + does not include drivers. PyTorch selects the GPU backend automatically; + TensorFlow and RAPIDS require the GPU image built with + ``TF_VERSION=2.21.0-gpu``. .. _Mamba: https://mamba.readthedocs.io/en/latest/installation.html .. _Conda: https://docs.conda.io/projects/conda/en/latest/user-guide/install/index.html .. _NVIDIA Container Toolkit: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html +.. _RAPIDS: https://rapids.ai +.. _current RAPIDS release: https://docs.rapids.ai/install