Best Practices for Replacing ament_target_dependencies in Kilted While Maintaining Compatibility

With the deprecation of the ament_target_dependencies macro happening close to the Kilted release deadline, I didn’t manage to update several of my packages in time. I suspect I’m not alone—many maintainers may now have released packages for Kilted that are triggering deprecation warnings during builds.

The buildfarm currently produces a warning but still builds successfully:

ament_target_dependencies() is deprecated.  Use target_link_libraries()
  with modern CMake targets instead.  Try replacing this call with:

Given that Kilted has already been released, I’d like to hear about the best path forward for maintainers who want to address this warning without breaking downstream packages or violating API/ABI compatibility expectations.

  • Is it advisable to replace ament_target_dependencies with target_link_libraries in patch releases for Kilted?
  • Or should we leave existing Kilted packages as-is and only apply the fix in rolling or future releases?
  • Does replacing ament_target_dependencies with target_link_libraries impact downstream users in a way that might break their builds or linkage?

Curious to hear what other package maintainers consider best practice here.

2 Likes

And what’s the strategy for packages that share the same source for multiple ROS 2 releases? Since which distro is it safe to substitute ament_target_dependencies?

3 Likes

It should be viable to go back to at least Humble. That said, some of your downstream dependencies on older distributions may not be setup for target_link_libraries properly, so it may be more work than just updating your own package.

2 Likes

In short:

Is it advisable to replace ament_target_dependencies with target_link_libraries in patch releases for Kilted?

Yes it is advisable.

Or should we leave existing Kilted packages as-is and only apply the fix in rolling or future releases?

You should apply fixes to Kilted. It’s probably fine to apply fixes as far back as ROS Foxy.

Does replacing ament_target_dependencies with target_link_libraries impact downstream users in a way that might break their builds or linkage?

AFAIK, your use of ament_target_dependencies versus target_link_libraries does not impact downstream users.

Since which distro is it safe to substitute ament_target_dependencies?

Foxy-ish


In long…

The problem: build some software

Let me start with a short story.
Some dude has a top-secret high-impact C++ project. It’s is going to change the world. Here it is:

# include <tinyxml2.h>
# include <iostream>

int main (int argc, char **argv) {
  if (argc != 2) {
    std::cerr << "Usage " << argv[0] << " some_world.sdf\n";
    return 1;
  }
  tinyxml2::XMLDocument doc;
  tinyxml2::XMLError result = doc.LoadFile(argv[1]);
  tinyxml2::XMLElement* root = doc.RootElement();
  if (!root) {
    std::cerr << "Invalid SDFormat\n";
    return 1;
  }
  tinyxml2::XMLElement* world = root->FirstChildElement("world");
  if (!world) {
    std::cerr << "Invalid SDFormat world\n";
    return 1;
  }
  world->SetAttribute("name", "Shane's world");
  doc->Print();
}

Some dude compiles it with:

g++ world_domination.cpp -I /usr/include -L/usr/lib/x86_64-linux-gnu -ltinyxml2 -std=c++17 -oworld_domination

However, this only builds on Ubuntu. Any serious world domination effort is going to need cross platform support, at least until the year of the linux desktop arrives.

Some dude wonders if world domination would be easier with CMake.

cmake_minimum_required(VERSION 3.15)
project(world_domination VERSION 1.0.0)

find_package(tinyxml2 REQUIRED)

add_executable(world_domination world_domination.cpp)
target_link_libraries(world_domination tinyxml2::tinyxml2)
mkdir build
cd build
cmake ..
cmake --build .

Great. It’s a bit more complex to use CMake, but cross-platfom world domination makes it worth it.
The world is conquered and no one ever has any problems again.

The end

My point is that CMake’s job is compile your software.
CMake needs to give the compiler:

  • Include directories, like /usr/include
  • Link directories, like /usr/lib/x86_64-linux-gnu
  • Libraries to link against, like tinyxml2
  • Misc compiler flags, like -std=c++17

So, how does CMake know what to give to the compiler?

The solution: modern CMake targets

This line tells CMake world_domination depends on tinyxml2.

target_link_libraries(world_domination tinyxml2::tinyxml2)

