Using INTERFACE, ALIAS, and IMPORTED Libraries

Learn to use abstract target types like INTERFACE, ALIAS, and IMPORTED to model complex project needs, organize build properties, and integrate pre-compiled binaries.

Greg Filak
Published

In the last lesson, we established the core principle of modern CMake: projects are a dependency graph of targets, and usage requirements flow automatically through that graph. We know how to model libraries built from our own source code and even simple header-only dependencies.

However, as projects grow, we encounter new organizational challenges that STATIC and SHARED libraries alone can't solve:

  1. How do we apply a common set of compiler flags to some, but not all, targets without duplicating code?
  2. How do we create names for our library targets that prevent naming collisions, especially in large projects?
  3. How do we integrate a third-party library that is only available as a pre-compiled binary (.a or .dll) rather than as source code?

This lesson will introduce three abstract target types that address these problems: INTERFACE libraries as property groups, ALIAS targets for namespacing, and IMPORTED targets for pre-compiled code.

Grouping Usage Requirements with INTERFACE Libraries

We've seen that an INTERFACE library is perfect for a header-only dependency. It has no source files and produces no output, but it can have properties like INTERFACE_INCLUDE_DIRECTORIES that are passed on to any consumer.

We can take this concept a step further. An INTERFACE library can be used as a general-purpose "bag of properties" to group any set of usage requirements, not just include directories.

Practical Example: Enforcing Strict Warnings

A common requirement is to build our internal code with a very strict set of compiler warnings to catch potential bugs early. However, we don't want to force these strict warnings onto the final application or any external consumer of our libraries, as that would be impolite.

We can solve this problem with an INTERFACE library. Let's create one in our root CMakeLists.txt to represent our project's internal warning policy:

CMakeLists.txt

cmake_minimum_required(VERSION 3.16)

project(Greeter VERSION "1.0")

# Create an INTERFACE target to hold our warning flags
add_library(StrictWarnings INTERFACE) 

# Attach compiler options to this target
target_compile_options(StrictWarnings 
  INTERFACE 
    -Wall 
    -Wextra 
    -Wpedantic 
) 

add_subdirectory(greeter)
add_subdirectory(app)
add_subdirectory(date)

These -Wall, -Wextra, and -Wpedantic flags are for the GCC and Clang compilers. If you want to follow along using MSVC, you can use /Wall and /W4 instead, but the flags aren't important right now - we're just using them as examples. We'll cover compiler options and how to make our CMakeLists.txt compatible across multiple compilers later in the course.

For now, we've created a simple target named StrictWarnings that doesn't build anything. Its only purpose is to carry the INTERFACE property COMPILE_OPTIONS with our desired warning flags.

Now, how do we use it our new target? In our GreeterLib's CMakeLists.txt, we can simply "link" to this interface target just like we would link it to any other target:

greeter/CMakeLists.txt

cmake_minimum_required(VERSION 3.16)

add_library(GreeterLib src/Greeter.cpp)

target_link_libraries(GreeterLib PRIVATE StrictWarnings) 

target_include_directories(GreeterLib PUBLIC
  "${CMAKE_CURRENT_SOURCE_DIR}/include"
)

Let's review our use of the INTERFACE and PRIVATE keywords in this context, as it's important that we're comfortable with the behaviour associated with these fundamental CMake behaviors.

The INTERFACE Keyword in target_compile_options()

The entire point of the StrictWarnings target is to give other targets something to link against if they want to adopt the target_compile_options() we associated with StrictWarnings.

For those properties to be inheritable by consumers, we need to use the word INTERFACE in our target_compile_options() command. Conceptually, PUBLIC could also have resulted in this inheritance behaviour, but it would have meant the properties would also apply to StrictWarnings itself.

This would be unnecessary and misleading in this case. StrictWarnings is an INTERFACE target, so it doesn't have anything to compile. The target_compile_options() function specifically checks for this and, if the target is an INTERFACE library, only INTERFACE compiler options can be set.

