8000 Install dicom3tools from PyPI instead of GitHub release assets by fedorov · Pull Request #561 · QIICR/dcmqi · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 51 additions & 55 deletions .github/actions/install-dicom3tools/action.yml
Original file line number Diff line number Diff line change
@@ -1,76 +1,72 @@
name: "Install dicom3tools"
description: >
Download a prebuilt dicom3tools distribution and prepend it to PATH, so that the
find_program(DCIODVFY_EXECUTABLE dciodvfy) calls in apps/*/Testing/CMakeLists.txt
succeed and the optional dciodvfy conformance tests are actually registered.
Install the dicom3tools command line utilities from PyPI and prepend the packaged
binaries to PATH, so that the find_program(DCIODVFY_EXECUTABLE dciodvfy) calls in
apps/*/Testing/CMakeLists.txt succeed and the optional dciodvfy conformance tests
are actually registered.

# To move to a newer dicom3tools build, bump RELEASE_TAG and replace all three
# digests below with the ones published for that release. The digests are verified
# unconditionally: a release asset can be replaced in place under an unchanged tag,
# so pinning the tag alone would not pin the bytes CI executes.
# Requires a Python >= 3.10 on PATH (every workflow using this action already sets one up).
#
# The version below is an exact pin, and an exact pin is all that is needed here: PyPI
# filenames are immutable, so a given version always resolves to the same bytes. That is
# why this no longer carries per-platform SHA-256 digests -- the previous implementation
# fetched GitHub release assets, which *can* be replaced in place under an unchanged tag,
# so there the tag alone did not pin what CI executed.
#
# To move to a newer dicom3tools build, bump the default below (or pass
# `dicom3tools_version:`), and keep docker/Makefile's dicom3tools_version in sync -- the
# Linux/x86_64 job installs the same package inside its dockcross container rather than
# going through this action.

inputs:
dicom3tools_version:
description: "Exact dicom3tools version to install from PyPI."
required: false
default: "20260901"

runs:
using: composite
steps:
- name: "Install dicom3tools (dciodvfy)"
shell: bash
env:
RELEASE_REPO: fedorov/dicom3tools
RELEASE_TAG: dicom3tools.20260901072548.post1
SHA256_LINUX: d793a53a0f6cc4bae28b2979f67b583783d0954a7da7a2cd40b7359adbf681ae
SHA256_MACOS: a5d1f718a2d47f5dc5566ec34762bf55641b2f8c8c5c7d922404f5a6a9fe3afe
SHA256_WINDOWS: 9bb25b5ee3692cd2690c8f62007f2c352f5870c81c5c2ca3abf08042d44781da
DICOM3TOOLS_VERSION: ${{ inputs.dicom3tools_version }}
run: |
set -euo pipefail

case "$RUNNER_OS" in
Linux) asset=dicom3tools-linux-x86_64.tar.gz; expected=$SHA256_LINUX ;;
macOS) asset=dicom3tools-macos-universal.tar.gz; expected=$SHA256_MACOS ;;
Windows) asset=dicom3tools-windows-x86_64.zip; expected=$SHA256_WINDOWS ;;
*) echo "Unsupported runner OS: $RUNNER_OS" >&2; exit 1 ;;
esac

archive="$RUNNER_TEMP/$asset"
dest="$RUNNER_TEMP/dicom3tools"
mkdir -p "$dest"
# `python` is what actions/setup-python puts on PATH on all three runner images;
# the manylinux container jobs export their interpreter under that name too.
PY=python
command -v "$PY" >/dev/null 2>&1 || PY=python3

url="https://github.com/$RELEASE_REPO/releases/download/$RELEASE_TAG/$asset"
echo "Downloading $url"
curl -fsSL --retry 3 --retry-delay 5 -o "$archive" "$url"
# --only-binary refuses the sdist: it is a stub that cannot build the tools, so
# falling back to it would produce a silent no-op instead of a clear failure on a
# platform with no published wheel.
"$PY" -m pip install --disable-pip-version-check --only-binary=:all: \
"dicom3tools==$DICOM3TOOLS_VERSION"

actual=$(openssl dgst -sha256 -r "$archive" | cut -d' ' -f1)
if [ "$actual" != "$expected" ]; then
echo "Checksum mismatch for $asset" >&2
echo " expected: $expected" >&2
echo " actual: $actual" >&2
exit 1
fi
# bin_dir() is the package's supported way to locate the packaged programs. Putting
# that directory on PATH (rather than relying on the console scripts) gives CMake the
# native executables directly, and satisfies the packaged shell-script tools, which
# invoke their siblings by bare name. On Windows the Cygwin runtime DLLs ship in the
# same directory as the executables, which is where Windows looks for them first.
{
read -r bindir_native
read -r bindir_posix
} < <("$PY" -c 'import dicom3tools; d = dicom3tools.bin_dir(); print(d); print(d.as_posix())')

