Skip to content

Commit 8118989

Browse files
committed
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.
1 parent c0379be commit 8118989

4 files changed

Lines changed: 281 additions & 0 deletions

File tree

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# SPDX-FileCopyrightText: 2026 The RISE Project
2+
# SPDX-License-Identifier: MIT
3+
---
4+
# This workflow is based on: https://github.com/tfmoraes/python-gdcm/blob/v3.2.6/.github/workflows/wheels.yml
5+
name: Build python-gdcm wheels (riscv64)
6+
7+
on:
8+
workflow_dispatch:
9+
inputs:
10+
version:
11+
description: 'Version glob to (re)build; empty builds every version of docs/packages/python-gdcm.yaml not released yet'
12+
required: false
13+
default: ''
14+
pull_request:
15+
branches: [main]
16+
paths:
17+
- '.github/workflows/build-python-gdcm.yml'
18+
- 'docs/packages/python-gdcm.yaml'
19+
push:
20+
branches: [main]
21+
paths:
22+
- '.github/workflows/build-python-gdcm.yml'
23+
- 'docs/packages/python-gdcm.yaml'
24+
25+
concurrency:
26+
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
27+
cancel-in-progress: true
28+
29+
permissions:
30+
contents: read # to fetch code (actions/checkout)
31+
32+
env:
33+
MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64
34+
35+
jobs:
36+
setup:
37+
uses: $/.github/workflows/_setup.yml
38+
with:
39+
package: python-gdcm
40+
version: ${{ inputs.version }}
41+
42+
build_wheels:
43+
needs: [setup]
44+
if: needs.setup.outputs.versions != '[]'
45+
name: Build python-gdcm ${{ matrix.version }} ${{ matrix.python }}-manylinux_riscv64
46+
runs-on: ubuntu-24.04-riscv
47+
timeout-minutes: 360
48+
strategy:
49+
fail-fast: false
50+
matrix:
51+
version: ${{ fromJSON(needs.setup.outputs.versions) }}
52+
python:
53+
- "cp312"
54+
- "cp313"
55+
- "cp314"
56+
- "cp314t"
57+
58+
env:
59+
PYTHON_GDCM_VERSION: ${{ matrix.version }}
60+
61+
steps:
62+
- name: Checkout python-gdcm ${{ env.PYTHON_GDCM_VERSION }}
63+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
64+
with:
65+
repository: tfmoraes/python-gdcm
66+
ref: v${{ env.PYTHON_GDCM_VERSION }}
67+
submodules: true
68+
persist-credentials: false
69+
70+
- name: Checkout python-wheels
71+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
72+
with:
73+
path: python-wheels
74+
persist-credentials: false
75+
76+
- name: Apply patches
77+
run: git apply -v python-wheels/patches/python-gdcm/${{ env.PYTHON_GDCM_VERSION }}/*.patch
78+
79+
- name: Write the wheel test
80+
run: |
81+
cat > test_python_gdcm.py <<'EOF'
82+
import importlib.metadata
83+
import os
84+
import subprocess
85+
import tempfile
86+
87+
import gdcm
88+
89+
assert gdcm.Version.GetVersion() == "${{ env.PYTHON_GDCM_VERSION }}", gdcm.Version.GetVersion()
90+
assert gdcm.Global.GetInstance().GetDicts().GetDictEntry(
91+
gdcm.Tag(0x0010, 0x0010)).GetName() == "Patient's Name"
92+
93+
licenses = sorted(
94+
str(f) for f in importlib.metadata.files("python-gdcm") if "/licenses/" in str(f))
95+
assert len(licenses) == 13, licenses
96+
assert any(f.endswith("/licenses/LICENSE.openssl") for f in licenses), licenses
97+
98+
WIDTH = HEIGHT = 64
99+
PIXELS = bytes(bytearray(((x * 3 + y * 5) % 256) for y in range(HEIGHT) for x in range(WIDTH)))
100+
101+
def buffer_of(image):
102+
return image.GetBuffer().encode("utf-8", "surrogateescape")
103+
104+
with tempfile.TemporaryDirectory() as directory:
105+
writer = gdcm.ImageWriter()
106+
image = writer.GetImage()
107+
image.SetNumberOfDimensions(2)
108+
image.SetDimension(0, WIDTH)
109+
image.SetDimension(1, HEIGHT)
110+
pixel_format = gdcm.PixelFormat()
111+
pixel_format.SetScalarType(gdcm.PixelFormat.UINT8)
112+
image.SetPixelFormat(pixel_format)
113+
image.SetPhotometricInterpretation(
114+
gdcm.PhotometricInterpretation(gdcm.PhotometricInterpretation.MONOCHROME2))
115+
pixeldata = gdcm.DataElement(gdcm.Tag(0x7fe0, 0x0010))
116+
pixeldata.SetByteStringValue(PIXELS)
117+
image.SetDataElement(pixeldata)
118+
path = os.path.join(directory, "raw.dcm")
119+
writer.SetFileName(path)
120+
writer.SetImage(image)
121+
assert writer.Write()
122+
123+
reader = gdcm.ImageReader()
124+
reader.SetFileName(path)
125+
assert reader.Read()
126+
assert reader.GetImage().GetDimension(0) == WIDTH
127+
assert reader.GetImage().GetDimension(1) == HEIGHT
128+
assert buffer_of(reader.GetImage()) == PIXELS
129+
130+
for name in ("DeflatedExplicitVRLittleEndian", "RLELossless",
131+
"JPEGLSLossless", "JPEG2000Lossless"):
132+
change = gdcm.ImageChangeTransferSyntax()
133+
change.SetTransferSyntax(gdcm.TransferSyntax(getattr(gdcm.TransferSyntax, name)))
134+
change.SetInput(reader.GetImage())
135+
assert change.Change(), name
136+
compressed = os.path.join(directory, name + ".dcm")
137+
encoder = gdcm.ImageWriter()
138+
encoder.SetFileName(compressed)
139+
encoder.SetImage(change.GetOutput())
140+
assert encoder.Write(), name
141+
decoder = gdcm.ImageReader()
142+
decoder.SetFileName(compressed)
143+
assert decoder.Read(), name
144+
assert buffer_of(decoder.GetImage()) == PIXELS, name
145+
print(name, "ok")
146+
147+
gdcmdump = os.path.join(os.path.dirname(gdcm.__file__), "_gdcm", "gdcmdump")
148+
dump = subprocess.run(
149+
[gdcmdump, path], check=True, capture_output=True, text=True).stdout
150+
assert "(7fe0,0010)" in dump, dump
151+
EOF
152+
153+
- name: Build wheels
154+
uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0
155+
with:
156+
output-dir: wheelhouse/
157+
only: ${{ matrix.python }}-manylinux_riscv64
158+
env:
159+
CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }}
160+
CIBW_BUILD_FRONTEND: build
161+
# Rocky 10's swig 4.3 replaces upstream's from-source SWIG 4.3.1 build.
162+
# The image's own /usr/local/bin/swig is 4.5.0, which dropped the
163+
# Python 2 compatibility macros GDCM's typemaps still use, so the
164+
# generated wrapper does not compile with it.
165+
CIBW_BEFORE_ALL: >-
166+
dnf -y install openssl-devel swig &&
167+
cp /usr/share/licenses/openssl-libs/LICENSE.txt {project}/LICENSE.openssl
168+
# -fPIE/-pie: the image's GCC links executables at 0x10000, and the RPATH
169+
# patchelf writes during auditwheel repair then grows the first segment
170+
# below mmap_min_addr, so every gdcm* application segfaults at exec. The
171+
# compiler flag is what keeps CMake's own ABI detection linking.
172+
# cmake and ninja have no riscv64 wheel on public PyPI; PIP_ONLY_BINARY
173+
# keeps pip from source-building a newer one than our registry's.
174+
CIBW_ENVIRONMENT: >-
175+
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"
176+
PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/
177+
PIP_ONLY_BINARY=cmake,ninja
178+
CIBW_TEST_SOURCES: test_python_gdcm.py
179+
CIBW_TEST_COMMAND: python test_python_gdcm.py
180+
181+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
182+
with:
183+
name: python-gdcm-${{ env.PYTHON_GDCM_VERSION }}-${{ matrix.python }}-manylinux_riscv64
184+
path: wheelhouse/*.whl
185+
if-no-files-found: error
186+
187+
publish:
188+
name: Publish python-gdcm ${{ matrix.version }}
189+
needs: [setup, build_wheels]
190+
if: needs.setup.outputs.versions != '[]'
191+
strategy:
192+
fail-fast: false
193+
matrix:
194+
version: ${{ fromJSON(needs.setup.outputs.versions) }}
195+
permissions:
196+
contents: write
197+
pull-requests: write
198+
uses: $/.github/workflows/_publish-wheel.yml
199+
secrets:
200+
app-private-key: ${{ secrets.RISEPROJECT_APP_PRIVATE_KEY }}
201+
with:
202+
artifact-pattern: python-gdcm-${{ matrix.version }}-*-manylinux_riscv64

docs/packages/python-gdcm.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package-name: python-gdcm
2+
source-code: https://github.com/tfmoraes/python-gdcm
3+
license: Apache-2.0
4+
versions:
5+
- version: 3.2.6
6+
patched: true
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
2+
From: Ludovic Henry <git@ludovic.dev>
3+
Date: Sun, 21 Sep 2026 12:40:00 +0200
4+
Subject: [PATCH] Ship the licences of the bundled GDCM libraries
5+
6+
The wheel bundles the whole GDCM stack and the third-party libraries
7+
GDCM vendors under gdcm_src/Utilities/: libgdcmcharls (CharLS),
8+
libgdcmexpat (Expat), libgdcmjpeg8/12/16 (IJG libjpeg with the DCMTK
9+
12/16-bit changes), libgdcmopenjp2 (OpenJPEG), libgdcmuuid (libuuid),
10+
libgdcmzlib (zlib) and libsocketxx, plus gdcmmd5, linked into
11+
libgdcmCommon. Each of those is BSD/MIT/zlib-style and requires its
12+
copyright notice to travel with the binary distribution, and the DICOM
13+
data dictionary the wheel ships as _gdcm/XML/*.xml carries the
14+
dicom3tools notice.
15+
16+
[project] license-files lists only "LICENSE", the wrapper's own
17+
Apache-2.0 text, and an explicit list has no default glob behind it, so
18+
none of those notices reach the wheel's dist-info/licenses directory.
19+
20+
List them, so every licence of what is redistributed ships with it.
21+
22+
Upstream-Status: To upstream [Not riscv64-specific: the released wheels on PyPI have the same gap on every platform.]
23+
---
24+
pyproject.toml | 12 ++++++++++++
25+
1 file changed, 12 insertions(+)
26+
27+
diff --git a/pyproject.toml b/pyproject.toml
28+
--- a/pyproject.toml
29+
+++ b/pyproject.toml
30+
@@ -19,6 +19,17 @@ authors = [
31+
license = "Apache-2.0"
32+
license-files = [
33+
"LICENSE",
34+
+ "gdcm_src/Copyright.txt",
35+
+ "gdcm_src/Source/DataDictionary/COPYRIGHT.dicom3tools",
36+
+ "gdcm_src/Utilities/gdcmcharls/License.txt",
37+
+ "gdcm_src/Utilities/gdcmexpat/COPYING",
38+
+ "gdcm_src/Utilities/gdcmjpeg/COPYRIGHT.dcmtk",
39+
+ "gdcm_src/Utilities/gdcmjpeg/README",
40+
+ "gdcm_src/Utilities/gdcmmd5/COPYING",
41+
+ "gdcm_src/Utilities/gdcmopenjpeg/LICENSE",
42+
+ "gdcm_src/Utilities/gdcmuuid/COPYING",
43+
+ "gdcm_src/Utilities/gdcmzlib/LICENSE",
44+
+ "gdcm_src/Utilities/socketxx/COPYING",
45+
]
46+
requires-python = ">=3.7"
47+
classifiers = [
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
2+
From: Ludovic Henry <git@ludovic.dev>
3+
Date: Sun, 21 Sep 2026 12:41:00 +0200
4+
Subject: [PATCH] Ship the licence of the bundled OpenSSL
5+
6+
GDCM_USE_SYSTEM_OPENSSL links the build against the image's OpenSSL, so
7+
auditwheel grafts libssl and libcrypto into the wheel. Add the licence
8+
text the build stages at the project root to [project] license-files,
9+
which has no default glob behind it.
10+
11+
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.]
12+
---
13+
pyproject.toml | 1 +
14+
1 file changed, 1 insertion(+)
15+
16+
diff --git a/pyproject.toml b/pyproject.toml
17+
--- a/pyproject.toml
18+
+++ b/pyproject.toml
19+
@@ -19,6 +19,7 @@ authors = [
20+
license = "Apache-2.0"
21+
license-files = [
22+
"LICENSE",
23+
+ "LICENSE.openssl",
24+
"gdcm_src/Copyright.txt",
25+
"gdcm_src/Source/DataDictionary/COPYRIGHT.dicom3tools",
26+
"gdcm_src/Utilities/gdcmcharls/License.txt",

0 commit comments

Comments
 (0)