Packaging with CPack
Learn to package your C++ projects using CMake's CPack. This lesson covers creating developer ZIP archives and professional Windows MSI installers with components.
In the last lesson, we set up a Continuous Integration (CI) pipeline using GitHub Actions. Our build server now automatically compiles our code and runs our tests on every pull request, acting as a quality gate to protect our main
branch.
But what happens to the files that are built? The executables, libraries, and other artifacts created by the CI run are temporary; they exist only for the duration of the job and are discarded when it finishes.
This is fine if our only goal is to test our project, but build servers have many more benefits, too. They're the natural place to perform tasks like using Doxygen to generate and publish the latest documentation, or packaging and releasing our entire project.
In the rest of this chapter, we'll focus on using CPack to package our project into easily sharable artifacts, such as simple .zip
archives or user-friendly Windows installers.
Later in the chapter, we'll have our build server (GitHub Actions) take over responsibility for this, as well as having it release the new versions of our project automatically.
From Install-Tree to Distributable Package
In an , we learned how to use the install()
command to define a clean layout for our project's outputs. The cmake --install
command takes the messy contents of our build directory and copies the necessary files into a clean "install-tree".
CPack builds upon this approach. It automatically runs this install step and then takes the contents of this install-tree and bundles them into a single archive or installer file.
We'll explore two common packaging scenarios:
- A Developer Package: A simple archive of our
GreeterLib
, packaged in a.zip
or similar format. It contains the headers, library files, and CMake configuration that another developer would need to use our library in their own project. - An End-User Package: A graphical
.msi
installer for ourGreeterApp
on Windows, designed for non-technical users to install our application with a few clicks.
Configuring CPack for a Developer Archive
The configuration for CPack lives right inside our CMakeLists.txt
file. The first step is to include the standard CPack
module, which makes all the necessary commands and variables available.
It's a common convention to place all CPack-related settings at the end of the root CMakeLists.txt
, with more complex configurations being extracted to a that we can include()
there.
CMakeLists.txt
cmake_minimum_required(VERSION 3.23)
project(Greeter)
include(cmake/Coverage.cmake)
include(cmake/Sanitize.cmake)
include(cmake/Tidy.cmake)
add_subdirectory(app)
add_subdirectory(greeter)
enable_testing()
add_subdirectory(tests)
add_subdirectory(benchmarks)
include(CPack)
To configure CPack's behaviour, we can set a series of CPACK_
variables. We can set these variables in all of the usual ways:
- Within our
CMakeLists.txt
files - Using
-D
arguments on the command line - Using presets, which we'll cover later
For example, let's configure a simple .zip
archive generator in our root CMakeLists.txt
file. If we're setting CPack variables within our CMakeList.txt
, it's important that we do this before we include()
the CPack
module:
CMakeLists.txt
# ...
# --- CPack Configuration ---
# Specify the generator for a ZIP archive
set(CPACK_GENERATOR "ZIP")
# Set basic package metadata
set(CPACK_PACKAGE_NAME "${PROJECT_NAME}")
set(CPACK_PACKAGE_VERSION "1.0")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Greeter Library")
set(CPACK_PACKAGE_VENDOR "StudyPlan.dev")
set(CPACK_PACKAGE_HOMEPAGE_URL "https://www.studyplan.dev")
# Include CPack AFTER setting the variables
include(CPack)
The most important command is the set(CPACK_GENERATOR "ZIP")
command. This tells CPack that we want it to use its built-in ZIP
generator to create a .zip
file. CPack supports many generators, including TGZ
(for .tar.gz
archives), DEB
(for Debian packages), and WIX
(for Windows installers, which we'll cover later).
This variable can be a list, which will ask CPack to produce multiple packages, eventually letting users download our content in whatever way they prefer:
set(CPACK_GENERATOR ZIP TGZ)
# Equivalently:
set(CPACK_GENERATOR "ZIP;TGZ")
The rest of our set()
commands are defining variables that CPack uses to generate metadata for the package.
CPack will choose sensible defaults for these values, so they are optional. For example, if we don't provide a name, version, or homepage URL for our package, CPack can instead use the values we set in our project()
command:
CMakeLists.txt
project(
Greeter VERSION "1.0"
HOMEPAGE_URL "https://www.studyplan.dev"
)
# ...
set(CPACK_GENERATOR "ZIP")
set(CPACK_PACKAGE_NAME "${PROJECT_NAME}")
set(CPACK_PACKAGE_VERSION "1.0")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Greeter Library")
set(CPACK_PACKAGE_VENDOR "StudyPlan.dev")
set(CPACK_PACKAGE_HOMEPAGE_URL "https://www.studyplan.dev")
include(CPack)
The list of all variables that can be used to control CPack's behavior is listed in the official documentation.
Generating the Library Package
With this configuration in place, generating the package is a simple command-line step that you run after a successful build. Remember, for this to work, we need appropriate install()
rules in place. Our library's CMakeLists.txt
file, with all the install rules we completed in our , is provided below:
greeter/CMakeLists.txt
cmake_minimum_required(VERSION 3.23)
project(GreeterLib VERSION 1.0.0)
include(GNUInstallDirs)
add_library(GreeterLib src/Greeter.cpp)
add_library(Greeter::Lib ALIAS GreeterLib)
set_target_properties(GreeterLib PROPERTIES
OUTPUT_NAME Greeter
EXPORT_NAME Lib
)
target_sources(GreeterLib
PUBLIC
FILE_SET HEADERS
BASE_DIRS "${PROJECT_SOURCE_DIR}/include"
FILES
"${PROJECT_SOURCE_DIR}/include/greeter/Greeter.h"
)
install(
TARGETS GreeterLib
EXPORT GreeterLibTargets
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
FILE_SET HEADERS DESTINATION
${CMAKE_INSTALL_INCLUDEDIR}
)
install(EXPORT GreeterLibTargets
FILE GreeterLibTargets.cmake
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/GreeterLib
NAMESPACE Greeter::
)
include(CMakePackageConfigHelpers)
configure_package_config_file(
"cmake/GreeterLibConfig.cmake.in"
"GreeterLibConfig.cmake"
INSTALL_DESTINATION
"${CMAKE_INSTALL_LIBDIR}/cmake/GreeterLib"
)
write_basic_package_version_file(
"GreeterLibConfigVersion.cmake"
VERSION ${PROJECT_VERSION}
COMPATIBILITY AnyNewerVersion
)
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/GreeterLibConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/GreeterLibConfigVersion.cmake"
DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/GreeterLib"
)
First, let's configure and build our project as usual from the build directory. We'll build in Release
mode, as that's what we would most commonly distribute.
If we don't already have presets for this, we should add them:
CMakePresets.json
{
"version": 3,
"configurePresets": [
// ...
{
"name": "release",
"inherits": "default",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
}
],
"buildPresets": [
// ...
{
"name": "release",
"configurePreset": "release",
"configuration": "Release"
}
],
"testPresets": [
// ...
{
"name": "release",
"inherits": "default",
"configurePreset": "release"
}
],
// ...
}
Then, we configure and build our project in the usual way:
cmake --preset release
cmake --build --preset release
Now, from our build directory, we run the cpack
command:
cd ./build
cpack
CPack will now:
- Internally run the
install
step to stage all the necessary files into a temporary directory. - Use the specified generator (
ZIP
) to bundle the contents of that directory into an archive. - Place the final archive in the build directory.
CPack: Create package using ZIP
CPack: Install projects
CPack: - Install project: Greeter []
CPack: Create package
CPack: - package: D:/projects/cmake/build/Greeter-1.0-win64.zip generated.
If you look in your build
directory, you'll find a new file named something like Greeter-1.0-win64.zip
. The exact nature of the package, such the architecture our library was compiled for, depends on our environment. We can control this in all the ways we covered previously, such as introducing a toolchain file.
Eventually, these packages will be created by our build server, not our local machine. In many cases, this can eliminate the need for cross-compilation. For example, if we want to build a Windows version of our library or program, we can just use a Windows build server.
In GitHub actions, that's as simple as creating a workflow that sets runs-on: windows-latest
in our YAML file. We'll walk through an example of this later in the chapter.
For now, if we take a look inside the generated .zip
file, we should see see it contains the complete install-tree we defined in our install()
commands, cleanly organized and ready for another developer to use:
Greeter-1.0-win64/
├─ include/
│ └─ greeter/
│ └─ Greeter.h
└─ lib/
├─ libGreeter.a (or Greeter.lib)
└─ cmake/
└─ GreeterLib/
├─ GreeterLibConfig.cmake
├─ GreeterLibConfigVersion.cmake
├─ GreeterLibTargets.cmake
└─ GreeterLibTargets-release.cmake
Simplifying Packaging with Presets
Just as we did for configuring, building, and testing, we can add presets for packaging to our CMakePresets.json
to make this process friendlier and more repeatable.
Version Requirements for Package Presets
The packagePresets
feature is a newer addition to CMake. To use it, we must meet two version requirements:
- We must use at least CMake 3.25.
- Our
CMakePresets.json
file must use schema version 6 or higher.
Let's update our root CMakeLists.txt
and CMakePresets.json
to reflect these new requirements.
CMakeLists.txt
cmake_minimum_required(VERSION 3.25)
project(Greeter VERSION "1.0")
# ... rest of file is unchanged
CMakePresets.json
{
"version": 6,
...
}
Creating a Package Preset
Now, we can add a packagePresets
section to our file. A package preset links to a configurePreset
to know which build directory it should operate on.
It can also contain a variables
map, where we can set the various CMake configuration properties we're currently defining in our CMakeLists.txt
. Some of the most imporant variables, such as generators
, packageName
, packageVersion
, description
, and vendor
have dedicated keys that can be set at the top level of the preset.
Let's create a default
preset that moves our configuration out of our CMakeLists.txt
and into CMakePresets.json
:
Files
We can inherit from this preset in the usual way. Let's add a dev-zip
preset for generating .zip
archives of our developer utilities (ie, the GreeterLib
target we set install()
rules for):
CMakePresets.json
{
"version": 6,
// ...
"packagePresets": [{
"name": "default",
"configurePreset": "release",
"description": "The Greeter Library",
"vendorName": "StudyPlan.dev",
"variables": {
"CPACK_PACKAGE_CONTACT": "support@studyplan.dev"
}
}, {
"name": "dev-zip",
"displayName": "Developer Archive (ZIP)",
"inherits": "default",
"generators": ["ZIP"]
}]
}
With this preset defined, our packaging command becomes much simpler and can be run from the project root, just like our other preset commands.
cpack --preset dev-zip
This command will find the zip
preset and it's base default
preset, identify the associated build directory, run the install
step if necessary, and execute the ZIP
generator, producing the exact same distributable archive as before.
CPack: Create package using ZIP
CPack: Install projects
CPack: - Install project: Greeter []
CPack: Create package
CPack: - package: D:/projects/cmake/build/Greeter-1.0-win64.zip generated.
Using Components to Group Files
Our current package is a monolith; it includes everything we've told install()
about. That's fine for now, as we have only defined installation rules for our library.
But what if we want to package different parts of our project for different audiences? For example, a developer using our library needs the headers and CMake files, but someone who just wants to install and run our program may only need the executable. We may want to create a third packages that just includes our documentation.
We need a way to group our installed files into different categories, letting us specify exactly what we want to package on each invocation of cpack
. This is the job of CPack Components.
A component is simply a named group of installed files. In this lesson, we'll take the first step by grouping all the files related to GreeterLib
into a single Development
component. In the next lesson, we'll add a separate Applications
component for our executable and create a user-friendly installer.
Step 1: Assigning the Development
Component
We'll edit greeter/CMakeLists.txt
and add the COMPONENT
keyword to everything we're installing, by tracking down our install()
commands. This logically tags every file installed by this script as belonging to the same group.
We can call our component whatever we want, but we'll go with Development
in this example given our library, header files and CMake files are developer tools:
greeter/CMakeLists.txt
cmake_minimum_required(VERSION 3.23)
project(GreeterLib VERSION 1.0.0)
include(GNUInstallDirs)
add_library(GreeterLib src/Greeter.cpp)
add_library(Greeter::Lib ALIAS GreeterLib)
set_target_properties(GreeterLib PROPERTIES
OUTPUT_NAME Greeter
EXPORT_NAME Lib
)
target_sources(GreeterLib
PUBLIC
FILE_SET HEADERS
BASE_DIRS "${PROJECT_SOURCE_DIR}/include"
FILES
"${PROJECT_SOURCE_DIR}/include/greeter/Greeter.h"
)
install(
TARGETS GreeterLib
EXPORT GreeterLibTargets
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
COMPONENT Development
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
COMPONENT Development
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
COMPONENT Development
FILE_SET HEADERS DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
COMPONENT Development
)
install(EXPORT GreeterLibTargets
FILE GreeterLibTargets.cmake
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/GreeterLib
NAMESPACE Greeter::
COMPONENT Development
)
include(CMakePackageConfigHelpers)
configure_package_config_file(
"cmake/GreeterLibConfig.cmake.in"
"GreeterLibConfig.cmake"
INSTALL_DESTINATION
"${CMAKE_INSTALL_LIBDIR}/cmake/GreeterLib"
)
write_basic_package_version_file(
"GreeterLibConfigVersion.cmake"
VERSION ${PROJECT_VERSION}
COMPATIBILITY AnyNewerVersion
)
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/GreeterLibConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/GreeterLibConfigVersion.cmake"
DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/GreeterLib"
COMPONENT Development
)
Step 2: Configuring CPack
Next, we need to tell CPack about our components. To make CPack's archive generators (such as ZIP) that we want them to support components, we need to enable the CPACK_ARCHIVE_COMPONENT_INSTALL
flag before we include CPack.
set(CPACK_ARCHIVE_COMPONENT_INSTALL ON)
After the include(CPack)
command, we have access to the cpack_add_component()
command, which we can use to tell CMake about our component:
cpack_add_component(Development)
This command allows us to provide more information about our component, which can be helpful for more complex packaging requirements, or simply as a form of documentation.
We've added a display name and description to our Development
component in our complete CMakeLists.txt
file below:
CMakeLists.txt
cmake_minimum_required(VERSION 3.23)
project(Greeter VERSION 1.0)
include(cmake/Coverage.cmake)
include(cmake/Sanitize.cmake)
include(cmake/Tidy.cmake)
add_subdirectory(app)
add_subdirectory(greeter)
enable_testing()
add_subdirectory(tests)
add_subdirectory(benchmarks)
set(CPACK_ARCHIVE_COMPONENT_INSTALL ON)
include(CPack)
cpack_add_component(Development
DISPLAY_NAME "The Greeter Library"
DESCRIPTION "Compiled Library, Header, and CMake Files"
)
Step 3: Configuring CPack
Now, we have an easy way to tell CPack exactly what it should include in each package it generates.
Which components should be included in a package is controlled by the CPACK_COMPONENTS_ALL
variable, which we can provide in our packagePreset
:
CMakePresets.json
{
"version": 6,
// ...
"packagePresets": [
// ...
{
"name": "dev-zip",
"displayName": "Developer Archive (ZIP)",
"configurePreset": "default",
"generators": ["ZIP"],
"variables": {
"CPACK_COMPONENTS_ALL": "Development"
}
}
]
}
Step 4: Verifying the Result
Let's run our preset to confirm our packaging still works, and that our developer package remains unchanged. From the project root:
cmake --preset release
cpack --build --preset release
cpack --preset dev-zip
The resulting .zip
archive will should contain the exact same install-tree for our library as before:
Greeter-1.2.3-win64/
├─ include/
│ └─ greeter/
│ └─ Greeter.h
└─ lib/
├─ Greeter.lib
└─ cmake/
└─ ...
Even though our output is currently the same, this has improved the structure and flexibility of our build. Later in the chapter, we'll take advantage of this when we create a separate package from the same project.
Summary
CPack is CMake's integrated solution for turning our build artifacts into distributable packages. It uses the install()
rules we've already defined to create everything from simple archives to complex graphical installers.
- CPack Configuration: Add
include(CPack)
to yourCMakeLists.txt
and setCPACK_*
variables to control package metadata and generator choice. - Developer Packages: Use the
ZIP
orTGZ
generator to create simple archives, perfect for sharing libraries with other developers. - Package Presets: Use
packagePresets
in yourCMakePresets.json
to create simple, named workflows for runningcpack
with specific configurations. - Components: Use the
COMPONENT
argument in yourinstall()
commands to group files. This allows you to generate different packages from the same project by selecting components in your package preset.
Packaging with GitHub Actions
Learn to automate the packaging process by integrating CPack with a multi-platform GitHub Actions workflow