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.

Greg Filak
Published

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:

  1. 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.
  2. An End-User Package: A graphical .msi installer for our GreeterApp 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:

  1. Internally run the install step to stage all the necessary files into a temporary directory.
  2. Use the specified generator (ZIP) to bundle the contents of that directory into an archive.
  3. 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:

  1. We must use at least CMake 3.25.
  2. 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

CMakePresets.json
CMakeLists.txt
Select a file to view its content

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 your CMakeLists.txt and set CPACK_* variables to control package metadata and generator choice.
  • Developer Packages: Use the ZIP or TGZ generator to create simple archives, perfect for sharing libraries with other developers.
  • Package Presets: Use packagePresets in your CMakePresets.json to create simple, named workflows for running cpack with specific configurations.
  • Components: Use the COMPONENT argument in your install() commands to group files. This allows you to generate different packages from the same project by selecting components in your package preset.
Next Lesson
Lesson 59 of 61

Packaging with GitHub Actions

Learn to automate the packaging process by integrating CPack with a multi-platform GitHub Actions workflow

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