diff --git a/.github/workflows/update-dcmqi.yml b/.github/workflows/update-dcmqi.yml index f53bd80..cc20520 100644 --- a/.github/workflows/update-dcmqi.yml +++ b/.github/workflows/update-dcmqi.yml @@ -50,15 +50,32 @@ jobs: echo "Assets: $assets" # Download assets and compute checksums + # Also extract binary names from one archive (Linux) declare -A checksums + binaries="" for asset in $assets; do echo "Downloading $asset..." url="https://github.com/QIICR/dcmqi/releases/download/$latest_tag/$asset" - sha256=$(curl -sL "$url" | sha256sum | awk '{print $1}') + tmpfile=$(mktemp) + curl -sL "$url" -o "$tmpfile" + sha256=$(sha256sum "$tmpfile" | awk '{print $1}') checksums["$asset"]="$sha256" echo " SHA256: $sha256" + + # Discover binaries from the Linux archive + if [[ "$asset" == *-linux.tar.gz ]] && [ -z "$binaries" ]; then + echo "Discovering binaries from $asset..." + binaries=$(tar -tzf "$tmpfile" | grep '^[^/]*/bin/[^/]*$' | grep -v '/$' | sed 's|.*/bin/||' | sort) + echo " Discovered binaries: $(echo $binaries | tr '\n' ' ')" + fi + rm -f "$tmpfile" done + if [ -z "$binaries" ]; then + echo "ERROR: Could not discover binaries from Linux archive" + exit 1 + fi + # Determine macOS asset pattern has_mac_split=false for asset in $assets; do @@ -140,6 +157,12 @@ jobs: echo 'set(dcmqi_archive_url "https://github.com/QIICR/dcmqi/releases/download/v${version}/${dcmqi_archive_filename}")' } > dcmqiUrls.cmake + # Update binaries.txt with discovered binary list + echo "$binaries" > binaries.txt + + # Update pyproject.toml [project.scripts] to match binaries.txt + python3 -c 'import re; b=open("binaries.txt").read().split(); blk="[project.scripts]\n"+"".join(f"{x} = \"dcmqi:{x}\"\n" for x in b)+"\n"; c=open("pyproject.toml").read(); open("pyproject.toml","w").write(re.sub(r"\[project\.scripts\]\n[^\[]*\n",blk,c))' + # Update README.md to reference the new dcmqi release version sed -i "s|dcmqi v${current_version}](https://github.com/QIICR/dcmqi/releases/tag/v${current_version})|dcmqi v${latest_version}](https://github.com/QIICR/dcmqi/releases/tag/v${latest_version})|" README.md @@ -149,7 +172,7 @@ jobs: branch="dcmqi-v${latest_version}" git checkout -b "$branch" - git add dcmqiUrls.cmake README.md + git add dcmqiUrls.cmake binaries.txt pyproject.toml README.md git commit -m "feat: update dcmqi from version v$current_version to v$latest_version" git push origin "$branch" diff --git a/CMakeLists.txt b/CMakeLists.txt index 40ceecd..533a6e2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,16 +32,8 @@ set(_permissions PERMISSIONS WORLD_READ WORLD_EXECUTE ) -set(itkimage2segimage_executable_name "itkimage2segimage${CMAKE_EXECUTABLE_SUFFIX}") -set(segimage2itkimage_executable_name "segimage2itkimage${CMAKE_EXECUTABLE_SUFFIX}") -set(tid1500writer_executable_name "tid1500writer${CMAKE_EXECUTABLE_SUFFIX}") -set(tid1500reader_executable_name "tid1500reader${CMAKE_EXECUTABLE_SUFFIX}") -set(itkimage2paramap_executable_name "itkimage2paramap${CMAKE_EXECUTABLE_SUFFIX}") -set(paramap2itkimage_executable_name "paramap2itkimage${CMAKE_EXECUTABLE_SUFFIX}") +file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/binaries.txt" _dcmqi_binaries) -install(PROGRAMS ${extract_dir}/bin/${itkimage2segimage_executable_name} DESTINATION "dcmqi/bin" ${_permissions}) -install(PROGRAMS ${extract_dir}/bin/${segimage2itkimage_executable_name} DESTINATION "dcmqi/bin" ${_permissions}) -install(PROGRAMS ${extract_dir}/bin/${tid1500writer_executable_name} DESTINATION "dcmqi/bin" ${_permissions}) -install(PROGRAMS ${extract_dir}/bin/${tid1500reader_executable_name} DESTINATION "dcmqi/bin" ${_permissions}) -install(PROGRAMS ${extract_dir}/bin/${itkimage2paramap_executable_name} DESTINATION "dcmqi/bin" ${_permissions}) -install(PROGRAMS ${extract_dir}/bin/${paramap2itkimage_executable_name} DESTINATION "dcmqi/bin" ${_permissions}) +foreach(_binary IN LISTS _dcmqi_binaries) + install(PROGRAMS "${extract_dir}/bin/${_binary}${CMAKE_EXECUTABLE_SUFFIX}" DESTINATION "dcmqi/bin" ${_permissions}) +endforeach() diff --git a/binaries.txt b/binaries.txt new file mode 100644 index 0000000..4b9727e --- /dev/null +++ b/binaries.txt @@ -0,0 +1,6 @@ +itkimage2segimage +segimage2itkimage +tid1500writer +tid1500reader +itkimage2paramap +paramap2itkimage diff --git a/src/dcmqi/__init__.py b/src/dcmqi/__init__.py index 0e1564d..5135cf3 100644 --- a/src/dcmqi/__init__.py +++ b/src/dcmqi/__init__.py @@ -8,25 +8,13 @@ import subprocess import sys +from collections.abc import Callable from importlib.metadata import distribution from pathlib import Path from typing import NoReturn from ._version import version as __version__ -__all__ = [ - "__version__", - "itkimage2paramap", - "itkimage2segimage", - "paramap2itkimage", - "segimage2itkimage", - "tid1500reader", - "tid1500writer", -] - - -DCMQI_BIN_DIR: Path = Path(__file__).parent - def _lookup(name: str) -> Path: executable_path = f"dcmqi/bin/{name}" @@ -43,31 +31,36 @@ def _program(name: str, args: list[str]) -> int: return subprocess.call([_lookup(name), *args], close_fds=False) -def itkimage2segimage() -> NoReturn: - """Run the itkimage2segimage executable with arguments passed to a Python script.""" - raise SystemExit(_program("itkimage2segimage", sys.argv[1:])) - - -def segimage2itkimage() -> NoReturn: - """Run the segimage2itkimage executable with arguments passed to a Python script.""" - raise SystemExit(_program("segimage2itkimage", sys.argv[1:])) - +def _make_wrapper(name: str) -> Callable[[], NoReturn]: + def _wrapper() -> NoReturn: + raise SystemExit(_program(name, sys.argv[1:])) -def tid1500writer() -> NoReturn: - """Run the tid1500writer executable with arguments passed to a Python script.""" - raise SystemExit(_program("tid1500writer", sys.argv[1:])) + _wrapper.__name__ = name + _wrapper.__qualname__ = name + _wrapper.__doc__ = ( + f"Run the {name} executable with arguments passed to a Python script." + ) + return _wrapper -def tid1500reader() -> NoReturn: - """Run the tid1500reader executable with arguments passed to a Python script.""" - raise SystemExit(_program("tid1500reader", sys.argv[1:])) - - -def itkimage2paramap() -> NoReturn: - """Run the itkimage2paramap executable with arguments passed to a Python script.""" - raise SystemExit(_program("itkimage2paramap", sys.argv[1:])) - - -def paramap2itkimage() -> NoReturn: - """Run the paramap2itkimage executable with arguments passed to a Python script.""" - raise SystemExit(_program("paramap2itkimage", sys.argv[1:])) +def _discover_binaries() -> list[str]: + """Return names of all executables installed in dcmqi/bin/.""" + files = distribution("dcmqi").files + if files is None: + return [] + binaries = [] + for _file in files: + parts = Path(str(_file)).parts + if len(parts) == 3 and parts[0] == "dcmqi" and parts[1] == "bin": + # Strip platform suffix (.exe on Windows) + name = Path(parts[2]).stem + binaries.append(name) + return sorted(binaries) + + +# Dynamically create wrapper functions for each installed binary +_binaries = _discover_binaries() +for _name in _binaries: + globals()[_name] = _make_wrapper(_name) + +__all__ = ["__version__", *_binaries] # noqa: PLE0604 diff --git a/tests/test_executable.py b/tests/test_executable.py index 24ecf21..4a8157a 100644 --- a/tests/test_executable.py +++ b/tests/test_executable.py @@ -11,28 +11,16 @@ from . import push_argv -all_tools = pytest.mark.parametrize( - "tool", - [ - "itkimage2segimage", - "segimage2itkimage", - "tid1500writer", - "tid1500reader", - "itkimage2paramap", - "paramap2itkimage", - ], +_BINARIES_FILE = Path(__file__).parent.parent / "binaries.txt" +_EXPECTED_TOOLS = sorted( + line.strip() for line in _BINARIES_FILE.read_text().splitlines() if line.strip() ) +all_tools = pytest.mark.parametrize("tool", _EXPECTED_TOOLS) + all_tools_version = pytest.mark.parametrize( ("tool", "expected_version"), - [ - ("itkimage2segimage", "1.0"), - ("segimage2itkimage", "1.0"), - ("tid1500writer", "1.0"), - ("tid1500reader", "1.0"), - ("itkimage2paramap", "1.0"), - ("paramap2itkimage", "1.0"), - ], + [(t, "1.0") for t in _EXPECTED_TOOLS], )