The text tinyxml2::tinyxml2 is the name of a CMake target. CMake targets have target properties. Some of tinyxml2::tinyxml2’s properties tell CMake what it needs to build world_domination. For example:

When you call target_link_libraries() with CMake targets, you’re telling CMake to use all the information provided by those targets to build your software.

The old solution: CMake variables

Long ago in a galaxy far far away, CMake expected you store this information in CMake variables.
These variables had conventional names that most-ish packages used. tinyxml2 might have put that info into variables like:

  • tinyxml2_INCLUDE_DIRS for include directories, like /usr/include
  • tinyxml2_LIBRARIES with full paths to libraries like /usr/lib/x86_64-linux-gnu/libtinyxml2.so
  • tinyxml2_HECK_IF_I_KNOW for misc compiler flags, like -std=c++17

You might tell CMake about those variables with calls like

target_include_directories(world_domination PRIVATE ${tinyxml2_INCLUDE_DIRS})
target_link_libraries(world_domination PRIVATE ${tinyxml2_LIBRARIES})
target_compile_options(world_domination PRIVATE ${tinyxml2_HECK_IF_I_KNOW})

Or, maybe you’d give CMake that information with even older calls like:

Old-style CMake variables have some problems, but I’m going to resist explaining them.

Weren’t we talking about ament_target_dependencies?

So ament_target_dependencies() did two things:

It was supposed to be convenient to use ament_target_dependencies(). However, not all packages used the same “conventional names”, and so couldn’t be used with ament_target_dependencies(). Then it got worse when our dependencies stopped providing old-style CMake variables. We tried to work around this, even creating our own wrappers to export old-style CMake variables. Then we made ament_target_dependencies() support modern CMake targets if the targets were put into a CMake variable called *_INTERFACES. However, the _INTERFACES was just a ROS convention. Our dependencies didn’t have to provide a variable with that name. By ROS Foxy we made most ROS packages export modern CMake targets too.

Catkin tried to make it possible to override packages. If the installed version of roscpp had a bug, then you could put a newer version of roscpp into your workspace and use that. Catkin made this possible by carefully sorting include directories before giving them to the compiler. All you had to do was find those dependencies with find_package(catkin REQUIRED COMPONENTS foo bar etc). Frankly overriding packages doesn’t work, not even in ROS 1, except in very fragile circumstances. Still, ament_target_dependencies() tried to support it by re-ordering include directories according to ament and colcon workspaces. Then we discovered modern CMake targets took away our ability to control the order of information passed to the compiler from transitive dependencies. We solved that in ROS Humble by installing headers into unique include directories. This made the sorting in ament_target_dependencies() unnecessary.

In summary, chances are the target_link_libraries() call in ament_target_dependencies() has been the only thing doing useful work for some time now. The rest was just making your build slower.

18 Likes

