Exporting renamed/multiple-named configs

I’m trying to find a way to rename an existing CMake project, but in a way that avoids disrupting existing users. Those users will (at least initially, and for a time) be expecting the project to define the options, targets, and output variables it previously used under the old name. That situation would continue, with deprecation warnings shown on any uses of the old name, for whatever transition period is deemed necessary. Then, the project would stop creating the configuration under the previous name, and only the new name would be recognized.

The option()s are easy enough to handle:

option(NEW_NAME_FLAG1 "Description" ON/OFF)

foreach (_option _FLAG1 ... _FLAGn)
  if (DEFINED CACHE{OLD_NAME${_option}})
    message(WARNING "Deprecation notice")
    get_property(_help CACHE NEW_NAME${_option} PROPERTY HELPSTRING)
    set(NEW_NAME${_option} "${OLD_NAME${_option}}"
      CACHE BOOL "${_help}" FORCE)
    unset(OLD_NAME${_option} CACHE)
  endif()
endforeach()

It’s the exported config that’s proving trickier. Two basic approaches come to mind. But since I’m hitting snags with either of them, I’m curious if anyone has any recommendations or suggestions.

Option 1: Export the full config under both names

  • Generate a full set of OldNameConfig.cmake, OldNameConfigVersion.cmake, etc. to install in lib/cmake/OldName/
  • Also generate a full set of NewNameConfig.cmake, NewNameConfigVersion.cmake, etc. to install in lib/cmake/NewName/
  • OldNameConfig.cmake should export targets in namespace OldName::
  • NewNameConfig.cmake should use namespace NewName::
  • Target names inside the namespace should remain unchanged. That is, the same targets should be exported in each config, just with different prefixing. However, results variables like PackageName_INCLUDE_DIRECTORIES should follow the config name, so OldName_INCLUDE_DIRECTORIES vs. NewName_INCLUDE_DIRECTORIES.
  • All configuration values should be identical in either config; they should be interchangeable except for the package name.

Option 2: Create a “proxy” config under the old name

  • Everything would be installed as NewNameConfig.cmake, NewNameConfigVersion.cmake, etc. using namespace NewName::, and installed to lib/cmake/NewName/.
  • OldNameConfig.cmake, installed to lib/cmake/OldName/, would ideally display a deprecation message, then:
    • Perform a find_package(NewName ...) on behalf of the caller,
    • Define a set of ALIAS targets OldName::target for each NewName::target
    • Do a set(OldName_RESULTn "${NewName_RESULTn}") and set(OLDNAME_RESULTn "${NEWNAME_RESULTn}") for each result variable
    • End with:
       find_package_handle_standard_args(OldName
         FOUND_VAR OldName_FOUND
         REQUIRED_VARS OldName_INCLUDE_DIRECTORY OldName_LIBRARY
         VERSION_VAR OldName_VERSION)
      

My strong preference would be the second option, primarily because the first one creates all sorts of squirrely issues, things like:

  1. The package template file becomes a lot less clean; right now it outputs messages referring to the @PACKAGE_PROJECT_NAME@::target targets, and sets @PACKAGE_PROJECT_NAME@_VAR variables, all of which can no longer use @PACKAGE_PROJECT_NAME@ unconditionally; it’ll depend on the namespace I’m exporting under. But then other ones, the @PACKAGE_PATH_VAR@ ones, remain unchanged.
    OK, yes, I could set a variable EXPORT_NAMESPACE for each run of configure_package_config_file() and change @PACKAGE_PROJECT_NAME@ to @PACKAGE_EXPORT_NAMESPACE@ easily enough, but…

  2. Maintaining two namespaces/package-names becomes especially tricky, when initially creating the config files inside the build directory.
    In the existing (quite cleanly-written) tooling, each component is a target, or vice versa. Each target gets exported to its own ComponentName.cmake file, which ...Config.cmake loads if that component is requested. Because the target names aren’t changing, I’d prefer to leave those files named the way they are. But there can’t be two ComponentName.cmake files configured to use different namespaces, and both exported to the same build directory.

The only tricky thing about option 2 is, I’d really like OldNameConfig.cmake to call the inner find_package(NewName ...) with all of the same arguments that the caller used when they called find_package(OldName ...).

Thing is, there doesn’t seem to be a way to get at those args in their “raw” form. I’d have to process all of the various OldName_FIND_REQUIRED*, OldName_FIND_QUIETLY, OldName_FIND_VERSION*, OldName_FIND_COMPONENTS, etc. variables to reverse-engineer the calling arguments, if I wanted to call find_package(NewName ...) the same way.

Am I (hopefully) missing something? (Either a solution to one or the other set of issues, or a completely different approach I haven’t even considered. I’m not picky.)

I would hand-code the OldName package to do forwarding. Something like this logic is used in VTK for deprecated components and deprecating VTK_USE_FILE. So, basically:

  • if there’s no minimum version requested, make deprecation warnings about the changes
  • if a version before the change is requested, stay quiet (because they need to do that in order to keep their minimum there); an AUTHOR_WARNING may be appropriate to let the developers know that things are afoot though
  • if a version with the new stuff is requested, error so that they migrate to the new names

Then I would provide targets under the old name:

find_package(NewName) # Forward arguments like `QUIET`, components, and/or version requests too
if (NOT NewName_FOUND)
  set(OldName_FOUND 0)
  set(OldName_NOT_FOUND_MESSAGE "NewName not found: ${NewName_NOT_FOUND_MESSAGE}")
  return ()
endif ()

if (NOT TARGET OldName::OldTarget AND TARGET NewName::NewTarget)
  add_library(OldName::OldTarget IMPORTED INTERFACE)
  set_target_properties(OldName::OldTarget PROPERTIES INTERFACE_LINK_LIBRARIES "NewName::NewTarget")
endif ()
1 Like

Yeah, that’s the basic plan I was thinking, for option 2. (Now that you’ve shown code for that approach, I can see how IMPORTED INTERFACE proxy targets are a better way to go than ALIAS targets, tho — yeah.) The possibility of the user calling find_package() with (or without) a VERSION and/or any of the multiple COMPONENTs in any combination makes it a bit of a pain — well, not really, just a tedious chore — to get the initial find_package(NewName) call right, but it’s far from impossible.

Still better than trying to export the targets two different ways. Which is probably doable, but I’m pretty sure NOT without making a complete mess of the code. And currently it’s clean and well-organized enough that I really don’t want to be the one who turns it into nasty spaghetti.

The best thing about option 2 is that it’s self-contained — I add the code to export as OldNameConfig.cmake, and in every other way the code stays exactly the same as it is now except for the PROJECT_NAME change to NewName. Whenever the transition period is deemed complete, the bit of code that handles the OldName exports can be just as cleanly dropped, and it’s like it never happened.

I especially like that suggestion, thanks!