The PRIVATE Keyword in target_link_libraries()

We want the properties we set in StrictWarnings to apply to targets that explicitly link against it - GreeterLib in this example - but that's where the story should end. They should not propagate further than that. To implement this, GreeterLib declared this direct dependency using target_link_libraries(), but the dependency was declared as PRIVATE.

This means that, when GreeterApp later links to GreeterLib, GreeterApp will not see GreeterLib's dependency on StrictWarnings. As such, GreeterApp will not adopt StrictWarnings as an indirect dependency, and will not inherit those compiler settings.

This perfectly encapsulates an internal development standard. We enforce the policy on our own library without imposing it on its consumers.

Creating Namespaced Aliases with ALIAS Libraries

An ALIAS library creates a second name for an existing target. It's not a copy; it's just a reference. Linking to the original name or the alias is identical.

A common convention in larger projects is to create "namespaced" aliases using the :: separator. When there are many targets, this convention makes the origin of a target clearer, and helps reduce the probability of naming conflicts.

Let's add an alias for our GreeterLib. We still need to use our regular name internally, but if our collection of Greeter targets is being shared for use in a larger project, providing namespaced aliases like Greeter::Lib and Greeter::App could be appreciated.

In a CMakeLists.txt, after defining the original target, we can add an alias like this:

greeter/CMakeLists.txt

cmake_minimum_required(VERSION 3.16)

add_library(GreeterLib src/Greeter.cpp)
add_library(Greeter::Lib ALIAS GreeterLib) 

target_include_directories(GreeterLib PUBLIC
  "${CMAKE_CURRENT_SOURCE_DIR}/include"
)

Consumers can now refer to our target using whichever name they prefer:

app/CMakeLists.txt

cmake_minimum_required(VERSION 3.16)

add_executable(GreeterApp src/main.cpp)

# These two are equivalent
target_link_libraries(GreeterApp GreeterLib)
target_link_libraries(GreeterApp Greeter::Lib)

Using Pre-Compiled Binaries with IMPORTED Libraries

So far, we've only worked with targets built from source files within our project. But many library creators prefer not to share their source code. They distribute their libraries only in a precompiled form, alongside the header files describing what is in the library.

Handling such a dependency is the job of an IMPORTED library. It's a target that represents a binary that already exists on disk. You create the target and then manually set the properties that tell CMake where to find its files.

For example, let's imagine that our GreeterLib component is shared as a precompiled binary, rather than the complete source code. If you want to replice this scenario to follow along, you can find the compiled library in your /build/greeter directory, and copy it into a more permanant location in the project, such as /greeter/lib.

You can also delete the /greeter/src directory, or just pretend it doesn't exist. Your project directory might look something like this:

(project root)/
└─ greeter/
   ├─ include/
   │  └─ greeter/
   │     └─ Greeter.h
   ├─ lib/
   │  └─ libgreeter.a
   └─ CMakeLists.txt
└─ ... (our other directories)

We need to update our greeter/CMakeLists.txt file to reflect this new situation. No longer is our build compiling GreeterLib from its /src directory - instead we need to import the already-compiled file from the /lib directory.

Lets take a look at the three changes we need to make, and then we'll walk through them step by step:

greeter/CMakeLists.txt

cmake_minimum_required(VERSION 3.16)

# 1. Change our library to IMPORTED STATIC GLOBAL
add_library(GreeterLib src/Greeter.cpp)
add_library(GreeterLib IMPORTED STATIC GLOBAL)

