Skip to content

COMP: Do not expose plug-in autogen dirs to dependents - #1444

Open
kislinsk wants to merge 5 commits into
commontk:masterfrom
MITK:fix-plugin-autogen-include-dirs
Open

COMP: Do not expose plug-in autogen dirs to dependents#1444
kislinsk wants to merge 5 commits into
commontk:masterfrom
MITK:fix-plugin-autogen-include-dirs

Conversation

@kislinsk

Copy link
Copy Markdown
Contributor

Disclaimer: We (MITK) are currently catching up on the latest CTK master branch
since we forked for Qt 6 back in 2023. We found a few bugs that are not caught by
your CI and mostly affect external users of CTK.

Priority: highest. Regression from the commit that added AUTOUIC header
propagation to ctkFunctionGetIncludeDirs.

Dependents receive every dependency's
<dep>_autogen/include[_<CONFIG>] on their include path so that a
dependency's AUTOUIC-generated ui_*.h resolves. That directory also holds
AUTOMOC's output, in a subdirectory named after a hash of the source
directory relative to the source tree, and mocs_compilation.cpp includes
its moc output with angle brackets.

Plug-ins of a project therefore share the hash, because they share the
relative layout (for MITK, every plug-in keeps its sources in src/internal,
hash TLJAIHGYSZ). CMake appends a target's own autogen include dir last
(observed at position 92 of 71+ entries, with dependency dirs at 9-44), so a
dependency with a same-named header wins the search.

The result: a plug-in compiles a dependency's meta object instead of its own.
Concretely, every MITK plug-in has src/internal/mitkPluginActivator.h, so
they all picked up org.mitk.core.services' version:

mocs_compilation.obj : error LNK2019: unresolved external symbol
  "mitk::org_mitk_core_services_Activator::org_mitk_core_services_Activator(void)"
  referenced in "QtPrivate::QMetaTypeForType<...>::getDefaultCtr"
mitkPluginActivator.obj : error LNK2001: unresolved external symbol
  "mitk::RestApiPluginActivator::metaObject(void) const"

The fix skips plug-ins when contributing these directories.
${dep}_INCLUDE_SUFFIXES is set only by ctkMacroBuildPlugin, never by
ctkMacroBuildLib, so it is a reliable discriminator. Libraries keep the
propagation, which Libs/DICOM/Widgets genuinely needs for CTKWidgets'
ui_ctkThumbnailListWidget.h, and their distinct relative layouts cannot
collide.

Note on an alternative that does not work: putting the target's own
autogen include dir earlier in my_includes has no effect, because CMake
collapses it to its own late-appended copy. Verified in the generated
.vcxproj.

Dependents get every dependency's <dep>_autogen/include_<CONFIG> on
their include path so that AUTOUIC-generated headers can be found.
That directory also holds AUTOMOC's output, in a subdirectory named
after a hash of the source directory relative to the source tree, and
mocs_compilation.cpp includes its moc output with angle brackets.

Plug-ins of a project tend to keep their sources in the same relative
directory, so they all share that hash. CMake appends a target's own
autogen include dir last, so a plug-in whose dependency has a header
of the same name wins the search and the plug-in ends up compiling the
dependency's meta object instead of its own. That leaves the plug-in's
own QObject without a meta object and pulls in an undefined
constructor of the foreign class.

Skip plug-ins when contributing these directories. Libraries keep
them, since they do need each other's generated ui_*.h, and their
distinct relative source layouts do not collide.
@lassoan

lassoan commented Jul 25, 2026

Copy link
Copy Markdown
Member

@kislinsk could you please add a minimal test that fails without this change? Without tests, it will take forever the AI agents to find a solution that fixes the issue for CTK without breaking the Slicer build.

@hjmjohnson could you please have a look at this?

Covers which directories ctkFunctionGetIncludeDirs contributes for a
library dependency and for a plug-in dependency: both end up on the
include path, but only the library contributes its autogen directory.

Running the test against the previous behaviour fails on the plug-in
case, since that directory was contributed for plug-ins as well.
@kislinsk

Copy link
Copy Markdown
Contributor Author

Added one, using the existing add_cmakescript_test harness. It exercises
ctkFunctionGetIncludeDirs with a library dependency and a plug-in dependency and checks
which directories each contributes. Against the previous behaviour it fails on the plug-in
case:

testcase 2 - pattern:org_commontk_bar_autogen  found:1  expected:0

On Slicer: the change only affects what a plug-in dependency contributes, libraries are
untouched, so it can only bite a project where one plug-in includes another plug-in's
generated ui_*.h. MITK has none, but I can't check Slicer, so that's worth a look before
merging.

For what it's worth, that is also the case I got wrong first: I initially dropped the
propagation altogether and it broke CTK's own build, because CTKDICOMWidgets includes
CTKWidgets' private ctkThumbnailListWidget_p.h, which needs ui_ctkThumbnailListWidget.h.
Hence libraries keeping it.

ctkMacroBuildPlugin passes everything it collected for compiling a
plug-in to target_include_directories() as PUBLIC, so the autogen
include directories contributed by the dependencies end up in the
INTERFACE_INCLUDE_DIRECTORIES of the exported target.

Those paths contain $<CONFIG>. A consumer expands it for every
configuration it generates, while CTK only ever created the directory
for the configurations it was built in, so CMake rejects the imported
target:

  Imported target "org_commontk_eventadmin" includes non-existent path
    ".../CTKPluginFramework_autogen/include_RelWithDebInfo"
  in its INTERFACE_INCLUDE_DIRECTORIES.

