diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 578d21e..6d8237a 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -6,19 +6,46 @@ on: - '*' tags: - '[0-9]+.[0-9]+.[0-9]+' - pull_request: +permissions: + contents: read jobs: publish: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@master - - name: Publish to Registry - uses: docker/build-push-action@v1 + - uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master + with: + persist-credentials: false + - name: Log in to Docker Hub + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ secrets.pcicdevops_at_dockerhub_username }} password: ${{ secrets.pcicdevops_at_dockerhub_password }} - repository: pcic/geospatial-python - tag_with_ref: true + + - name: Publish to Registry + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 + with: + push: true + tags: | + pcic/geospatial-python:${{ github.ref_name }} + ${{ github.ref_name == 'master' && 'pcic/geospatial-python:latest' || '' }} + + smoke-test: + needs: publish + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master + with: + persist-credentials: false + + - name: Pull the published image + run: docker pull pcic/geospatial-python:${{ github.ref_name }} + + - name: Verify each library imports and reports a version + run: | + docker run --rm \ + -v "$PWD/ci:/ci:ro" \ + pcic/geospatial-python:${{ github.ref_name }} \ + python3 /ci/smoke_test.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b3ec7d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,220 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +*.lcov +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +# Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +# poetry.lock +# poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +# pdm.lock +# pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi/* +!.pixi/config.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule* +celerybeat.pid + +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +# .idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ +# Temporary file for partial code execution +tempCodeRunnerFile.py + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml diff --git a/Dockerfile b/Dockerfile index 8e2d2e7..202ac1a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,77 @@ -FROM ubuntu:24.04 +# syntax=docker/dockerfile:1 -LABEL Maintainer="James Hiebert " +# Ubuntu 26.04 LTS ships Python 3.14 and GDAL 3.12 as distro defaults, so the +# GDAL Python bindings (built from source against libgdal-dev) match the system +# library exactly. The build is split into two stages: a build stage with the +# compilers and -dev headers needed to compile the C extensions, and a slim +# runtime stage that carries only the shared libraries they link against. + +############################ +# Build stage +############################ +FROM ubuntu:26.04 AS build + +LABEL org.opencontainers.image.authors="James Hiebert " ARG DEBIAN_FRONTEND=noninteractive +ENV PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 \ + python3-dev \ + python3-venv \ + g++ \ + libgdal-dev \ + libhdf5-dev \ + libnetcdf-dev \ + libyaml-dev \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +# Build into an isolated virtualenv instead of the system interpreter; this +# avoids pip's --break-system-packages +RUN python3 -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" \ + VIRTUAL_ENV="/opt/venv" -RUN apt-get update && apt-get install -yq \ - libhdf5-dev \ - libnetcdf-dev \ - libyaml-dev \ - libgdal-dev \ - python3 \ - python3-dev \ - python3-pip \ - cython3 && \ - rm -rf /var/lib/apt/lists/* +# numpy must be present before GDAL so the gdal_array bindings are built with +# numpy support. The GDAL binding version is pinned to whatever libgdal-dev the +# distro provides, so the two can never drift apart. +RUN pip install numpy +RUN pip install \ + "gdal==$(gdal-config --version)" \ + h5py \ + netCDF4 \ + psycopg2 \ + PyYAML \ + pillow + +############################ +# Runtime stage +############################ +FROM ubuntu:26.04 AS runtime + +LABEL org.opencontainers.image.authors="James Hiebert " \ + org.opencontainers.image.title="geospatial-python" \ + org.opencontainers.image.description="Ubuntu + Python base image for geospatial netCDF data and web apps" \ + org.opencontainers.image.source="https://github.com/pacificclimate/docker-geospatial-python" + +ARG DEBIAN_FRONTEND=noninteractive -ENV CPLUS_INCLUDE_PATH=/usr/include/gdal -ENV C_INCLUDE_PATH=/usr/include/gdal +# Only the shared runtime libraries the compiled extensions link against +# (libgdal38 pulls in PROJ, GEOS, etc. on its own). No compilers or headers. +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 \ + libgdal38 \ + libhdf5-310 \ + libnetcdf22 \ + libpq5 \ + libyaml-0-2 \ + && rm -rf /var/lib/apt/lists/* -RUN pip install numpy --break-system-packages +COPY --from=build /opt/venv /opt/venv -RUN pip install gdal==3.8.4 h5py netCDF4 psycopg2 PyYAML pillow --break-system-packages \ No newline at end of file +ENV PATH="/opt/venv/bin:$PATH" \ + VIRTUAL_ENV="/opt/venv" \ + PYTHONUNBUFFERED=1 diff --git a/ci/smoke_test.py b/ci/smoke_test.py new file mode 100644 index 0000000..d3fc6a9 --- /dev/null +++ b/ci/smoke_test.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +"""Smoke test for the published geospatial-python image. + +Imports each bundled library and prints its version, exiting non-zero if any +import or version lookup fails. Run inside the image, e.g. + + docker run --rm -v "$PWD:/work" -w /work IMAGE python3 ci/smoke_test.py +""" +import importlib +import sys + +# (module to import, attribute holding the version) +LIBRARIES = [ + ("numpy", "__version__"), + ("osgeo.gdal", "__version__"), + ("h5py", "__version__"), + ("netCDF4", "__version__"), + ("psycopg2", "__version__"), + ("yaml", "__version__"), + ("PIL", "__version__"), +] + + +def main() -> int: + failures = [] + for module_name, version_attr in LIBRARIES: + try: + module = importlib.import_module(module_name) + version = getattr(module, version_attr) + print(f"OK {module_name} {version}") + except Exception as exc: # noqa: BLE001 - report and keep going + failures.append(f"{module_name}: {exc}") + print(f"FAIL {module_name}: {exc}") + + if failures: + print(f"\n{len(failures)} library check(s) failed", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main())