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.
@@ -0,0 +1,4 @@
1
+ node: $Format:%H$
2
+ node-date: $Format:%cI$
3
+ describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$
4
+ ref-names: $Format:%D$
@@ -0,0 +1 @@
1
+ .git_archival.txt export-subst
@@ -0,0 +1,11 @@
1
+ version: 2
2
+ updates:
3
+ # Maintain dependencies for GitHub Actions
4
+ - package-ecosystem: "github-actions"
5
+ directory: "/"
6
+ schedule:
7
+ interval: "weekly"
8
+ groups:
9
+ actions:
10
+ patterns:
11
+ - "*"
@@ -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,9 @@
1
+ build/
2
+ dist/
3
+ wheelhouse/
4
+ *.egg-info/
5
+ __pycache__/
6
+ *.py[cod]
7
+ .pytest_cache/
8
+ .venv/
9
+ src/dicom3tools/_version.py
@@ -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()