From 81189897195095eaede4deecfc522668b219a0fb Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Mon, 21 Sep 2026 15:44:28 +0000 Subject: [PATCH] python-gdcm: Add version 3.2.6 Build the SWIG Python bindings of GDCM (Grassroots DICOM) and the GDCM C++ libraries they wrap for riscv64, mirroring the Linux leg of upstream's wheels.yml. Rocky 10's swig replaces upstream's from-source SWIG build, and cmake/ninja come from pypi.riseproject.dev since public PyPI has no riscv64 wheel for either. Two patches add the licences of everything the wheel bundles to the glob-less [project] license-files list. --- .github/workflows/build-python-gdcm.yml | 202 ++++++++++++++++++ docs/packages/python-gdcm.yaml | 6 + ...cences-of-the-bundled-GDCM-libraries.patch | 47 ++++ ...p-the-licence-of-the-bundled-OpenSSL.patch | 26 +++ 4 files changed, 281 insertions(+) create mode 100644 .github/workflows/build-python-gdcm.yml create mode 100644 docs/packages/python-gdcm.yaml create mode 100644 patches/python-gdcm/3.2.6/0001-Ship-the-licences-of-the-bundled-GDCM-libraries.patch create mode 100644 patches/python-gdcm/3.2.6/0002-Ship-the-licence-of-the-bundled-OpenSSL.patch diff --git a/.github/workflows/build-python-gdcm.yml b/.github/workflows/build-python-gdcm.yml new file mode 100644 index 0000000000..4af0402148 --- /dev/null +++ b/.github/workflows/build-python-gdcm.yml @@ -0,0 +1,202 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# This workflow is based on: https://github.com/tfmoraes/python-gdcm/blob/v3.2.6/.github/workflows/wheels.yml +name: Build python-gdcm wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'Version glob to (re)build; empty builds every version of docs/packages/python-gdcm.yaml not released yet' + required: false + default: '' + pull_request: + branches: [main] + paths: + - '.github/workflows/build-python-gdcm.yml' + - 'docs/packages/python-gdcm.yaml' + push: + branches: [main] + paths: + - '.github/workflows/build-python-gdcm.yml' + - 'docs/packages/python-gdcm.yaml' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + +env: + MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64 + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + with: + package: python-gdcm + version: ${{ inputs.version }} + + build_wheels: + needs: [setup] + if: needs.setup.outputs.versions != '[]' + name: Build python-gdcm ${{ matrix.version }} ${{ matrix.python }}-manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 360 + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + python: + - "cp312" + - "cp313" + - "cp314" + - "cp314t" + + env: + PYTHON_GDCM_VERSION: ${{ matrix.version }} + + steps: + - name: Checkout python-gdcm ${{ env.PYTHON_GDCM_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: tfmoraes/python-gdcm + ref: v${{ env.PYTHON_GDCM_VERSION }} + submodules: true + persist-credentials: false + + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: python-wheels + persist-credentials: false + + - name: Apply patches + run: git apply -v python-wheels/patches/python-gdcm/${{ env.PYTHON_GDCM_VERSION }}/*.patch + + - name: Write the wheel test + run: | + cat > test_python_gdcm.py <<'EOF' + import importlib.metadata + import os + import subprocess + import tempfile + + import gdcm + + assert gdcm.Version.GetVersion() == "${{ env.PYTHON_GDCM_VERSION }}", gdcm.Version.GetVersion() + assert gdcm.Global.GetInstance().GetDicts().GetDictEntry( + gdcm.Tag(0x0010, 0x0010)).GetName() == "Patient's Name" + + licenses = sorted( + str(f) for f in importlib.metadata.files("python-gdcm") if "/licenses/" in str(f)) + assert len(licenses) == 13, licenses + assert any(f.endswith("/licenses/LICENSE.openssl") for f in licenses), licenses + + WIDTH = HEIGHT = 64 + PIXELS = bytes(bytearray(((x * 3 + y * 5) % 256) for y in range(HEIGHT) for x in range(WIDTH))) + + def buffer_of(image): + return image.GetBuffer().encode("utf-8", "surrogateescape") + + with tempfile.TemporaryDirectory() as directory: + writer = gdcm.ImageWriter() + image = writer.GetImage() + image.SetNumberOfDimensions(2) + image.SetDimension(0, WIDTH) + image.SetDimension(1, HEIGHT) + pixel_format = gdcm.PixelFormat() + pixel_format.SetScalarType(gdcm.PixelFormat.UINT8) + image.SetPixelFormat(pixel_format) + image.SetPhotometricInterpretation( + gdcm.PhotometricInterpretation(gdcm.PhotometricInterpretation.MONOCHROME2)) + pixeldata = gdcm.DataElement(gdcm.Tag(0x7fe0, 0x0010)) + pixeldata.SetByteStringValue(PIXELS) + image.SetDataElement(pixeldata) + path = os.path.join(directory, "raw.dcm") + writer.SetFileName(path) + writer.SetImage(image) + assert writer.Write() + + reader = gdcm.ImageReader() + reader.SetFileName(path) + assert reader.Read() + assert reader.GetImage().GetDimension(0) == WIDTH + assert reader.GetImage().GetDimension(1) == HEIGHT + assert buffer_of(reader.GetImage()) == PIXELS + + for name in ("DeflatedExplicitVRLittleEndian", "RLELossless", + "JPEGLSLossless", "JPEG2000Lossless"): + change = gdcm.ImageChangeTransferSyntax() + change.SetTransferSyntax(gdcm.TransferSyntax(getattr(gdcm.TransferSyntax, name))) + change.SetInput(reader.GetImage()) + assert change.Change(), name + compressed = os.path.join(directory, name + ".dcm") + encoder = gdcm.ImageWriter() + encoder.SetFileName(compressed) + encoder.SetImage(change.GetOutput()) + assert encoder.Write(), name + decoder = gdcm.ImageReader() + decoder.SetFileName(compressed) + assert decoder.Read(), name + assert buffer_of(decoder.GetImage()) == PIXELS, name + print(name, "ok") + + gdcmdump = os.path.join(os.path.dirname(gdcm.__file__), "_gdcm", "gdcmdump") + dump = subprocess.run( + [gdcmdump, path], check=True, capture_output=True, text=True).stdout + assert "(7fe0,0010)" in dump, dump + EOF + + - name: Build wheels + uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 + with: + output-dir: wheelhouse/ + only: ${{ matrix.python }}-manylinux_riscv64 + env: + CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }} + CIBW_BUILD_FRONTEND: build + # Rocky 10's swig 4.3 replaces upstream's from-source SWIG 4.3.1 build. + # The image's own /usr/local/bin/swig is 4.5.0, which dropped the + # Python 2 compatibility macros GDCM's typemaps still use, so the + # generated wrapper does not compile with it. + CIBW_BEFORE_ALL: >- + dnf -y install openssl-devel swig && + cp /usr/share/licenses/openssl-libs/LICENSE.txt {project}/LICENSE.openssl + # -fPIE/-pie: the image's GCC links executables at 0x10000, and the RPATH + # patchelf writes during auditwheel repair then grows the first segment + # below mmap_min_addr, so every gdcm* application segfaults at exec. The + # compiler flag is what keeps CMake's own ABI detection linking. + # cmake and ninja have no riscv64 wheel on public PyPI; PIP_ONLY_BINARY + # keeps pip from source-building a newer one than our registry's. + CIBW_ENVIRONMENT: >- + CMAKE_ARGS="-DGDCM_USE_SYSTEM_OPENSSL:BOOL=ON -DSWIG_EXECUTABLE=/usr/bin/swig -DCMAKE_C_FLAGS=-fPIE -DCMAKE_CXX_FLAGS=-fPIE -DCMAKE_EXE_LINKER_FLAGS=-pie" + PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ + PIP_ONLY_BINARY=cmake,ninja + CIBW_TEST_SOURCES: test_python_gdcm.py + CIBW_TEST_COMMAND: python test_python_gdcm.py + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: python-gdcm-${{ env.PYTHON_GDCM_VERSION }}-${{ matrix.python }}-manylinux_riscv64 + path: wheelhouse/*.whl + if-no-files-found: error + + publish: + name: Publish python-gdcm ${{ matrix.version }} + needs: [setup, build_wheels] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + permissions: + contents: write + pull-requests: write + uses: $/.github/workflows/_publish-wheel.yml + secrets: + app-private-key: ${{ secrets.RISEPROJECT_APP_PRIVATE_KEY }} + with: + artifact-pattern: python-gdcm-${{ matrix.version }}-*-manylinux_riscv64 diff --git a/docs/packages/python-gdcm.yaml b/docs/packages/python-gdcm.yaml new file mode 100644 index 0000000000..52b31f438a --- /dev/null +++ b/docs/packages/python-gdcm.yaml @@ -0,0 +1,6 @@ +package-name: python-gdcm +source-code: https://github.com/tfmoraes/python-gdcm +license: Apache-2.0 +versions: +- version: 3.2.6 + patched: true diff --git a/patches/python-gdcm/3.2.6/0001-Ship-the-licences-of-the-bundled-GDCM-libraries.patch b/patches/python-gdcm/3.2.6/0001-Ship-the-licences-of-the-bundled-GDCM-libraries.patch new file mode 100644 index 0000000000..859f6ee327 --- /dev/null +++ b/patches/python-gdcm/3.2.6/0001-Ship-the-licences-of-the-bundled-GDCM-libraries.patch @@ -0,0 +1,47 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Sun, 21 Sep 2026 12:40:00 +0200 +Subject: [PATCH] Ship the licences of the bundled GDCM libraries + +The wheel bundles the whole GDCM stack and the third-party libraries +GDCM vendors under gdcm_src/Utilities/: libgdcmcharls (CharLS), +libgdcmexpat (Expat), libgdcmjpeg8/12/16 (IJG libjpeg with the DCMTK +12/16-bit changes), libgdcmopenjp2 (OpenJPEG), libgdcmuuid (libuuid), +libgdcmzlib (zlib) and libsocketxx, plus gdcmmd5, linked into +libgdcmCommon. Each of those is BSD/MIT/zlib-style and requires its +copyright notice to travel with the binary distribution, and the DICOM +data dictionary the wheel ships as _gdcm/XML/*.xml carries the +dicom3tools notice. + +[project] license-files lists only "LICENSE", the wrapper's own +Apache-2.0 text, and an explicit list has no default glob behind it, so +none of those notices reach the wheel's dist-info/licenses directory. + +List them, so every licence of what is redistributed ships with it. + +Upstream-Status: To upstream [Not riscv64-specific: the released wheels on PyPI have the same gap on every platform.] +--- + pyproject.toml | 12 ++++++++++++ + 1 file changed, 12 insertions(+) + +diff --git a/pyproject.toml b/pyproject.toml +--- a/pyproject.toml ++++ b/pyproject.toml +@@ -19,6 +19,17 @@ authors = [ + license = "Apache-2.0" + license-files = [ + "LICENSE", ++ "gdcm_src/Copyright.txt", ++ "gdcm_src/Source/DataDictionary/COPYRIGHT.dicom3tools", ++ "gdcm_src/Utilities/gdcmcharls/License.txt", ++ "gdcm_src/Utilities/gdcmexpat/COPYING", ++ "gdcm_src/Utilities/gdcmjpeg/COPYRIGHT.dcmtk", ++ "gdcm_src/Utilities/gdcmjpeg/README", ++ "gdcm_src/Utilities/gdcmmd5/COPYING", ++ "gdcm_src/Utilities/gdcmopenjpeg/LICENSE", ++ "gdcm_src/Utilities/gdcmuuid/COPYING", ++ "gdcm_src/Utilities/gdcmzlib/LICENSE", ++ "gdcm_src/Utilities/socketxx/COPYING", + ] + requires-python = ">=3.7" + classifiers = [ diff --git a/patches/python-gdcm/3.2.6/0002-Ship-the-licence-of-the-bundled-OpenSSL.patch b/patches/python-gdcm/3.2.6/0002-Ship-the-licence-of-the-bundled-OpenSSL.patch new file mode 100644 index 0000000000..307b85a51e --- /dev/null +++ b/patches/python-gdcm/3.2.6/0002-Ship-the-licence-of-the-bundled-OpenSSL.patch @@ -0,0 +1,26 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Sun, 21 Sep 2026 12:41:00 +0200 +Subject: [PATCH] Ship the licence of the bundled OpenSSL + +GDCM_USE_SYSTEM_OPENSSL links the build against the image's OpenSSL, so +auditwheel grafts libssl and libcrypto into the wheel. Add the licence +text the build stages at the project root to [project] license-files, +which has no default glob behind it. + +Upstream-Status: Inappropriate [LICENSE.openssl is copied out of the build image by this project's riscv64 workflow; upstream's own workflow stages no such file, and its glob-less license-files entry would silently drop it anyway.] +--- + pyproject.toml | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/pyproject.toml b/pyproject.toml +--- a/pyproject.toml ++++ b/pyproject.toml +@@ -19,6 +19,7 @@ authors = [ + license = "Apache-2.0" + license-files = [ + "LICENSE", ++ "LICENSE.openssl", + "gdcm_src/Copyright.txt", + "gdcm_src/Source/DataDictionary/COPYRIGHT.dicom3tools", + "gdcm_src/Utilities/gdcmcharls/License.txt",