# All three archives are flat: every tool sits at the archive root, and the
# Windows build ships its Cygwin DLLs next to the executables, which is where
# Windows looks for them first.
case "$asset" in
*.tar.gz)
tar xzf "$archive" -C "$dest"
;;
*.zip)
# unzip is not present on the hosted Windows images; 7z is.
if command -v 7z >/dev/null 2>&1; then
7z x -bso0 -o"$dest" "$archive"
else
unzip -q "$archive" -d "$dest"
fi
;;
esac
# Python terminates lines with CRLF on Windows and `read -r` keeps the CR, which
# would otherwise end up inside the path and in GITHUB_PATH. No-op elsewhere.
bindir_native=${bindir_native%$'\r'}
bindir_posix=${bindir_posix%$'\r'}

exe="$dest/dciodvfy"
[ -f "$exe" ] || exe="$dest/dciodvfy.exe"
exe="$bindir_posix/dciodvfy"
[ -f "$exe" ] || exe="$bindir_posix/dciodvfy.exe"
if [ ! -f "$exe" ]; then
echo "dciodvfy not found in $asset" >&2
ls -la "$dest" >&2
echo "dciodvfy not found in $bindir_posix" >&2
ls -la "$bindir_posix" >&2
exit 1
fi
chmod +x "$exe"

# Smoke test. With no file argument dciodvfy validates (empty) stdin, which is
# enough to exercise dynamic loading. Exit codes >= 126 mean the binary could not
Expand All @@ -85,5 +81,5 @@ runs:
exit 1
fi

echo "$dest" >> "$GITHUB_PATH"
echo "dciodvfy installed at $exe"
echo "$bindir_native" >> "$GITHUB_PATH"
echo "dciodvfy $DICOM3TOOLS_VERSION installed at $exe"
41 changes: 24 additions & 17 deletions .github/workflows/cmake-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,13 @@ jobs:
run: |
cd docker && make dcmqi

# docker/Makefile's prereq.build_dicom3tools target is meant to put dciodvfy on PATH
# for this job, but it does not: it downloads dicom3tools_winexe_*.zip (prebuilt Cygwin
# Windows binaries, not source), extracts it with `tar xjf`, and cds into a directory
# the flat archive does not contain. The whole && chain fails into `|| echo "Reusing "`,
# so the step reports success while find_program() finds nothing and the dciodvfy tests
# are silently dropped. Even once that is repaired, the prebuilt Linux release cannot be
# substituted here: it needs glibc >= 2.34 and dockcross/manylinux2014-x64 has 2.17.
# See https://github.com/fedorov/dicom3tools/issues/19.
# dciodvfy comes from the dicom3tools PyPI wheels, installed into the dockcross
# container by docker/Makefile's prereq.install_dicom3tools target. This job runs its
# tests inside that container, so the .github/actions/install-dicom3tools step used by
# the other jobs would not reach them. The manylinux_2_17_x86_64 wheel matches
# dockcross/manylinux2014-x64's glibc 2.17; the target it replaces downloaded the
# Windows build and silently failed, leaving the conformance tests unregistered.
# `make dcmqi.test` now asserts they are registered before running the suite.
- name: "Test dcmqi"
run: |
cd docker && make dcmqi.test
Expand Down Expand Up @@ -199,6 +198,12 @@ jobs:
gcc --version
"$PYTHON" --version

# Provides dciodvfy, which apps/*/Testing/CMakeLists.txt look up with find_program().
# Without it CMake silently drops the DICOM conformance tests. The dicom3tools wheels
# include a manylinux_2_28_aarch64 build, which matches this container exactly.
- name: "Install dicom3tools"
uses: ./.github/actions/install-dicom3tools

# Same layout and caching strategy as the macOS build: DCMTK/ITK need both their source
# and build trees, zlib only its install tree.
#
Expand Down Expand Up @@ -291,16 +296,18 @@ jobs:
run: |
cd "$GITHUB_WORKSPACE"/dcmqi-build && ninja