# 2. Change our include directory visibility to INTERFACE
target_include_directories(GreeterLib PUBLIC
target_include_directories(GreeterLib INTERFACE
  "${CMAKE_CURRENT_SOURCE_DIR}/include"
)

# 3. Provide the path to our prebuild binary
set_target_properties(GreeterLib PROPERTIES 
  # This property points to the actual binary file
  IMPORTED_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/lib/libgreeter.a" 
)

Step 1: Changing to an IMPORTED STATIC GLOBAL Library

Here's what these three keywords do:

IMPORTED: Instead of our add_library() command providing the list of source files, we now declare that the library is already compiled and only needs to be IMPORTED into our build.

STATIC: With IMPORTED libraries, we also need to explicitly state whether they are STATIC or SHARED. We'll revisit this topic in the next lesson and learn the difference between specifying and omitting the library type within an add_library() command.

GLOBAL: A side-effect of targets that are defined using the IMPORTED keyword is that their scope is restricted to just the CMakeLists.txt file in which they are declared. We can change this, and return GreeterLib to the global scope it previously had, by adding the GLOBAL keyword. This makes the target accessible to other CMakeLists.txt files in our build - most notably, the /app/CMakeLists.txt file, where GreeterApp declares GreeterLib as a dependency.

Step 2: Changing the Include Directories to INTERFACE

Previously, our target_include_directories() were declared as PUBLIC, as they were required both for GreeterLib and anything that consumes it.

With GreeterLib now being a precompiled IMPORTED library, it doesn't need these header files itself, as it no longer needs to be compiled. As such, we can restrict their visibility to just consumers by using the INTERFACE keyword.

Step 3: Setting the IMPORTED_LOCATION Property

Finally, and most obviously, we need to tell CMake where this precompiled library is on our hard drive. CMake expects this location to be provided as the IMPORTED_LOCATION property on the target. There is no convenient helper function for this, so we just use the general set_target_properties() function to set it:

greeter/CMakeLists.txt

cmake_minimum_required(VERSION 3.16)

add_library(GreeterLib STATIC IMPORTED GLOBAL)

target_include_directories(GreeterLib INTERFACE
  "${CMAKE_CURRENT_SOURCE_DIR}/include"
)

# This is where we attach the physical file locations to
# our abstract target
set_target_properties(GreeterLib PROPERTIES 
  # This property points to the actual binary file
  IMPORTED_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/lib/libgreeter.a" 
)

Using the Imported Target

From the perspective of any other target, GreeterLib is just another library. The root CMakeLists.txt file continues to import it using add_subdirectory(), the app/CMakeLists.txt file for our GreeterApp target continues to link to it, and nothing in our source code needs to change either.

The IMPORTED keyword let us create a simple wrapper for a pre-compiled binary, allowed it to be integrating into our build just like any other target.

After a change like this, it is a good idea to clean out our /build directory by deleting everything in there. This removes old unused files still lurking around from previous builds and, when we next run the configure step, cmake will regenerate a fresh copy of just the stuff we need.

cmake ..

We can then create a build and run app/GreeterApp or app/GreeterApp.exe to verify that everything still works after our changes:

cmake --build .
./app/GreeterApp
Hello from the modular Greeter library!

Summary

This lesson introduced advanced target types that allow you to create clean, modular, and robust abstractions for your build logic and dependencies.

  • INTERFACE Libraries as Property Groups: Use INTERFACE targets to bundle common sets of usage requirements (like compiler flags) that can be applied to other targets.
  • ALIAS Libraries for Namespacing: Use add_library(... ALIAS ...) to create clean, namespaced Project::Component names for your targets. This is a key modern CMake best practice.
  • IMPORTED Libraries for Binaries: Use add_library(... IMPORTED) to wrap pre-compiled third-party libraries. This creates a proper target that can be integrated into your dependency graph, abstracting away the physical file paths.

By mastering these patterns, you can move beyond simple builds and start designing sophisticated build systems that are a pleasure to work with, even as your projects scale in complexity.

Next Lesson
Lesson 23 of 25

Using Shared Libraries

Adding support for user-configurable library types and an initial introduction to target installation.

Have a question about this lesson?
Answers are generated by AI models and may not be accurate