diff --git a/.circleci/config.yml b/.circleci/config.yml index 190bf59f..f04ba1ac 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,33 +10,19 @@ jobs: steps: - checkout - - restore_cache: - keys: - - env-v6-{{ .Branch }}- - - env-v6-master- - - env-v6- + - run: + name: Ensure uv is present + command: uv -V || curl -LsSf https://astral.sh/uv/install.sh | sh - run: name: Setup git-annex command: | - sudo apt update && sudo apt-get install apt-transport-https ca-certificates -y && sudo update-ca-certificates - if [[ ! -d /opt/circleci/git-annex.linux ]]; then - cd /tmp - wget https://downloads.kitenet.net/git-annex/linux/current/git-annex-standalone-amd64.tar.gz - tar xzf git-annex-standalone-amd64.tar.gz -C /opt/circleci/ - fi + uv tool install git-annex git config --global user.name 'NiPy' git config --global user.email 'nipreps@gmail.com' - run: name: Setup DataLad command: | - python3 -m pip install --no-cache-dir -U pip "setuptools >= 45.0" "setuptools_scm[toml] >= 6.2" - python3 -m pip install --no-cache-dir -U datalad datalad-osf - - - save_cache: - key: env-v6-{{ .Branch }}-{{ .BuildNum }} - paths: - - /opt/circleci/git-annex.linux - - /opt/circleci/.pyenv/versions + uv tool install --with-executables-from=datalad-osf,datalad-next datalad - restore_cache: keys: @@ -46,9 +32,6 @@ jobs: - run: name: Install test data from GIN command: | - export PATH=/opt/circleci/git-annex.linux:$PATH - pyenv local 3 - eval "$(pyenv init --path)" mkdir -p /tmp/data cd /tmp/data datalad install -r https://gin.g-node.org/oesteban/nitransforms-tests @@ -61,10 +44,10 @@ jobs: - restore_cache: keys: - - build-v1-{{ .Branch }}-{{ epoch }} - - build-v1-{{ .Branch }}- - - build-v1-master- - - build-v1- + - build-v2-{{ .Branch }}-{{ epoch }} + - build-v2-{{ .Branch }}- + - build-v2-master- + - build-v2- paths: - /tmp/docker - run: @@ -81,13 +64,13 @@ jobs: set -e if [[ "$success" = "0" ]]; then echo "Pulling from local registry" - docker tag localhost:5000/ubuntu ubuntu:xenial-20200114 + docker tag localhost:5000/ubuntu ubuntu:jammy-20250730 docker pull localhost:5000/nitransforms docker tag localhost:5000/nitransforms nitransforms:latest else echo "Pulling from Docker Hub" - docker pull ubuntu:xenial-20200114 - docker tag ubuntu:xenial-20200114 localhost:5000/ubuntu + docker pull ubuntu:jammy-20250730 + docker tag ubuntu:jammy-20250730 localhost:5000/ubuntu docker push localhost:5000/ubuntu fi - run: @@ -99,7 +82,7 @@ jobs: -t nitransforms:latest \ --build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \ --build-arg VCS_REF=`git rev-parse --short HEAD` \ - --build-arg VERSION=$( python3 -m setuptools_scm ) . \ + --build-arg VERSION=$( uv run --no-project -w setuptools_scm -m setuptools_scm ) . \ && e=0 && break || sleep 15 done && [ "$e" -eq "0" ] docker tag nitransforms:latest localhost:5000/nitransforms @@ -110,13 +93,13 @@ jobs: docker exec -it registry /bin/registry garbage-collect --delete-untagged \ /etc/docker/registry/config.yml - save_cache: - key: build-v1-{{ .Branch }}-{{ epoch }} + key: build-v2-{{ .Branch }}-{{ epoch }} paths: - /tmp/docker - run: name: Check version packaged in Docker image command: | - THISVERSION=${CIRCLE_TAG:-$(python3 -m setuptools_scm)} + THISVERSION=${CIRCLE_TAG:-$( uv run --no-project -w setuptools_scm -m setuptools_scm )} INSTALLED_VERSION=$(\ docker run -it --rm --entrypoint=python nitransforms \ -c 'import nitransforms as nit; print(nit.__version__, end="")' ) @@ -141,10 +124,11 @@ jobs: -w /src/nitransforms -v $PWD:/src/nitransforms \ -v /tmp/data/nitransforms-tests:/data -e TEST_DATA_HOME=/data \ -e COVERAGE_FILE=/tmp/coverage/.pytest.coverage \ - -v /tmp/fslicense/license.txt:/opt/freesurfer/license.txt:ro \ + -v /tmp/fslicense/license.txt:/usr/local/freesurfer/license.txt:ro \ -v /tmp/tests:/tmp nitransforms:latest \ pytest --junit-xml=/tmp/summaries/pytest.xml \ --cov nitransforms --cov-report xml:/tmp/coverage/unittests.xml \ + -n auto \ nitransforms/ - run: name: Submit unit test coverage diff --git a/Dockerfile b/Dockerfile index a1c5f4b2..6ecdd22b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,22 @@ # Ubuntu 22.04 LTS - Jammy -ARG BASE_IMAGE=ubuntu:jammy-20240125 +ARG BASE_IMAGE=ubuntu:jammy-20250730 # # Build wheel # -FROM python:slim AS src -RUN pip install build -RUN apt-get update && \ - apt-get install -y --no-install-recommends git +FROM ghcr.io/astral-sh/uv:python3.13-alpine AS src +RUN apk add git COPY . /src -RUN python -m build /src +RUN uv build --wheel /src # # Download stages # # Utilities for downloading packages -FROM ${BASE_IMAGE} as downloader +FROM ${BASE_IMAGE} AS downloader # Bump the date to current to refresh curl/certificates/etc -RUN echo "2023.07.20" +RUN echo "2025.09.25" RUN apt-get update && \ apt-get install -y --no-install-recommends \ binutils \ @@ -30,54 +28,16 @@ RUN apt-get update && \ RUN update-ca-certificates -f -# FreeSurfer 7.3.2 -FROM downloader as freesurfer -COPY docker/files/freesurfer7.3.2-exclude.txt /usr/local/etc/freesurfer7.3.2-exclude.txt -COPY docker/files/fs-cert.pem /usr/local/etc/fs-cert.pem -RUN curl --cacert /usr/local/etc/fs-cert.pem \ - -sSL https://surfer.nmr.mgh.harvard.edu/pub/dist/freesurfer/7.3.2/freesurfer-linux-ubuntu22_amd64-7.3.2.tar.gz \ - | tar zxv --no-same-owner -C /opt --exclude-from=/usr/local/etc/freesurfer7.3.2-exclude.txt - -# AFNI -FROM downloader as afni -# Bump the date to current to update AFNI -RUN echo "2023.07.20" -RUN mkdir -p /opt/afni-latest \ - && curl -fsSL --retry 5 https://afni.nimh.nih.gov/pub/dist/tgz/linux_openmp_64.tgz \ - | tar -xz -C /opt/afni-latest --strip-components 1 \ - --exclude "linux_openmp_64/*.gz" \ - --exclude "linux_openmp_64/funstuff" \ - --exclude "linux_openmp_64/shiny" \ - --exclude "linux_openmp_64/afnipy" \ - --exclude "linux_openmp_64/lib/RetroTS" \ - --exclude "linux_openmp_64/lib_RetroTS" \ - --exclude "linux_openmp_64/meica.libs" \ - # Keep only what we use - && find /opt/afni-latest -type f -not \( \ - -name "3dTshift" -or \ - -name "3dUnifize" -or \ - -name "3dAutomask" -or \ - -name "3dvolreg" -or \ - -name "3dNwarpApply" \ - \) -delete - # Micromamba -FROM downloader as micromamba - -# Install a C compiler to build extensions when needed. -# traits<6.4 wheels are not available for Python 3.11+, but build easily. -RUN apt-get update && \ - apt-get install -y --no-install-recommends build-essential && \ - apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* +FROM downloader AS micromamba WORKDIR / # Bump the date to current to force update micromamba -RUN echo "2024.02.06" +RUN echo "2025.09.05" RUN curl -Ls https://micro.mamba.pm/api/micromamba/linux-64/latest | tar -xvj bin/micromamba ENV MAMBA_ROOT_PREFIX="/opt/conda" COPY env.yml /tmp/env.yml -# COPY requirements.txt /tmp/requirements.txt WORKDIR /tmp RUN micromamba create -y -f /tmp/env.yml && \ micromamba clean -y -a @@ -85,94 +45,54 @@ RUN micromamba create -y -f /tmp/env.yml && \ # # Main stage # -FROM ${BASE_IMAGE} as nitransforms +FROM ${BASE_IMAGE} AS nitransforms # Configure apt ENV DEBIAN_FRONTEND="noninteractive" \ LANG="en_US.UTF-8" \ LC_ALL="en_US.UTF-8" -# Some baseline tools; bc is needed for FreeSurfer, so don't drop it RUN apt-get update && \ apt-get install -y --no-install-recommends \ - bc \ - ca-certificates \ - curl \ - git \ - gnupg \ - lsb-release \ - netbase \ - xvfb && \ + libexpat1 \ + libgomp1 \ + && \ apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* -# Configure PPAs for libpng12 and libxp6 -RUN GNUPGHOME=/tmp gpg --keyserver hkps://keyserver.ubuntu.com --no-default-keyring --keyring /usr/share/keyrings/linuxuprising.gpg --recv 0xEA8CACC073C3DB2A \ - && GNUPGHOME=/tmp gpg --keyserver hkps://keyserver.ubuntu.com --no-default-keyring --keyring /usr/share/keyrings/zeehio.gpg --recv 0xA1301338A3A48C4A \ - && echo "deb [signed-by=/usr/share/keyrings/linuxuprising.gpg] https://ppa.launchpadcontent.net/linuxuprising/libpng12/ubuntu jammy main" > /etc/apt/sources.list.d/linuxuprising.list \ - && echo "deb [signed-by=/usr/share/keyrings/zeehio.gpg] https://ppa.launchpadcontent.net/zeehio/libxp/ubuntu jammy main" > /etc/apt/sources.list.d/zeehio.list - -# Dependencies for AFNI; requires a discontinued multiarch-support package from bionic (18.04) -RUN apt-get update -qq \ - && apt-get install -y -q --no-install-recommends \ - ed \ - gsl-bin \ - libglib2.0-0 \ - libglu1-mesa-dev \ - libglw1-mesa \ - libgomp1 \ - libjpeg62 \ - libpng12-0 \ - libxm4 \ - libxp6 \ - netpbm \ - tcsh \ - xfonts-base \ - xvfb \ - && curl -sSL --retry 5 -o /tmp/multiarch.deb http://archive.ubuntu.com/ubuntu/pool/main/g/glibc/multiarch-support_2.27-3ubuntu1.5_amd64.deb \ - && dpkg -i /tmp/multiarch.deb \ - && rm /tmp/multiarch.deb \ - && apt-get install -f \ - && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \ - && gsl2_path="$(find / -name 'libgsl.so.19' || printf '')" \ - && if [ -n "$gsl2_path" ]; then \ - ln -sfv "$gsl2_path" "$(dirname $gsl2_path)/libgsl.so.0"; \ - fi \ - && ldconfig - -# Install files from stages -COPY --from=freesurfer /opt/freesurfer /opt/freesurfer -COPY --from=afni /opt/afni-latest /opt/afni-latest +# Install FreeSurfer and AFNI bins from images +COPY --from=freesurfer/freesurfer:7.4.1 \ + /usr/local/freesurfer/bin/mri_vol2vol \ + /usr/local/freesurfer/bin/ +COPY --from=afni/afni_make_build:AFNI_25.2.09 \ + /opt/afni/install/libf2c.so \ + /opt/afni/install/libmri.so \ + /usr/local/lib/ +COPY --from=afni/afni_make_build:AFNI_25.2.09 \ + /opt/afni/install/3dAllineate \ + /opt/afni/install/3dNwarpApply \ + /opt/afni/install/3dWarp \ + /opt/afni/install/3drefit \ + /opt/afni/install/3dvolreg \ + /usr/local/bin/ # Simulate SetUpFreeSurfer.sh ENV OS="Linux" \ FS_OVERRIDE=0 \ FIX_VERTEX_AREA="" \ FSF_OUTPUT_FORMAT="nii.gz" \ - FREESURFER_HOME="/opt/freesurfer" + FREESURFER_HOME="/usr/local/freesurfer" ENV SUBJECTS_DIR="$FREESURFER_HOME/subjects" \ FUNCTIONALS_DIR="$FREESURFER_HOME/sessions" \ - MNI_DIR="$FREESURFER_HOME/mni" \ LOCAL_DIR="$FREESURFER_HOME/local" \ - MINC_BIN_DIR="$FREESURFER_HOME/mni/bin" \ - MINC_LIB_DIR="$FREESURFER_HOME/mni/lib" \ - MNI_DATAPATH="$FREESURFER_HOME/mni/data" -ENV PERL5LIB="$MINC_LIB_DIR/perl5/5.8.5" \ - MNI_PERL5LIB="$MINC_LIB_DIR/perl5/5.8.5" \ - PATH="$FREESURFER_HOME/bin:$FREESURFER_HOME/tktools:$MINC_BIN_DIR:$PATH" + PATH="$FREESURFER_HOME/bin:$PATH" # AFNI config -ENV PATH="/opt/afni-latest:$PATH" \ - AFNI_IMSAVE_WARNINGS="NO" \ - AFNI_PLUGINPATH="/opt/afni-latest" - -# Workbench config -ENV PATH="/opt/workbench/bin_linux64:$PATH" +ENV AFNI_IMSAVE_WARNINGS="NO" # Create a shared $HOME directory RUN useradd -m -s /bin/bash -G users neuro WORKDIR /home/neuro -ENV HOME="/home/neuro" \ - LD_LIBRARY_PATH="/usr/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH" +ENV HOME="/home/neuro" COPY --from=micromamba /bin/micromamba /bin/micromamba COPY --from=micromamba /opt/conda/envs/nitransforms /opt/conda/envs/nitransforms @@ -180,9 +100,7 @@ COPY --from=micromamba /opt/conda/envs/nitransforms /opt/conda/envs/nitransforms ENV MAMBA_ROOT_PREFIX="/opt/conda" RUN micromamba shell init -s bash && \ echo "micromamba activate nitransforms" >> $HOME/.bashrc -ENV PATH="/opt/conda/envs/nitransforms/bin:$PATH" \ - CPATH="/opt/conda/envs/nitransforms/include:$CPATH" \ - LD_LIBRARY_PATH="/opt/conda/envs/nitransforms/lib:$LD_LIBRARY_PATH" +ENV PATH="/opt/conda/envs/nitransforms/bin:$PATH" # FSL environment ENV LANG="C.UTF-8" \ @@ -196,22 +114,10 @@ ENV LANG="C.UTF-8" \ FSLREMOTECALL="" \ FSLGECUDAQ="cuda.q" -# Unless otherwise specified each process should only use one thread - nipype -# will handle parallelization -ENV MKL_NUM_THREADS=1 \ - OMP_NUM_THREADS=1 - # Install package -# CRITICAL: Make sure python setup.py --version has been run at least once -# outside the container, with access to the git history. COPY --from=src /src/dist/*.whl . RUN python -m pip install --no-cache-dir $( ls *.whl )[all] - -RUN find $HOME -type d -exec chmod go=u {} + && \ - find $HOME -type f -exec chmod go=u {} + && \ - rm -rf $HOME/.npm $HOME/.conda $HOME/.empty - RUN ldconfig WORKDIR /tmp/ diff --git a/env.yml b/env.yml index d550959b..0990b16a 100644 --- a/env.yml +++ b/env.yml @@ -2,43 +2,13 @@ name: nitransforms channels: - https://fsl.fmrib.ox.ac.uk/fsldownloads/fslconda/public/ - conda-forge -# Update this ~yearly; last updated Jan 2024 +# Update this ~yearly; last updated Sep 2025 dependencies: - - python=3.11 - # Needed for svgo and bids-validator; consider moving to deno - - nodejs=20 - # Intel Math Kernel Library for numpy - - mkl=2023.2.0 - - mkl-service=2.4.0 - # git-annex for templateflow users with DataLad superdatasets - - git-annex=*=alldep* - # ANTs 2.5.3 is linked against libitk 5.4 - let's pin both there - - libitk=5.4 - # Base scientific python stack; required by FSL, so pinned here - - numpy=1.26 - - scipy=1.11 - - matplotlib=3.8 - - pandas=2.2 - - h5py=3.10 - # Dependencies compiled against numpy, best to stick with conda - - nitime=0.10 - - scikit-image=0.22 - - scikit-learn=1.4 - # Utilities - - graphviz=9.0 - - pandoc=3.1 - # Workflow dependencies: ANTs - - ants=2.5.3 - # Workflow dependencies: FSL (versions pinned in 6.0.7.7) - - fsl-bet2=2111.4 - - fsl-flirt=2111.2 - - fsl-fast4=2111.3 - - fsl-fugue=2201.4 - - fsl-mcflirt=2111.0 - - fsl-miscmaths=2203.2 - - fsl-topup=2203.2 - # - pip - # - pip: - # - -r requirements.txt + - python=3.13 + - numpy=2.3 + - scipy=1.16 + - h5py=3.14 + - ants=2.6 + - fsl-flirt=2111.4 variables: FSLOUTPUTTYPE: NIFTI_GZ diff --git a/pyproject.toml b/pyproject.toml index c4a1b8e4..aae30240 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,9 +20,9 @@ description = "NiTransforms -- Neuroimaging spatial transforms in Python." license = {text = "MIT License"} requires-python = ">= 3.10" dependencies = [ - "numpy >= 2.1", - "scipy >= 1.8", - "nibabel >= 5.2", + "numpy >= 2.0", + "scipy >= 1.10", + "nibabel >= 5.1.1", "h5py >= 3.11", ] dynamic = ["version"]