# Guard against the silent-skip failure mode: when find_program() fails to locate
# dciodvfy, CMake drops the conformance tests without failing anything, so the checks
# stop running unnoticed. Assert they were registered before running the suite.
- name: "Verify dciodvfy tests are registered"
run: |
cd "$GITHUB_WORKSPACE"/dcmqi-build/dcmqi-build
count=$(ctest -N -R dciodvfy | sed -n 's/^Total Tests: //p')
echo "dciodvfy tests registered: ${count:-0}"
[ "${count:-0}" -gt 0 ]

# The ajv tests are skipped here: ajv is optional and CMake drops those tests when it
# is absent.
#
# The dciodvfy (dicom3tools) tests are also skipped, and unlike on macOS and Windows
# that is not yet fixable by installing the prebuilt dicom3tools release. Those Linux
# binaries are built on ubuntu-22.04 and reference GLIBC_2.34 / GLIBC_2.29, while this
# job runs in manylinux_2_28_aarch64 (glibc 2.28) -- and no aarch64 asset is published
# at all. Tracked upstream in https://github.com/fedorov/dicom3tools/issues/19; once
# manylinux-built Linux assets exist, add the .github/actions/install-dicom3tools step
# used by the macOS and Windows workflows.
# is absent. The dciodvfy tests do run, on the wheel installed above.
- name: "Test dcmqi"
run: |
cd "$GITHUB_WORKSPACE"/dcmqi-build/dcmqi-build
Expand Down
65 changes: 48 additions & 17 deletions docker/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,22 @@ TMP = /tmp
# Shell
SHELL = /bin/bash

dicom3tools_version=dicom3tools_1.00.snapshot.20200512090031
# dicom3tools is installed from PyPI, which publishes manylinux wheels this image can run.
# Keep this pin in sync with .github/actions/install-dicom3tools/action.yml, which does the
# same for the macOS, Windows and Linux/arm64 jobs.
dicom3tools_version=20260901

# Each dockcross invocation is a separate `docker run`, so only the mounted repository
# survives between targets: the tools have to be installed under the (cached) build
# directory rather than somewhere in the container's own filesystem.
dicom3tools_venv=/work/$(BUILD_DIR)/dicom3tools-venv

# Expands, inside the container, to the directory holding the packaged dicom3tools
# programs -- what find_program(DCIODVFY_EXECUTABLE dciodvfy) needs on PATH. The recipes
# below are expanded by make, then by the host shell, and only then handed to the
# container, so the `$` is escaped to survive the host shell: the substitution has to run
# where the venv exists, which is inside the container.
dicom3tools_bin=\$$($(dicom3tools_venv)/bin/python -c 'import dicom3tools; print(dicom3tools.bin_dir())')

#
# Targets
Expand All @@ -31,18 +46,28 @@ prereq.pull_dockcross:
$(DOCKER) run --rm dockcross/manylinux2014-x64 > $(TMP)/dockcross
chmod u+x $(TMP)/dockcross

# Download and build dicom3tools
prereq.build_dicom3tools: prereq.pull_dockcross
# Install dicom3tools (provides dciodvfy) from PyPI.
#
# This replaces a download-and-build of the upstream sources that never worked: it fetched
# the *Windows* .zip, unpacked it with `tar xjf`, and cd'd into a directory the flat archive
# does not contain, with the whole && chain swallowed by `|| echo "Reusing "`. The step
# reported success while find_program() found nothing, so the dciodvfy conformance tests
# were silently dropped from this job.
#
# The wheels require Python >= 3.10, so this deliberately does not use the cp38 interpreter
# the rest of this Makefile pins for the build itself.
prereq.install_dicom3tools: prereq.pull_dockcross
mkdir -p $(ROOT_DIR)/$(BUILD_DIR)
cd $(ROOT_DIR)/$(BUILD_DIR) \
&& [ ! -e $(dicom3tools_version) ] \
&& wget --no-check-certificate https://github.com/QIICR/dicom3tools/releases/download/20200512090031/dicom3tools_winexe_1.00.snapshot.20200512090031.zip -O dicom3tools.zip \
&& tar xjf dicom3tools.zip \
&& cd $(dicom3tools_version)/ \
&& $(TMP)/dockcross ./Configure \
&& $(TMP)/dockcross imake -I./config -DUseQIICRID \
&& ( $(TMP)/dockcross make -j$(grep -c processor /proc/cpuinfo) World > /dev/null 2>&1 )\
|| echo "Reusing "
cd $(ROOT_DIR) && \
$(TMP)/dockcross bash -c 'set -eu; \
PY=$$(ls -1d /opt/python/cp31[0-9]-cp31[0-9]/bin/python 2>/dev/null | sort -V | tail -n 1); \
if [ -z "$$PY" ]; then echo "No Python >= 3.10 found in the dockcross image" >&2; exit 1; fi; \
"$$PY" -m venv $(dicom3tools_venv); \
$(dicom3tools_venv)/bin/python -m pip install --disable-pip-version-check \
--only-binary=:all: "dicom3tools==$(dicom3tools_version)"; \
BIN=$$($(dicom3tools_venv)/bin/python -c "import dicom3tools; print(dicom3tools.bin_dir())"); \
test -x "$$BIN/dciodvfy" || { echo "dciodvfy missing from $$BIN" >&2; exit 1; }; \
echo "dciodvfy $(dicom3tools_version) installed at $$BIN/dciodvfy"'

