Skip to content

COMP: Build DCMTK with FetchContent instead of ExternalProject#6397

Closed
hjmjohnson wants to merge 5 commits into
InsightSoftwareConsortium:mainfrom
hjmjohnson:dcmtk-fetchcontent
Closed

COMP: Build DCMTK with FetchContent instead of ExternalProject#6397
hjmjohnson wants to merge 5 commits into
InsightSoftwareConsortium:mainfrom
hjmjohnson:dcmtk-fetchcontent

Conversation

@hjmjohnson

@hjmjohnson hjmjohnson commented Jun 4, 2026

Copy link
Copy Markdown
Member

Builds DCMTK with FetchContent + in-scope add_subdirectory instead of ExternalProject_Add, so DCMTK is configured in ITK's own CMake scope and consumes ITK's codec libraries as the live imported targets (ITK::itktiff/itkpng/zlib/itkjpeg) — the GDCM model @blowekamp described in #6393, with no $<TARGET_FILE:> marshalling or :::-separated CMAKE_ARGS.

This is the full FetchContent alternative to #6393 (the surgical ExternalProject fix). Structured as one commit per integration hurdle so each can be reviewed independently.

Commits (one per hurdle)

Commit Hurdle
COMP: Build DCMTK in-scope with FetchContent core mechanism: FetchContent_MakeAvailable + itk_module_use() dependency bootstrap + codec-variable wiring + DCMTK_USE_ICU-honoring charset config
COMP: Export DCMTK targets through ITK's module namespace itk_module_target() naming, ITK::<lib> aliases, build-tree targets file
COMP: Silence DCMTK third-party compiler warnings itk_module_warnings_disable (DCMTK compiles in ITK's /W4 scope)
COMP: Install DCMTK libraries and headers with ITK install libs into ITK's export set + DCMTK headers into ITK's include tree
COMP: Build in-scope DCMTK alongside MINC on MSVC snapshot/restore CMAKE_REQUIRED_*; assert MSVC HAVE_TMPNAM so MINC's shared-cache probe is not poisoned
Why DCMTK differs from GDCM (and why this needs FetchContent)

GDCM is add_subdirectory()'d, so it already runs in ITK's scope and can take ITK's imported targets directly. DCMTK was an ExternalProject — a separate CMake process where imported targets can't cross the boundary, forcing the $<TARGET_FILE:> + ::: marshalling. FetchContent_MakeAvailable is essentially "fetch then add_subdirectory", which puts DCMTK in ITK's scope and removes that marshalling. EXCLUDE_FROM_ALL is intentionally not used on the in-scope DCMTK directory: it leaked into ITK's own test targets and dropped them from the ALL build. DCMTK's libraries are required by ITKIODCMTK (so they belong in ALL) and DCMTK builds no apps (BUILD_APPS OFF) or tests, so nothing extraneous is added.

Validation (Windows / MSVC)
  • Configure + generate succeed, including install(EXPORT ITKTargets) (where install/export-set inconsistencies surface).
  • DCMTK compiles in ITK's Ninja graph and ITKIODCMTK links; DCMTK libraries are ITK-named (itkdcmdata-6.0.lib …).
  • The per-file MSVC D9025 warning bleed drops to zero after the warnings commit.
  • Full default-modules build + install-tree validation is covered by CI (all required checks green).
AI assistance
  • Tool: Claude Code (claude-opus-4-8)
  • Role: implemented the FetchContent conversion and worked through each integration hurdle (dependency bootstrapping, module-namespace export, warning isolation, install, MINC cross-module cache poisoning) against ITK's module macros.
  • Built and verified on Windows/MSVC (DCMTK + ITKIODCMTK, exit 0) before pushing.

Relates to #6393, #5539; supersedes the ExternalProject approach if the team prefers FetchContent.

@github-actions github-actions Bot added type:Compiler Compiler support or related warnings type:Infrastructure Infrastructure/ecosystem related changes, such as CMake or buildbots area:ThirdParty Issues affecting the ThirdParty module labels Jun 4, 2026
@greptile-apps

This comment was marked as resolved.

Comment thread Modules/ThirdParty/DCMTK/CMakeLists.txt Outdated
Comment thread Modules/ThirdParty/DCMTK/CMakeLists.txt Outdated
@hjmjohnson

Copy link
Copy Markdown
Member Author

The first Windows CI run surfaced a real bug in the in-scope conversion that my local target-by-target builds had missed (I was building ITKIODCMTK directly, never the full all):

Pixi-Cxx (windows-2022) failed with 3699/3849 tests "Not Run" — the test executables were never built. Root cause: EXCLUDE_FROM_ALL on the in-scope DCMTK add_subdirectory leaked into ITK's own *-test modules (added later in the same ITKModuleEnablement loop) and dropped every test executable from the ALL target.

Fixed in bc50dc5 by dropping EXCLUDE_FROM_ALL entirely: DCMTK's libraries are required by ITKIODCMTK so they belong in ALL anyway, and DCMTK builds no apps (BUILD_APPS OFF) or tests, so nothing extraneous is pulled in. As a bonus this lets plain FetchContent_MakeAvailable() cover every CMake version (>= 3.14), so the 3.30 FetchContent_Populate deprecation (the earlier @greptile-apps P2) is resolved without a version gate.

Verified locally: cmake --build build now links the test drivers and a deliberately-deleted ITKImageGridGTestDriver.exe is rebuilt by ALL. Re-running CI on bc50dc5.

@greptile-apps

greptile-apps Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Good diagnosis and clean fix. A few observations on bc50dc5:

The fix is correct. EXCLUDE_FROM_ALL on an add_subdirectory sets a directory property that propagates to all targets defined in that directory and its descendants. When ITK's module enablement loop later calls into subdirectories that inherit that property scope, targets defined there also get excluded from ALL. Dropping it entirely is the right call given:

  1. DCMTK's libraries are a hard dependency of ITKIODCMTK — they'd be built regardless.
  2. BUILD_APPS OFF and no tests mean nothing extraneous lands in ALL.

The comment block at ~line 190 documents this reasoning well, which is good since "why not EXCLUDE_FROM_ALL" will come up in review.

One thing to double-check with the CI run: ITKDCMTK_NO_SRC 1 (line 7) tells ITK's module machinery there's no source to compile in the module directory itself. That's still accurate post-FetchContent (DCMTK sources live under dcmtk_SOURCE_DIR, not Modules/ThirdParty/DCMTK/src/), so that should be fine — but worth confirming ITKDCMTKModule still links correctly in CI, since ITKDCMTK_NO_SRC affects how itk_module_impl() handles that target.

Minor — ICU path is now inconsistent: The ExternalProject_Add(icu ...) block (lines ~68–95) is gated on DCMTK_USE_ICU which defaults off, but if that path is exercised the ICU_ROOT_DIR wiring into the in-scope DCMTK (set(ICU_ROOT "${ICU_ROOT_DIR}" ...)) relies on the ExternalProject completing before DCMTK's configure step runs — which isn't guaranteed in an in-scope build. Since this path isn't tested by CI, a TODO comment there would be appropriate to flag it for a follow-up.

@hjmjohnson

Copy link
Copy Markdown
Member Author

Root cause of the Windows CI failure — a fundamental blocker for the in-scope approach

I chased the "3699 tests Not Run" down to its actual cause, and it is not about tests or EXCLUDE_FROM_ALL (that was a red herring — the test failures were just fallout from the build aborting). The Windows build stops here, at compile step ~54 of 5868:

Modules/ThirdParty/MINC/.../libminc/.../volume_io/Prog_utils/files.c.obj
fatal error C1189: "System defines neither mkstemp(), tempnam(), nor tmpnam()"
ninja: build stopped: subcommand failed.

Why

DCMTK and ITK's MINC both probe the C library with identical cache-variable names:

Module Code
MINC CHECK_FUNCTION_EXISTS(mkstemp HAVE_MKSTEMP), ... (tmpnam HAVE_TMPNAM), ... (tempnam HAVE_TEMPNAM)
DCMTK CHECK_FUNCTION_EXISTS(... HAVE_<FUNC>) for a large list of libc functions, run in GenerateDCMTKConfigure.cmake

With FetchContent + add_subdirectory, DCMTK is configured in ITK's shared CMake scope and runs first. CHECK_FUNCTION_EXISTS caches its result in the named variable, and the call is a no-op if that cache entry already exists — so MINC inherits DCMTK's results (and/or DCMTK's leftover CMAKE_REQUIRED_* state), ends up with none of the three defined, and fires its #error. The same class of collision produced the GDCM strncasecmp (GDCM_HAVE__STRNICMP) failure seen earlier.

Why this is the crux

This is precisely what the ExternalProject design prevents: DCMTK is configured in a separate CMake process, so its hundreds of HAVE_* probes and CMAKE_REQUIRED_* mutations can never pollute ITK's cache or sibling third-party modules (MINC, GDCM, …). FetchContent's whole premise — one shared scope — is what reintroduces the pollution.

A fix would mean save/restoring all CMAKE_REQUIRED_* around the DCMTK subdirectory and preventing every HAVE_* cache-name collision between DCMTK and ~15 vendored ITK third-party modules — fragile whack-a-mole that would need revisiting every time DCMTK or a sibling adds a probe.

Recommendation

This confirms the in-scope/FetchContent direction is not viable for DCMTK without process isolation, which is the very thing ExternalProject already provides. I'd suggest keeping this PR as a documented exploration and landing the surgical ExternalProject fix in #6393 instead. Happy to pursue mitigation if the team wants to, but it would be a substantial and brittle undertaking.

@hjmjohnson hjmjohnson marked this pull request as draft June 4, 2026 23:06
@blowekamp

Copy link
Copy Markdown
Member

Using fetch content over external project remove the need to export DCMTK targets with ITK specific code. And similarly describe ITK target to DCMTK with variables. This is extra code burdens ITK developers.

Can we determine what are the potential conflicted CMake Cache variables between ITK and DCMTK. Perhaps by examining a cache of ITK (without DCMTK) and one of a standalone DCMTK? Then we might need to modify a DCMTK or some other ITK subproject to address the conflict.

For the HAVE_MKSTEMP above, perhaps MINC could be modified to use the MINC prefix for more of there CMake variables.

@hjmjohnson hjmjohnson force-pushed the dcmtk-fetchcontent branch from bc50dc5 to e7fb08c Compare June 5, 2026 17:55
@hjmjohnson

Copy link
Copy Markdown
Member Author

@blowekamp Took your suggestion — enumerated the ITK↔DCMTK cache-variable collisions and fixed them in-tree (bf19e389). The in-scope build now configures with a deterministic MINC config.h.

Collision analysis (your "diff the caches" approach)

Rather than two separate caches, I diffed MINC's generated config.h with vs. without the in-scope DCMTK in the same build. DCMTK's CHECK_FUNCTION_EXISTS probes leak into MINC two ways:

  1. Direct name collisions — DCMTK caches HAVE_MKSTEMP, HAVE_TMPNAM, HAVE_TEMPNAM (and the rest of MINC's libc-probe names). check_function_exists is a no-op when its result var is already cached, so MINC inherited DCMTK's values → all three of mkstemp/tempnam/tmpnam dropped → the Windows #error "System defines neither...".
  2. Indirect / vestigial macrosconfig.h.cmake references HAVE_FORK/HAVE_VFORK, but MINC only probes the HAVE_WORKING_* variants (the C sources key on those). With ExternalProject those macros stayed #undef; in-scope, DCMTK's HAVE_FORK/HAVE_VFORK leaked straight through. (Other gap macros — HAVE_NDIR_H, HAVE_INT16_T, HAVE_INT32_T — are not cached by DCMTK, so they did not leak.)
Fix
  • MINC (libminc/CMakeLists.txt): probe into MINC_-prefixed cache variables via small minc_check_function_exists/minc_check_symbol_exists wrappers, then set the unprefixed name locally for config.h — so config.h.cmake and the vendored C are untouched. Pinned the vestigial HAVE_FORK/HAVE_VFORK to MINC's own HAVE_WORKING_* results.
  • DCMTK (Modules/ThirdParty/DCMTK/CMakeLists.txt): snapshot/restore CMAKE_REQUIRED_* around FetchContent_MakeAvailable(dcmtk) so DCMTK's probe state can't perturb sibling feature tests.
Validation

Local (Linux, in-scope DCMTK + MINC, internal everything):

  • MINC config.h is now byte-identical with vs. without DCMTK — the leak is gone and the result is deterministic.
  • The refactor is behavior-preserving: config.h matches the pre-change build except the two intended HAVE_FORK/HAVE_VFORK deltas (now driven by MINC's own fork/vfork probe instead of left unset).
  • itkminc2 builds in both configurations; pre-commit run --all-files passes.

The actual Windows #error can't be reproduced on Linux (mkstemp exists here), so Windows CI on bf19e389 is the first executor of the cross-platform resolution. The mechanism — MINC's prefixed probes run independently of DCMTK's cache — is what the Linux determinism test confirms.

@hjmjohnson hjmjohnson force-pushed the dcmtk-fetchcontent branch from bf19e38 to f08a836 Compare June 5, 2026 20:53
@hjmjohnson

Copy link
Copy Markdown
Member Author

Reworked the MINC fix (f08a836) — the earlier cache-isolation approach was wrong (it made MINC run its own CHECK_FUNCTION_EXISTS(tmpnam), which also fails on MSVC, so it would have broken MINC on Windows even without DCMTK). Replaced with the upstream-blessed override.

Confirmed root cause

main/Windows builds MINC fine. The in-scope DCMTK runs its CHECK_*_EXISTS probes in ITK's shared scope and caches HAVE_TMPNAM=0 on MSVC — its tmpnam probe uses CHECK_FUNCTION_EXISTS, which can't detect MSVC CRT functions (self-declared prototype fails to link). MINC's CHECK_FUNCTION_EXISTS(tmpnam HAVE_TMPNAM) is then a cache-hit no-op, so MINC inherits 0; with none of mkstemp/tempnam/tmpnam defined, files.c / netcdf_convenience.c hit the #error. On main MINC benignly inherits a correct HAVE_TMPNAM=1 cached by another module first.

Fix
  • Modules/ThirdParty/MINC/.../libminc/CMakeLists.txt: tmpnam() exists in the MSVC CRT, so if(MSVC) set(HAVE_TMPNAM 1) — mirrors MINC's own config.h.msvc-win32. The local set() also overrides any value left in the shared cache, so it's robust to the DCMTK pollution regardless of probe order.
  • Modules/ThirdParty/DCMTK/CMakeLists.txt: snapshot/restore CMAKE_REQUIRED_* around FetchContent_MakeAvailable(dcmtk) (hygiene — keeps DCMTK's probe state from perturbing sibling checks).

Inert on non-MSVC (Linux/macOS config.h unchanged); Windows CI is the first executor of the MSVC path.

@hjmjohnson hjmjohnson marked this pull request as ready for review June 6, 2026 01:37
Replace the DCMTK ExternalProject with FetchContent + add_subdirectory so
DCMTK is configured in ITK's own CMake scope (the GDCM model). DCMTK now
consumes ITK's codec libraries as the live imported targets
(ITK::itktiff/itkpng/zlib/itkjpeg) through the standard *_LIBRARIES
variables -- no $<TARGET_FILE:> extraction or :::-separated CMAKE_ARGS.

itk_module_use() is called up front so the codec variables are populated
before DCMTK is configured. FetchContent_MakeAvailable(dcmtk) does the
add_subdirectory; EXCLUDE_FROM_ALL is not used because on the in-scope
DCMTK directory it leaks into ITK's own test targets and drops them from
the ALL build. Charset conversion honors the DCMTK_USE_ICU opt-in, wiring
ICU_ROOT through when set and otherwise using DCMTK's built-in oficonv
data. Build-tree namespace aliases are a placeholder pending full
module-target export.
Replace the placeholder build-tree aliases with itk_module_target(), which
applies ITK's output naming (itk<lib>-X.Y), creates the namespaced
ITK::<lib> aliases that itk_module_impl() links ITKDCMTKModule against, and
appends DCMTK's targets to ITK's build-tree targets file. The targets file
variable is set during module enablement, so this works before
itk_module_impl(). Installation is deferred, hence NO_INSTALL.
Disable warnings for the DCMTK subdirectory with itk_module_warnings_disable
before it is configured. ITK builds at /W4 while DCMTK builds at /W3, so
every DCMTK source previously emitted an MSVC D9025 flag-override warning on
top of DCMTK's own diagnostics. itk_module_impl() disables warnings for
third-party modules, but DCMTK is added before that call, so apply it here.
Drop NO_INSTALL so itk_module_target() installs the DCMTK libraries into
ITK's library directory and export set, and install each DCMTK module's
public headers (<module>/include/dcmtk) plus the generated config headers
into ITK's include tree so consumers of an installed ITK can include
<dcmtk/...>. install(EXPORT ITKTargets) generates cleanly: the DCMTK
targets' interface dependencies are ITK's exported codec targets, other
DCMTK targets, or system libraries.
@hjmjohnson hjmjohnson force-pushed the dcmtk-fetchcontent branch from f08a836 to f3fa220 Compare June 6, 2026 01:55
@hjmjohnson hjmjohnson requested a review from blowekamp June 6, 2026 11:50
@hjmjohnson

Copy link
Copy Markdown
Member Author

@blowekamp. This ia an attempt to use FetchContent for DCMTK as you previously indicated would be desirable. I don't have a good use case for this, so I would like to leave it in your hands to review and determine next steps.

1). I am happy to abandon this work if it is not what you were hoping for.
2). I am happy for you to take this over and do with it what you want.

Hans

Building DCMTK in ITK's shared CMake scope (FetchContent + add_subdirectory)
runs DCMTK's configure in that scope, with two side effects on sibling
third-party modules:

- Leftover CMAKE_REQUIRED_* state from DCMTK's probes. Snapshot and restore it
  around FetchContent_MakeAvailable(dcmtk).

- DCMTK's GenerateDCMTKConfigure.cmake globally redefines standard CMake check
  commands (CHECK_FUNCTION_EXISTS on Windows, CHECK_CXX_SYMBOL_EXISTS). CMake
  commands are global, so those overrides leak into ITK modules configured
  afterward: their CHECK_FUNCTION_EXISTS calls run DCMTK's header-less
  check_symbol_exists, report the function missing, and the module trips a
  #error -- MINC on tmpnam, GDCM on _strnicmp, depending on configure order.
  Restore the real implementations after DCMTK by including a copy of each
  module under a fresh path (include_guard(GLOBAL) blocks a plain re-include),
  fixing every dependent module regardless of order.

The ExternalProject build never hit either because DCMTK configures in a
separate CMake process.
@hjmjohnson hjmjohnson force-pushed the dcmtk-fetchcontent branch from f3fa220 to db0fcf2 Compare June 6, 2026 22:31
@hjmjohnson

Copy link
Copy Markdown
Member Author

Assuming this work is not a desirable path, so closing this PR.

@hjmjohnson hjmjohnson closed this Jun 11, 2026
@blowekamp

Copy link
Copy Markdown
Member

I would like to come back to this to revisit. It looked like very good progress was made, but I have not had time to look closely.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:ThirdParty Issues affecting the ThirdParty module type:Compiler Compiler support or related warnings type:Infrastructure Infrastructure/ecosystem related changes, such as CMake or buildbots

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants