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.
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:
- How do we apply a common set of compiler flags to some, but not all, targets without duplicating code?
- How do we create names for our library targets that prevent naming collisions, especially in large projects?
- 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: UseINTERFACE
targets to bundle common sets of usage requirements (like compiler flags) that can be applied to other targets.ALIAS
Libraries for Namespacing: Useadd_library(... ALIAS ...)
to create clean, namespacedProject::Component
names for your targets. This is a key modern CMake best practice.IMPORTED
Libraries for Binaries: Useadd_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.
Using Shared Libraries
Adding support for user-configurable library types and an initial introduction to target installation.