# Download and install "ajv"
prereq.install_npm_packages: prereq.pull_dockcross
Expand All @@ -59,10 +84,10 @@ prereq.install_npm_packages: prereq.pull_dockcross
$(TMP)/dockcross bash -c "export PATH=/work/build/node-v12.19.1-linux-x64/bin:$$PATH && sudo npm install ajv-cli@3.3.0 -g"

# Configure, build and package
dcmqi.generate_package: prereq.pull_dockcross
dcmqi.generate_package: prereq.pull_dockcross prereq.install_dicom3tools
cd $(ROOT_DIR) && \
$(TMP)/dockcross cmake -B$(BUILD_DIR) -H. -GNinja -DCMAKE_BUILD_TYPE:STRING=Release -DPYTHON_EXECUTABLE:FILEPATH=/opt/python/cp38-cp38/bin/python && \
$(TMP)/dockcross bash -c "export PATH=/work/build/node-v6.9.5-linux-x64/bin:/work/build/$(dicom3tools_version)/appsrc/dcfile:$$PATH && ninja -C$(BUILD_DIR) -v" && \
$(TMP)/dockcross bash -c "export PATH=/work/build/node-v6.9.5-linux-x64/bin:$(dicom3tools_bin):$$PATH && ninja -C$(BUILD_DIR) -v" && \
$(TMP)/dockcross ninja -C$(BUILD_DIR)/dcmqi-build package

# Build "dcmqi" docker image
Expand Down Expand Up @@ -99,11 +124,17 @@ dcmqi.docker_image_test:
docker run --rm $(ORG)/dcmqi:`git describe --tags --exact-match 2> /dev/null || echo "latest"` tid1500reader --help
docker run --rm $(ORG)/dcmqi:`git describe --tags --exact-match 2> /dev/null || echo "latest"` tid1500writer --help

dcmqi: prereq.build_dicom3tools prereq.install_npm_packages dcmqi.generate_package dcmqi.docker_image dcmqi.docker_image_test
dcmqi: prereq.install_dicom3tools prereq.install_npm_packages dcmqi.generate_package dcmqi.docker_image dcmqi.docker_image_test

dcmqi.test:
# Guard against the silent-skip failure mode: when find_program() fails to locate dciodvfy,
# CMake drops the conformance tests without failing anything, so the checks stop running
# unnoticed -- which is exactly what the broken download above did for years. `ctest -N`
# only lists what was registered at configure time, so this needs no PATH of its own.
dcmqi.test: prereq.install_dicom3tools
cd $(ROOT_DIR) && \
$(TMP)/dockcross bash -c "cd /work/$(BUILD_DIR)/dcmqi-build && count=\$$(ctest -N -R dciodvfy | sed -n 's/^Total Tests: //p') && echo \"dciodvfy tests registered: \$${count:-0}\" && [ \"\$${count:-0}\" -gt 0 ]"
cd $(ROOT_DIR) && \
$(TMP)/dockcross bash -c "export PATH=/work/build/node-v6.9.5-linux-x64/bin:/work/build/$(dicom3tools_version)/appsrc/dcfile:/opt/python/cp38-cp38/bin/:$$PATH && cd /work/build/dcmqi-build && ctest -j4 -D ExperimentalTest -VV --no-compress-output"
$(TMP)/dockcross bash -c "export PATH=/work/build/node-v6.9.5-linux-x64/bin:$(dicom3tools_bin):/opt/python/cp38-cp38/bin/:$$PATH && cd /work/build/dcmqi-build && ctest -j4 -D ExperimentalTest -VV --no-compress-output"

dcmqi.clean:
rm -rf $(ROOT_DIR)/$(BUILD_DIR)
Expand Down
0