Single-config generators are unaffected, because the directory is then
called include and does exist, which is why this only shows on
multi-config generators.

A dependency's generated ui_*.h is needed to compile the plug-in, not
by anything downstream, so contribute those directories as PRIVATE.
@kislinsk

Copy link
Copy Markdown
Contributor Author

While syncing MITK to this branch our Windows CI hit a third problem from the same
propagation change, so I added a commit for it here rather than opening another PR.

ctkMacroBuildPlugin passes everything it collected to target_include_directories() as
PUBLIC, so a dependency's autogen directory ends up in the exported interface of the
plug-in:

INTERFACE_INCLUDE_DIRECTORIES ".../CTKPluginFramework_autogen/include$<$<BOOL:1>:_$<CONFIG>>"

A consumer expands $<CONFIG> for every configuration it generates, while CTK only created
the directory for the configuration it was built in, so CMake refuses the imported target:

Imported target "org_commontk_eventadmin" includes non-existent path
  ".../CTKPluginFramework_autogen/include_RelWithDebInfo"
in its INTERFACE_INCLUDE_DIRECTORIES.

This one is worth a look for Slicer specifically: unlike the moc collision, it does not
depend on any source layout or ui_*.h usage. Any multi-config consumer linking a CTK plug-in
target hits it, and it is silent on single-config generators, which is why we only saw it on
Windows.

Note the offending entry comes from CTKPluginFramework, a library dependency, so the
plug-in exclusion in the first commit does not cover it. A dependency's generated ui_*.h is
needed to compile the plug-in, not by anything downstream, so the fix contributes those
directories as PRIVATE. Verified with a minimal consumer project: it reproduces the error
above against master and configures cleanly with the change, and no _autogen entry survives
into CTKExports.cmake.

@kislinsk

kislinsk commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

This is not done yet. The current solution (with and without this PR) tends to produce very long file names. So long, that MSVC/Windows is not able to handle such object files any more (should be < 260 chars). E.g.:

D:\jenkins\MITK\build\MITK-build\Plugins\org.mitk.gui.qt.pharmacokinetics.concentration.mri\org_mitk_gui_qt_pharmacokinetics_concentration_mri_autogen\EJRQKI7XPS_Release\qrc_org_mitk_gui_qt_pharmacokinetics_concentration_mri_manifest.cpp(1,1): error C1083: Cannot open compiler generated file: '': Invalid argument

I try to come up with something less repetitive and shorter...

kislinsk added 2 commits July 26, 2026 11:03
The default autogen directory is <target>_autogen, and a plug-in
target name is the symbolic name with dots replaced by underscores.
It therefore repeats, in full, the name of the directory it sits in.
Together with the generated source names, whose length is driven by
the same symbolic name, this reaches the 260 character path limit of
the Windows toolchain for plug-ins with long names, and the compiler
fails with "Cannot open compiler generated file".

The autogen directory only has to be unique per target. The plug-in's
own binary directory already provides that, so the target name adds
nothing but length.
The generated manifest and cached-resource .qrc files were named
after the symbolic name of the plug-in. Both are written to the
plug-in's own binary directory, so the name is unique without it,
and AUTORCC derives the name of the generated source from it. For a
plug-in with a long symbolic name that source ends up carrying the
name twice, once in the directory and once in the file, which is a
noticeable part of the 260 character path limit of the Windows
toolchain.

The resource prefix, which is what lookups use at runtime, comes
from the content of the .qrc file and is unaffected.
@kislinsk

Copy link
Copy Markdown
Contributor Author

Two more commits, both aimed at the path length.

COMP: Shorten the autogen directory of a plug-in sets AUTOGEN_BUILD_DIR to /autogen instead of the default _autogen. A plug-in target name is the symbolic name with dots replaced by underscores, so the default repeats, in full, the name of the directory it already sits in. The directory only has to be unique per target, and a plug-in has its own binary directory — the macro already warns when the project name and the directory name disagree.

This applies to plug-ins only; libraries keep the default layout. That matters for the earlier commits in this PR: ctkFunctionGetIncludeDirs still contributes _autogen/include… for library dependencies, and the filter that keeps those out of the exported interface still matches them. Plug-in autogen directories are no longer contributed at all, so nothing else refers to them by path.

COMP: Shorten the names of the generated plug-in .qrc files renames the generated <symbolic_name>_manifest.qrc and <symbolic_name>_cached.qrc to manifest.qrc and cached.qrc. Both are written to the plug-in's own binary directory, so the symbolic name is not needed for uniqueness, and AUTORCC derives the generated source name from the .qrc name. The resource prefix, which is what runtime lookups use, comes from the content of the .qrc and is unchanged.

Effect on the longest generated path in an MITK build, for the plug-in that was failing:

before  …/org_mitk_gui_qt_pharmacokinetics_concentration_mri_autogen/include_Release/EJRQKI7XPS/qrc_org_mitk_gui_qt_pharmacokinetics_concentration_mri_manifest_Release_CMAKE_.cpp
after   …/autogen/include_Release/EJRQKI7XPS/qrc_manifest_Release_CMAKE_.cpp

@lassoan

lassoan commented Jul 26, 2026

Copy link
Copy Markdown
Member

Thanks for the updates @kislinsk. I'll wait for @hjmjohnson to have a look at these proposed changes.

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants