dicom3tools 20260901__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dicom3tools-20260901/.git_archival.txt +4 -0
- dicom3tools-20260901/.gitattributes +1 -0
- dicom3tools-20260901/.github/dependabot.yml +11 -0
- dicom3tools-20260901/.github/scripts/verify_pins.py +117 -0
- dicom3tools-20260901/.github/workflows/cd.yml +171 -0
- dicom3tools-20260901/.github/workflows/ci.yml +83 -0
- dicom3tools-20260901/.github/workflows/update-pins.yml +143 -0
- dicom3tools-20260901/.gitignore +9 -0
- dicom3tools-20260901/CMakeLists.txt +135 -0
- dicom3tools-20260901/LICENSE +212 -0
- dicom3tools-20260901/PKG-INFO +361 -0
- dicom3tools-20260901/README.md +323 -0
- dicom3tools-20260901/binaries.txt +42 -0
- dicom3tools-20260901/dicom3toolsUrls.cmake +113 -0
- dicom3tools-20260901/pyproject.toml +210 -0
- dicom3tools-20260901/scripts/retag_linux_wheel.sh +127 -0
- dicom3tools-20260901/scripts/retag_macos_wheel.sh +86 -0
- dicom3tools-20260901/scripts/update_dicom3tools_urls.py +209 -0
- dicom3tools-20260901/src/dicom3tools/__init__.py +138 -0
- dicom3tools-20260901/src/dicom3tools/_version.py +24 -0
- dicom3tools-20260901/src/dicom3tools/_version.pyi +2 -0
- dicom3tools-20260901/src/dicom3tools/py.typed +0 -0
- dicom3tools-20260901/tests/__init__.py +12 -0
- dicom3tools-20260901/tests/test_executable.py +198 -0
- dicom3tools-20260901/tests/test_package.py +34 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.git_archival.txt export-subst
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Verify that every archive pinned in ``dicom3toolsUrls.cmake`` is still downloadable and still
|
|
4
|
+
has the checksum recorded for it.
|
|
5
|
+
|
|
6
|
+
This guards the seam between the two layers. The pins point at GitHub release assets, and a
|
|
7
|
+
release can be re-cut with new assets under the same tag. When that happens every wheel job
|
|
8
|
+
fails deep inside CMake with an opaque hash mismatch. Failing here instead says plainly which
|
|
9
|
+
archive moved and what to do about it.
|
|
10
|
+
|
|
11
|
+
The dicom3tools assets carry no version in their filenames -- every release publishes
|
|
12
|
+
``dicom3tools-linux-x86_64.tar.gz`` and friends -- so the tag is the only thing that says which
|
|
13
|
+
release a pin refers to. That makes two extra checks worth doing here, both of which a
|
|
14
|
+
checksum mismatch would only report as "wrong bytes":
|
|
15
|
+
|
|
16
|
+
* the tag still parses as ``dicom3tools.<snapshot>[.postN]``, and
|
|
17
|
+
* the snapshot and version recorded alongside the checksums agree with it.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import hashlib
|
|
23
|
+
import re
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from urllib.error import HTTPError, URLError
|
|
27
|
+
from urllib.request import urlopen
|
|
28
|
+
|
|
29
|
+
CMAKE_FILE = Path(__file__).parents[2] / "dicom3toolsUrls.cmake"
|
|
30
|
+
PLATFORMS = ("linux_x86_64", "linux_aarch64", "macos_arm64", "macos_x86_64", "win64")
|
|
31
|
+
TAG_RE = re.compile(r"^dicom3tools\.(?P<snapshot>\d{14})(?:\.post\d+)?$")
|
|
32
|
+
CHUNK = 1 << 20
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _scalar(text: str, name: str) -> str:
|
|
36
|
+
"""Read a set(<name> "<literal>") value, ignoring lines that reference variables."""
|
|
37
|
+
match = re.search(rf'^set\({re.escape(name)}\s+"([^"$]*)"', text, re.MULTILINE)
|
|
38
|
+
if match is None:
|
|
39
|
+
msg = f"Could not find a literal value for '{name}' in {CMAKE_FILE.name}"
|
|
40
|
+
raise SystemExit(msg)
|
|
41
|
+
return match.group(1)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _check_provenance(text: str, tag: str) -> int:
|
|
45
|
+
"""Confirm the recorded snapshot and version were derived from the pinned tag."""
|
|
46
|
+
match = TAG_RE.match(tag)
|
|
47
|
+
if match is None:
|
|
48
|
+
print(
|
|
49
|
+
f"::error::pinned tag '{tag}' is not a dicom3tools release tag "
|
|
50
|
+
"(expected dicom3tools.<14-digit snapshot>[.postN])"
|
|
51
|
+
)
|
|
52
|
+
return 1
|
|
53
|
+
|
|
54
|
+
snapshot = match.group("snapshot")
|
|
55
|
+
expected = {
|
|
56
|
+
"dicom3tools_snapshot": snapshot,
|
|
57
|
+
"dicom3tools_version": snapshot[:8],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
failures = 0
|
|
61
|
+
for name, want in expected.items():
|
|
62
|
+
got = _scalar(text, name)
|
|
63
|
+
if got != want:
|
|
64
|
+
print(f"::error::{name} is '{got}', but tag '{tag}' implies '{want}'")
|
|
65
|
+
failures += 1
|
|
66
|
+
return failures
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def main() -> int:
|
|
70
|
+
text = CMAKE_FILE.read_text(encoding="utf-8")
|
|
71
|
+
|
|
72
|
+
repo = _scalar(text, "DICOM3TOOLS_BINARIES_REPO")
|
|
73
|
+
tag = _scalar(text, "DICOM3TOOLS_BINARIES_TAG")
|
|
74
|
+
base = f"https://github.com/{repo}/releases/download/{tag}"
|
|
75
|
+
print(f"checking pinned archives in {repo}@{tag}\n")
|
|
76
|
+
|
|
77
|
+
failures = _check_provenance(text, tag)
|
|
78
|
+
|
|
79
|
+
for platform in PLATFORMS:
|
|
80
|
+
filename = _scalar(text, f"{platform}_filename")
|
|
81
|
+
expected = _scalar(text, f"{platform}_sha256")
|
|
82
|
+
url = f"{base}/{filename}"
|
|
83
|
+
print(f"--- {platform}: {filename}")
|
|
84
|
+
|
|
85
|
+
digest = hashlib.sha256()
|
|
86
|
+
try:
|
|
87
|
+
with urlopen(url) as response:
|
|
88
|
+
while chunk := response.read(CHUNK):
|
|
89
|
+
digest.update(chunk)
|
|
90
|
+
except (HTTPError, URLError) as exc:
|
|
91
|
+
print(f"::error::{filename} could not be downloaded from {base}: {exc}")
|
|
92
|
+
failures += 1
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
actual = digest.hexdigest()
|
|
96
|
+
if actual == expected:
|
|
97
|
+
print(" ok")
|
|
98
|
+
else:
|
|
99
|
+
print(
|
|
100
|
+
f"::error::{filename} checksum mismatch: pinned {expected}, got {actual}"
|
|
101
|
+
)
|
|
102
|
+
failures += 1
|
|
103
|
+
|
|
104
|
+
if failures:
|
|
105
|
+
print(
|
|
106
|
+
f"\n{failures} problem(s) with the pins. If the release was intentionally "
|
|
107
|
+
"re-cut, or the pins should move to a newer snapshot, refresh them with:\n"
|
|
108
|
+
f" python scripts/update_dicom3tools_urls.py --repo {repo} --tag <tag>"
|
|
109
|
+
)
|
|
110
|
+
return 1
|
|
111
|
+
|
|
112
|
+
print("\nall pins verified")
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if __name__ == "__main__":
|
|
117
|
+
sys.exit(main())
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
name: wheels
|
|
2
|
+
|
|
3
|
+
# Builds the platform wheels and publishes them to PyPI on release.
|
|
4
|
+
#
|
|
5
|
+
# This workflow does no compiling: it downloads the prebuilt dicom3tools archives pinned in
|
|
6
|
+
# dicom3toolsUrls.cmake and packages them, which is why it finishes in minutes rather than the
|
|
7
|
+
# hours the binaries themselves take. The archives are produced by the build workflows in
|
|
8
|
+
# ImagingDataCommons/dicom3tools and attached to a release there.
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
release:
|
|
13
|
+
types:
|
|
14
|
+
- published
|
|
15
|
+
|
|
16
|
+
concurrency:
|
|
17
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
18
|
+
cancel-in-progress: true
|
|
19
|
+
|
|
20
|
+
env:
|
|
21
|
+
FORCE_COLOR: 3
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
make_sdist:
|
|
25
|
+
name: Make SDist
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v7
|
|
29
|
+
with:
|
|
30
|
+
fetch-depth: 0
|
|
31
|
+
|
|
32
|
+
- name: Build SDist
|
|
33
|
+
run: pipx run build --sdist
|
|
34
|
+
|
|
35
|
+
- uses: actions/upload-artifact@v7
|
|
36
|
+
with:
|
|
37
|
+
name: cibw-sdist
|
|
38
|
+
path: dist/*.tar.gz
|
|
39
|
+
|
|
40
|
+
build_wheels:
|
|
41
|
+
name: Wheel on ${{ matrix.os }} (${{ matrix.arch }})
|
|
42
|
+
runs-on: ${{ matrix.os }}
|
|
43
|
+
strategy:
|
|
44
|
+
fail-fast: false
|
|
45
|
+
matrix:
|
|
46
|
+
include:
|
|
47
|
+
# One runner per target architecture -- see the note in ci.yml.
|
|
48
|
+
- os: ubuntu-latest
|
|
49
|
+
arch: x86_64
|
|
50
|
+
- os: ubuntu-24.04-arm
|
|
51
|
+
arch: aarch64
|
|
52
|
+
- os: macos-15
|
|
53
|
+
arch: arm64
|
|
54
|
+
- os: macos-15-intel
|
|
55
|
+
arch: x86_64
|
|
56
|
+
- os: windows-latest
|
|
57
|
+
arch: AMD64
|
|
58
|
+
|
|
59
|
+
steps:
|
|
60
|
+
- uses: actions/checkout@v7
|
|
61
|
+
with:
|
|
62
|
+
fetch-depth: 0
|
|
63
|
+
|
|
64
|
+
- uses: pypa/cibuildwheel@v4.2.0
|
|
65
|
+
env:
|
|
66
|
+
CIBW_ARCHS: ${{ matrix.arch }}
|
|
67
|
+
|
|
68
|
+
- name: Upload wheels
|
|
69
|
+
uses: actions/upload-artifact@v7
|
|
70
|
+
with:
|
|
71
|
+
name: cibw-wheels-${{ matrix.os }}-${{ matrix.arch }}
|
|
72
|
+
path: wheelhouse/*.whl
|
|
73
|
+
|
|
74
|
+
test_sdist:
|
|
75
|
+
name: Test SDist with python ${{ matrix.python }}
|
|
76
|
+
needs: [make_sdist]
|
|
77
|
+
runs-on: ubuntu-latest
|
|
78
|
+
strategy:
|
|
79
|
+
fail-fast: false
|
|
80
|
+
matrix:
|
|
81
|
+
python: ["3.10", "3.13"]
|
|
82
|
+
|
|
83
|
+
steps:
|
|
84
|
+
- uses: actions/checkout@v7
|
|
85
|
+
- uses: actions/setup-python@v7
|
|
86
|
+
with:
|
|
87
|
+
python-version: ${{ matrix.python }}
|
|
88
|
+
|
|
89
|
+
- uses: actions/download-artifact@v8
|
|
90
|
+
with:
|
|
91
|
+
name: cibw-sdist
|
|
92
|
+
path: dist
|
|
93
|
+
|
|
94
|
+
- name: Install SDist and test dependencies
|
|
95
|
+
run: |
|
|
96
|
+
pip install pytest pytest-cov
|
|
97
|
+
pip install dist/*.tar.gz
|
|
98
|
+
rm -rf dist
|
|
99
|
+
|
|
100
|
+
- name: Test installed SDist
|
|
101
|
+
run: pytest ./tests
|
|
102
|
+
|
|
103
|
+
check_dist:
|
|
104
|
+
name: Check dist
|
|
105
|
+
needs: [build_wheels, make_sdist, test_sdist]
|
|
106
|
+
runs-on: ubuntu-latest
|
|
107
|
+
steps:
|
|
108
|
+
- uses: actions/download-artifact@v8
|
|
109
|
+
with:
|
|
110
|
+
path: all
|
|
111
|
+
|
|
112
|
+
- run: pipx run twine check --strict all/*/*
|
|
113
|
+
|
|
114
|
+
- name: Show wheel tags
|
|
115
|
+
run: ls -la all/*/*
|
|
116
|
+
|
|
117
|
+
# Where a release goes is decided by two things, both read off the GitHub release itself.
|
|
118
|
+
#
|
|
119
|
+
# The tag prefix picks out releases *of this package*. Only v-prefixed tags publish, matching
|
|
120
|
+
# the tag_regex setuptools_scm reads versions from, so any other tag namespace this
|
|
121
|
+
# repository grows -- and the dicom3tools.<snapshot> tags that name the archives it wraps --
|
|
122
|
+
# cannot trigger an upload.
|
|
123
|
+
#
|
|
124
|
+
# The prerelease flag then picks the index. Mark the release a pre-release (and tag it
|
|
125
|
+
# v<version>rc<n>) to rehearse on TestPyPI; a full release publishes to PyPI. Nothing goes to
|
|
126
|
+
# both, so a rehearsal cannot consume the real version number -- which matters because
|
|
127
|
+
# neither index ever lets a version be reused, even after deletion.
|
|
128
|
+
|
|
129
|
+
upload_testpypi:
|
|
130
|
+
name: Publish to TestPyPI
|
|
131
|
+
needs: [check_dist]
|
|
132
|
+
environment: testpypi
|
|
133
|
+
permissions:
|
|
134
|
+
id-token: write
|
|
135
|
+
runs-on: ubuntu-latest
|
|
136
|
+
if: >-
|
|
137
|
+
github.event_name == 'release' && github.event.action == 'published'
|
|
138
|
+
&& startsWith(github.event.release.tag_name, 'v')
|
|
139
|
+
&& github.event.release.prerelease
|
|
140
|
+
|
|
141
|
+
steps:
|
|
142
|
+
- uses: actions/download-artifact@v8
|
|
143
|
+
with:
|
|
144
|
+
pattern: cibw-*
|
|
145
|
+
path: dist
|
|
146
|
+
merge-multiple: true
|
|
147
|
+
|
|
148
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
149
|
+
with:
|
|
150
|
+
repository-url: https://test.pypi.org/legacy/
|
|
151
|
+
|
|
152
|
+
upload_pypi:
|
|
153
|
+
name: Publish to PyPI
|
|
154
|
+
needs: [check_dist]
|
|
155
|
+
environment: pypi
|
|
156
|
+
permissions:
|
|
157
|
+
id-token: write
|
|
158
|
+
runs-on: ubuntu-latest
|
|
159
|
+
if: >-
|
|
160
|
+
github.event_name == 'release' && github.event.action == 'published'
|
|
161
|
+
&& startsWith(github.event.release.tag_name, 'v')
|
|
162
|
+
&& !github.event.release.prerelease
|
|
163
|
+
|
|
164
|
+
steps:
|
|
165
|
+
- uses: actions/download-artifact@v8
|
|
166
|
+
with:
|
|
167
|
+
pattern: cibw-*
|
|
168
|
+
path: dist
|
|
169
|
+
merge-multiple: true
|
|
170
|
+
|
|
171
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
env:
|
|
14
|
+
FORCE_COLOR: 3
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
lint:
|
|
18
|
+
name: Lint
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v7
|
|
22
|
+
- uses: actions/setup-python@v7
|
|
23
|
+
with:
|
|
24
|
+
python-version: "3.12"
|
|
25
|
+
- run: pipx run ruff check --output-format=github .
|
|
26
|
+
- run: pipx run ruff format --check --diff .
|
|
27
|
+
|
|
28
|
+
wheels:
|
|
29
|
+
name: Wheel on ${{ matrix.os }} (${{ matrix.arch }})
|
|
30
|
+
runs-on: ${{ matrix.os }}
|
|
31
|
+
strategy:
|
|
32
|
+
fail-fast: false
|
|
33
|
+
matrix:
|
|
34
|
+
include:
|
|
35
|
+
# One runner per target architecture: the wheels wrap prebuilt single-architecture
|
|
36
|
+
# binaries, so there is nothing to cross-compile -- dicom3toolsUrls.cmake picks the
|
|
37
|
+
# archive from the host processor.
|
|
38
|
+
- os: ubuntu-latest
|
|
39
|
+
arch: x86_64
|
|
40
|
+
# Native arm64 Linux, free for public repositories, and the same runner the binaries
|
|
41
|
+
# themselves are built on in ImagingDataCommons/dicom3tools.
|
|
42
|
+
- os: ubuntu-24.04-arm
|
|
43
|
+
arch: aarch64
|
|
44
|
+
- os: macos-15
|
|
45
|
+
arch: arm64
|
|
46
|
+
- os: macos-15-intel
|
|
47
|
+
arch: x86_64
|
|
48
|
+
- os: windows-latest
|
|
49
|
+
arch: AMD64
|
|
50
|
+
|
|
51
|
+
steps:
|
|
52
|
+
- uses: actions/checkout@v7
|
|
53
|
+
with:
|
|
54
|
+
fetch-depth: 0
|
|
55
|
+
|
|
56
|
+
# Builds the wheel, then runs tests/ against the installed wheel inside cibuildwheel's
|
|
57
|
+
# test environment (test-command in pyproject.toml). That covers the DICOM round-trip,
|
|
58
|
+
# so a broken archive pin, a missing tool or a binary that cannot read DICOM fails here
|
|
59
|
+
# rather than after publishing.
|
|
60
|
+
- uses: pypa/cibuildwheel@v4.2.0
|
|
61
|
+
env:
|
|
62
|
+
CIBW_ARCHS: ${{ matrix.arch }}
|
|
63
|
+
|
|
64
|
+
- name: Report wheel tag
|
|
65
|
+
shell: bash
|
|
66
|
+
run: ls -la wheelhouse/
|
|
67
|
+
|
|
68
|
+
- uses: actions/upload-artifact@v7
|
|
69
|
+
with:
|
|
70
|
+
name: wheel-${{ matrix.os }}-${{ matrix.arch }}
|
|
71
|
+
path: wheelhouse/*.whl
|
|
72
|
+
|
|
73
|
+
# Guards the seam: confirms the archives named in dicom3toolsUrls.cmake still exist at the
|
|
74
|
+
# pinned URLs with the pinned checksums, and that the version recorded next to them still
|
|
75
|
+
# matches the pinned release tag. If the release hosting them is re-cut, this fails with a
|
|
76
|
+
# clear message instead of every wheel job failing on a hash mismatch.
|
|
77
|
+
verify_pins:
|
|
78
|
+
name: Verify archive pins
|
|
79
|
+
runs-on: ubuntu-latest
|
|
80
|
+
steps:
|
|
81
|
+
- uses: actions/checkout@v7
|
|
82
|
+
- name: Check every pinned archive
|
|
83
|
+
run: python .github/scripts/verify_pins.py
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
name: Update binary pins
|
|
2
|
+
|
|
3
|
+
# Tracks new dicom3tools releases and opens a PR moving the pins in dicom3toolsUrls.cmake to
|
|
4
|
+
# them.
|
|
5
|
+
#
|
|
6
|
+
# Upstream publishes a snapshot roughly monthly and ImagingDataCommons/dicom3tools cuts a
|
|
7
|
+
# release from it; without something watching, the pins here quietly age and the wheels keep
|
|
8
|
+
# shipping binaries from whenever someone last remembered. This does the mechanical half --
|
|
9
|
+
# checksums, tag, version -- and leaves the judgement (does CI still pass on all five
|
|
10
|
+
# platforms?) to the PR.
|
|
11
|
+
#
|
|
12
|
+
# The PR is deliberately not auto-merged: a new snapshot can add, rename or drop tools, and
|
|
13
|
+
# CMakeLists.txt fails the build if anything in binaries.txt has disappeared. That failure
|
|
14
|
+
# should be read by a person.
|
|
15
|
+
|
|
16
|
+
on:
|
|
17
|
+
workflow_dispatch:
|
|
18
|
+
inputs:
|
|
19
|
+
tag:
|
|
20
|
+
description: "dicom3tools release tag to pin (default: the latest release)"
|
|
21
|
+
required: false
|
|
22
|
+
type: string
|
|
23
|
+
schedule:
|
|
24
|
+
# Mondays, 06:17 UTC. Off the hour, because scheduled workflows on the hour are the ones
|
|
25
|
+
# GitHub delays most.
|
|
26
|
+
- cron: "17 6 * * 1"
|
|
27
|
+
|
|
28
|
+
permissions:
|
|
29
|
+
# Enough to push the branch and open the PR. Note that this also needs "Allow GitHub Actions
|
|
30
|
+
# to create and approve pull requests" enabled in the repository's Actions settings.
|
|
31
|
+
contents: write
|
|
32
|
+
pull-requests: write
|
|
33
|
+
|
|
34
|
+
concurrency:
|
|
35
|
+
group: ${{ github.workflow }}
|
|
36
|
+
cancel-in-progress: false
|
|
37
|
+
|
|
38
|
+
env:
|
|
39
|
+
BINARIES_REPO: ImagingDataCommons/dicom3tools
|
|
40
|
+
|
|
41
|
+
jobs:
|
|
42
|
+
update:
|
|
43
|
+
name: Refresh pins
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/checkout@v7
|
|
47
|
+
|
|
48
|
+
- uses: actions/setup-python@v7
|
|
49
|
+
with:
|
|
50
|
+
python-version: "3.12"
|
|
51
|
+
|
|
52
|
+
- name: Work out which release to pin
|
|
53
|
+
id: target
|
|
54
|
+
env:
|
|
55
|
+
GH_TOKEN: ${{ github.token }}
|
|
56
|
+
REQUESTED_TAG: ${{ inputs.tag }}
|
|
57
|
+
run: |
|
|
58
|
+
set -euo pipefail
|
|
59
|
+
|
|
60
|
+
if [ -n "$REQUESTED_TAG" ]; then
|
|
61
|
+
tag="$REQUESTED_TAG"
|
|
62
|
+
else
|
|
63
|
+
tag="$(gh api "repos/$BINARIES_REPO/releases/latest" --jq .tag_name)"
|
|
64
|
+
fi
|
|
65
|
+
echo "latest release: $tag"
|
|
66
|
+
|
|
67
|
+
pinned="$(grep -oP '(?<=^set\(DICOM3TOOLS_BINARIES_TAG ")[^"]*' dicom3toolsUrls.cmake)"
|
|
68
|
+
echo "currently pinned: $pinned"
|
|
69
|
+
|
|
70
|
+
if [ "$tag" = "$pinned" ]; then
|
|
71
|
+
echo "pins are already at $tag"
|
|
72
|
+
echo "proceed=false" >> "$GITHUB_OUTPUT"
|
|
73
|
+
exit 0
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
# A release exists from the moment the first build workflow finishes, so it can be
|
|
77
|
+
# seen here with only some of its archives attached. Pinning then is not a failure
|
|
78
|
+
# worth alerting on -- the next run picks it up complete.
|
|
79
|
+
missing=""
|
|
80
|
+
assets="$(gh api "repos/$BINARIES_REPO/releases/tags/$tag" --jq '.assets[].name')"
|
|
81
|
+
for asset in \
|
|
82
|
+
dicom3tools-linux-x86_64.tar.gz \
|
|
83
|
+
dicom3tools-linux-aarch64.tar.gz \
|
|
84
|
+
dicom3tools-macos-arm64.tar.gz \
|
|
85
|
+
dicom3tools-macos-x86_64.tar.gz \
|
|
86
|
+
dicom3tools-windows-x86_64.zip
|
|
87
|
+
do
|
|
88
|
+
grep -qxF "$asset" <<<"$assets" || missing="$missing $asset"
|
|
89
|
+
done
|
|
90
|
+
|
|
91
|
+
if [ -n "$missing" ]; then
|
|
92
|
+
echo "::notice::release $tag is missing$missing -- its build workflows have not all finished. Skipping."
|
|
93
|
+
echo "proceed=false" >> "$GITHUB_OUTPUT"
|
|
94
|
+
exit 0
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
|
98
|
+
echo "pinned=$pinned" >> "$GITHUB_OUTPUT"
|
|
99
|
+
echo "proceed=true" >> "$GITHUB_OUTPUT"
|
|
100
|
+
|
|
101
|
+
- name: Rewrite the pins
|
|
102
|
+
if: steps.target.outputs.proceed == 'true'
|
|
103
|
+
env:
|
|
104
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
105
|
+
run: |
|
|
106
|
+
python scripts/update_dicom3tools_urls.py \
|
|
107
|
+
--repo "$BINARIES_REPO" --tag "${{ steps.target.outputs.tag }}"
|
|
108
|
+
|
|
109
|
+
- name: Confirm the new pins download and match
|
|
110
|
+
if: steps.target.outputs.proceed == 'true'
|
|
111
|
+
run: python .github/scripts/verify_pins.py
|
|
112
|
+
|
|
113
|
+
- name: Read the new wheel version
|
|
114
|
+
if: steps.target.outputs.proceed == 'true'
|
|
115
|
+
id: version
|
|
116
|
+
run: |
|
|
117
|
+
set -euo pipefail
|
|
118
|
+
version="$(grep -oP '(?<=^set\(dicom3tools_version ")[^"]*' dicom3toolsUrls.cmake)"
|
|
119
|
+
echo "version=$version" >> "$GITHUB_OUTPUT"
|
|
120
|
+
|
|
121
|
+
- name: Open a pull request
|
|
122
|
+
if: steps.target.outputs.proceed == 'true'
|
|
123
|
+
uses: peter-evans/create-pull-request@v7
|
|
124
|
+
with:
|
|
125
|
+
branch: update-pins
|
|
126
|
+
base: main
|
|
127
|
+
add-paths: dicom3toolsUrls.cmake
|
|
128
|
+
commit-message: "Pin dicom3tools ${{ steps.target.outputs.tag }}"
|
|
129
|
+
title: "Pin dicom3tools ${{ steps.target.outputs.tag }}"
|
|
130
|
+
delete-branch: true
|
|
131
|
+
body: |
|
|
132
|
+
Moves the binary pins from `${{ steps.target.outputs.pinned }}` to
|
|
133
|
+
[`${{ steps.target.outputs.tag }}`](https://github.com/${{ env.BINARIES_REPO }}/releases/tag/${{ steps.target.outputs.tag }}).
|
|
134
|
+
|
|
135
|
+
Wheels built from this would be version `${{ steps.target.outputs.version }}`.
|
|
136
|
+
|
|
137
|
+
Opened automatically by `.github/workflows/update-pins.yml`. Merging it does not
|
|
138
|
+
publish anything -- releasing is a separate, deliberate step (see the README).
|
|
139
|
+
|
|
140
|
+
**What to check before merging:** that CI builds and tests all five wheels. A new
|
|
141
|
+
upstream snapshot can add, rename or drop programs; if one named in `binaries.txt`
|
|
142
|
+
is gone, the build fails at configure time with a clear message, and the fix is to
|
|
143
|
+
update `binaries.txt` and `[project.scripts]` together.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Wheel layer: download a prebuilt dicom3tools archive and install its programs into the
|
|
2
|
+
# Python package. No compiling happens here -- the archives are built by the workflows in
|
|
3
|
+
# ImagingDataCommons/dicom3tools and attached to its releases.
|
|
4
|
+
#
|
|
5
|
+
# Structured after the plastimatch, dcmqi and s5cmd python distributions:
|
|
6
|
+
# https://github.com/ImagingDataCommons/plastimatch-python-distributions/blob/main/CMakeLists.txt
|
|
7
|
+
|
|
8
|
+
cmake_minimum_required(VERSION 3.15...3.26)
|
|
9
|
+
project(${SKBUILD_PROJECT_NAME} LANGUAGES NONE)
|
|
10
|
+
|
|
11
|
+
# Sets in the current scope:
|
|
12
|
+
# - dicom3tools_archive_url
|
|
13
|
+
# - dicom3tools_archive_sha256
|
|
14
|
+
# - dicom3tools_archive_filename
|
|
15
|
+
# - dicom3tools_snapshot
|
|
16
|
+
# - dicom3tools_version
|
|
17
|
+
include(${CMAKE_CURRENT_SOURCE_DIR}/dicom3toolsUrls.cmake)
|
|
18
|
+
|
|
19
|
+
message(STATUS "dicom3tools snapshot: ${dicom3tools_snapshot} (version ${dicom3tools_version})")
|
|
20
|
+
message(STATUS "dicom3tools archive: ${dicom3tools_archive_filename}")
|
|
21
|
+
message(STATUS "dicom3tools archive url: ${dicom3tools_archive_url}")
|
|
22
|
+
|
|
23
|
+
#
|
|
24
|
+
# Download & extract archive
|
|
25
|
+
#
|
|
26
|
+
# The dicom3tools archives are flat: `make install` puts everything in bin/<platform>/ and the
|
|
27
|
+
# build workflows tar that directory's *contents*, so there is no bin/ and no top-level
|
|
28
|
+
# directory inside the archive. Every program lands directly in extract_dir.
|
|
29
|
+
#
|
|
30
|
+
set(download_dir "${PROJECT_BINARY_DIR}")
|
|
31
|
+
set(extract_dir "${PROJECT_BINARY_DIR}/dicom3tools-binary-distribution")
|
|
32
|
+
include(FetchContent)
|
|
33
|
+
FetchContent_Populate(dicom3tools
|
|
34
|
+
URL ${dicom3tools_archive_url}
|
|
35
|
+
URL_HASH SHA256=${dicom3tools_archive_sha256}
|
|
36
|
+
DOWNLOAD_DIR ${download_dir}
|
|
37
|
+
SOURCE_DIR "${extract_dir}"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
#
|
|
41
|
+
# Install programs
|
|
42
|
+
#
|
|
43
|
+
|
|
44
|
+
set(_permissions PERMISSIONS
|
|
45
|
+
OWNER_READ OWNER_WRITE OWNER_EXECUTE
|
|
46
|
+
GROUP_READ GROUP_EXECUTE
|
|
47
|
+
WORLD_READ WORLD_EXECUTE
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Files that are documentation rather than tools. The build workflows copy these in from the
|
|
51
|
+
# source tree so that the license travels with the binaries it covers and the wheel can record
|
|
52
|
+
# which upstream snapshot it wraps.
|
|
53
|
+
set(_documents COPYRIGHT VERSION.txt)
|
|
54
|
+
|
|
55
|
+
file(GLOB _entries LIST_DIRECTORIES false "${extract_dir}/*")
|
|
56
|
+
|
|
57
|
+
set(_programs "")
|
|
58
|
+
set(_installed_documents "")
|
|
59
|
+
foreach(_entry IN LISTS _entries)
|
|
60
|
+
get_filename_component(_name "${_entry}" NAME)
|
|
61
|
+
if(_name IN_LIST _documents)
|
|
62
|
+
list(APPEND _installed_documents "${_entry}")
|
|
63
|
+
continue()
|
|
64
|
+
endif()
|
|
65
|
+
# `make install` leaves himrunid behind as a zero byte file -- it is generated from a
|
|
66
|
+
# template that produces nothing without a site UID. Shipping it would put an unrunnable
|
|
67
|
+
# file in bin/ for the Python wrapper layer to discover and offer as a callable.
|
|
68
|
+
file(SIZE "${_entry}" _size)
|
|
69
|
+
if(_size EQUAL 0)
|
|
70
|
+
message(STATUS "Skipping empty file: ${_name}")
|
|
71
|
+
continue()
|
|
72
|
+
endif()
|
|
73
|
+
list(APPEND _programs "${_entry}")
|
|
74
|
+
endforeach()
|
|
75
|
+
|
|
76
|
+
list(LENGTH _programs _program_count)
|
|
77
|
+
if(_program_count EQUAL 0)
|
|
78
|
+
message(FATAL_ERROR
|
|
79
|
+
"No programs found in ${dicom3tools_archive_filename}. The archive layout changed: it is "
|
|
80
|
+
"expected to be flat, with every program at the archive root.")
|
|
81
|
+
endif()
|
|
82
|
+
message(STATUS "Installing ${_program_count} dicom3tools programs")
|
|
83
|
+
|
|
84
|
+
# The Windows archive carries the Cygwin runtime DLLs the .exe files need alongside them, and
|
|
85
|
+
# they are picked up by the same glob. They have to travel into the wheel in the same directory
|
|
86
|
+
# as the executables or nothing runs on a machine without Cygwin installed.
|
|
87
|
+
install(PROGRAMS ${_programs} DESTINATION "dicom3tools/bin" ${_permissions})
|
|
88
|
+
|
|
89
|
+
#
|
|
90
|
+
# Check that every program promised a console script is actually here
|
|
91
|
+
#
|
|
92
|
+
# Fail at configure time with a useful message rather than leaving pyproject.toml's
|
|
93
|
+
# [project.scripts] pointing at something the wheel does not contain -- that failure would
|
|
94
|
+
# otherwise surface as a launcher on the user's PATH that raises FileNotFoundError.
|
|
95
|
+
#
|
|
96
|
+
file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/binaries.txt" _wanted REGEX "^[^#]")
|
|
97
|
+
|
|
98
|
+
set(_missing "")
|
|
99
|
+
foreach(_binary IN LISTS _wanted)
|
|
100
|
+
string(STRIP "${_binary}" _binary)
|
|
101
|
+
if(_binary STREQUAL "")
|
|
102
|
+
continue()
|
|
103
|
+
endif()
|
|
104
|
+
# Checked with and without .exe rather than through CMAKE_EXECUTABLE_SUFFIX, which is empty
|
|
105
|
+
# for a LANGUAGES NONE project: on Windows most tools are .exe, but a few (dcanon, dccmp,
|
|
106
|
+
# dcdiff) are installed shell scripts and carry no suffix even there.
|
|
107
|
+
if(NOT EXISTS "${extract_dir}/${_binary}" AND NOT EXISTS "${extract_dir}/${_binary}.exe")
|
|
108
|
+
list(APPEND _missing "${_binary}")
|
|
109
|
+
endif()
|
|
110
|
+
endforeach()
|
|
111
|
+
|
|
112
|
+
if(_missing)
|
|
113
|
+
string(REPLACE ";" " " _missing_pretty "${_missing}")
|
|
114
|
+
message(FATAL_ERROR
|
|
115
|
+
"Programs listed in binaries.txt are not present in ${dicom3tools_archive_filename}: "
|
|
116
|
+
"${_missing_pretty}. Either the archive is incomplete -- a dicom3tools build that loses "
|
|
117
|
+
"its awk-generated headers silently drops whole converters -- or the tool was renamed "
|
|
118
|
+
"upstream and binaries.txt needs updating.")
|
|
119
|
+
endif()
|
|
120
|
+
|
|
121
|
+
#
|
|
122
|
+
# Install upstream license and provenance
|
|
123
|
+
#
|
|
124
|
+
# dicom3tools is distributed under David Clunie's BSD-style license, which the wheel has to
|
|
125
|
+
# reproduce because it redistributes the binaries in binary form. VERSION.txt records the
|
|
126
|
+
# upstream snapshot the binaries were built from.
|
|
127
|
+
#
|
|
128
|
+
if(_installed_documents)
|
|
129
|
+
install(FILES ${_installed_documents} DESTINATION "dicom3tools/share")
|
|
130
|
+
else()
|
|
131
|
+
message(WARNING
|
|
132
|
+
"Neither COPYRIGHT nor VERSION.txt is present in ${dicom3tools_archive_filename}. Releases "
|
|
133
|
+
"cut before the build workflows started copying them in do not have them; the wheel will "
|
|
134
|
+
"be built without the upstream license text.")
|
|
135
|
+
endif()
|