Does this also apply to the variables like ${std_msgs_TARGETS} apparently containing the name(s) of target(s) in packages containing message definitions? I have only seen this in the Kilted docs and as far as i can tell it hasn’t been backported (Refer to target_link_libraries instead of ament_target_dependencies by sloretz · Pull Request #5225 · ros2/ros2_documentation · GitHub).

Yes, ${project_name_TARGETS} was added in ROS Foxy.

1 Like

Well, it seems using target_link_libraries is a bit more cumbersome.

How do I know whether I should depend on pkg::pkg or ${pkg_TARGETS}? I know the distinction is somewhere along the line of message packages, but how the heck should I know which ones are they? Or we could depend everywhere on ${pkg_TARGETS}, but I thought the motion is towards the modern targets and away from variables…

Also, some funny packages do not provide target pkg::pkg (rclcpp_components, I’m looking at you). I get that the developer should instead choose on which target to depend. But how does he know which targets are available? Reading CMakeLists.txt, or, even worse, the generated *-targets.cmake? Whooo…

Shouldn’t it be a standard that each ROS package has a target named pkg::pkg which contains everything that’s needed to build against that package?

1 Like

By the way, CMake is introducing a similar concept as part of the introduction of Common Package Specification (CPS) files to replace CMake config files, see for example from https://www.kitware.com/navigating-cmake-dependencies-with-cps/ :

This means that imported targets generated from a CPS will always have the form a::b, with one exception—some packages may provide an INTERFACE target with the same name as the package which references that package’s default_components.

In a nutshell, that would mean that for example you will be able to have:

find_package(<pkgname> REQUIRED)
target_link_libraries([..] PRIVATE <pkgname>)

to link the “default_components” targets of <pkgname>, a bit like the old ${<pkgname>_TARGETS} convention of ROS. Anyhow, realistically non-experimental full support for CPS will land in apt packages if all goes well only in Ubuntu 26.04, so it will take several years to be usuable by ROS, so a convention of having a <pkgname>::<pkgname> target for each ROS/ament package makes perfectly sense!

1 Like

Thanks for confirming my line of thought! I’ve created an issue for this: Bad developer experience: Complicated usage of target_link_libraries instead of ament_target_dependencies · Issue #584 · ament/ament_cmake · GitHub .

Thanks for the pointer, that’s a quite interesting movement :slight_smile: Let’s see if Bazel, Cargo and others jump in :slight_smile:

1 Like

for what is worth, I also believe that a reasonable idea would be to consider CPS.

The more we do things in the “standard way”, the better.

On the other hand, that would be a much worse “breaking change” compared to what is being discussed here.

In complete seriousness, I wonder if a “magic wand” conversion tool based on LLm can be created to allow automatic migration, without user’s intervention.

Maybe an AI agent that sends PR to package maintainers “fixing” they CMakeLists.txt files

1 Like

Who else is using CPS?

I don’t know, but I think it is worth educating oneself about where CMAKE is going.

I am leaving this here:

6 Likes

How do I know whether I should depend on pkg::pkg or ${pkg_TARGETS}?

Change target_link_libraries() back to ament_target_dependencies(), copy-paste the code from the deprecation warning (it tells you whether to use pkg::pkg or ${pkg_TARGETS}), change it back to target_link_libraries(). Easy. :wink:

2 Likes

Easy until ament_target_dependencies is removed. And then what?

Good point.

One thing we do is create a cmake variable with the list of dependencies and loop over them for find_package and then pass this variable to ament_target_dependencies. Now that won’t work because it is unclear whether we use pkg:pkg or pkg_TARGETS in target_link_libraries. And so we have to repeat the list in two places which is prone to errors.

We discussed this in the PMC meeting this week (ROS PMC Minutes for June 10, 2025). At least for the near future, it doesn’t make sense to fully remove, rather just keep it deprecated and generating warnings. Technically, since the deprecation was already released in the kilted release, then removing it in rolling in preparation for the lyrical release makes a lot of sense. In practice, enough maintainers haven’t done anything with the deprecation warning to justify completely removing it (and it doesn’t really cost anything to leave it in, it’s just not best practice at this point)

In short, if you want to depend on all targets provided by an ament_cmake package named foo, previously you did:

ament_target_dependencies(my_target PUBLIC foo)

Now you do:

target_link_libraries(my_target PUBLIC ${foo_TARGETS})

I put a longer explanation in the issue @peci1 opened.


How do I know whether I should depend on pkg::pkg or ${pkg_TARGETS} ?

Use ${pkg_TARGETS} when you don’t yet know what you need. Use specific targets when you do. pkg::pkg is just the name of a target. It isn’t more special than pkg::bar.


One thing we do is create a cmake variable with the list of dependencies and loop over them for find_package and then pass this variable to ament_target_dependencies. Now that won’t work because it is unclear whether we use pkg:pkg or pkg_TARGETS in target_link_libraries.

You might be interested in ament_cmake_auto. It can make a target depend on all of a package’s dependencies (see ament_auto_add_library), which seems to be what you’re doing. I don’t think there’s any documentation, and it’s not used in the core packages, but there are downstream packages you can learn from ROS Package: ament_cmake_auto .

4 Likes

Easy until ament_target_dependencies is removed. And then what?

I was just joking, I didn’t seriously propose that abusing the deprecation warning like this was a good idea! :slight_smile:

1 Like

@sloretz Thanks for the suggestion about ament_cmake_auto. I did not know about these macros. It simplifies our CMakeLists.txt quite a bit. I wish these were documented somewhere.