Introduction

CMake is a very powerful build tool that is commonly used on C++ projects. Oftentimes, you want to bundle your C++ project/application into an installer that you can distrubute to a larger audience. This post will go over a small example illustrating how you can use CMake, along with CPack and NSIS to create a highly configurable installer for your needs.

Prerequisites

To follow along with this example, you can grab the code here. Additionally you’ll need the following tools installed:

  • CMake 3.14+
  • NSIS 3.0.0+

CPack

CPack is a packaging tool included with CMake that allows you to create packages of targets or files. It abstracts the package creation tool from your CMake code, enabling you to generate a variety of package types from a single code base. You can use a variety of packaging backends with CPack such as 7zip, zip, NSIS, IFW and so on. For this example we’ll be generating an installer using NSIS.

NSIS

NSIS is a scripting language/system to create windows installers. It’s extremely versatile and flexible, making it a powerful back end to CPack.

Steps

First we need to create a working C++ project so that we have something to distribute in the installer. The project will have a main application that depends on a single library.

The top level CMakeLists.txt looks like this:

cmake_minimum_required(VERSION 3.14)

project(CMakeInstallExample LANGUAGES C CXX)

list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)

# set the project version. 
set(PROJECT_VER_PATCH 0)
set(PROJECT_VER_MINOR 2)
set(PROJECT_VER_MAJOR 1)

set(PROJECT_VER "${PROJECT_VER_MAJOR}.${PROJECT_VER_MINOR}.${PROJECT_VER_PATCH}")

add_subdirectory(foo)

add_executable(CMakeInstallExample src/main.cpp)

target_link_libraries(CMakeInstallExample foo)

Nothing too crazy going on here. First we set the minimum required version of cmake, set the project name and languages and then create a version variable. Next we add the sub-directory that contains the library files (see here for details on how this project is set up). Finally, we add the main executable and then link the library previously added to the executable.

When you open the generated solution (if you’re using Visual Studio) you should see 6 build targets:

  • ALL_BUILD
  • CMakeInstallExample
  • foo
  • INSTALL
  • PACKAGE
  • ZERO_CHECK

The CMakeInstallExample target will build the main executable. Since foo is linked to the CMakeInstallExample target, it will be built as well (if necessary).

To build an installer, all you have to do is build the CMakeInstallerExample in Release, then build the PACKAGE target. Once the build has finished, you should have an installer in your build directory called CMakeInstallerExample-${PROJECT_VER}-win64.

So how does this work? Let’s break it down.

Install Rules

In the top level CMakeLists.txt there are the following install rules:

# set install rules (windows only)
if(WIN32)
    if(MSVC)
        install(TARGETS CMakeInstallExample
            DESTINATION bin COMPONENT binaries)
        # install the target file since the foo target isn't built in this context.
        install(FILES $<TARGET_FILE:foo>
            DESTINATION bin COMPONENT binaries)
        install(FILES data/data.txt
            DESTINATION data COMPONENT data_files)
    endif(MSVC)
endif(WIN32)

These install rules dictate:

  • What targets and/or files are installed
  • Where the targets and/or files are installed (relative to the base installation path)
  • What component the installed files are a part of

For example, the first install rule tells cmake to install the main executable target to the bin folder of the installation directory.

install(TARGETS CMakeInstallExample
            DESTINATION bin COMPONENT binaries)

Next, we install the target file (note the use of generator expressions here) of the foo library to the bin folder as well. When building shared libraries this will be a *.dll (or equivalent depending on your platform) file. Finally we also install a dummy data file data.txt in the data folder of the installation directory.

The result is the following directory structure in the installation location:

|${PROGRAM_INSTALL_DIRECTORY}/
|--bin/
|    CMakeInstallExample.exe
|    foo.dll
|--data/
|    data.txt

Additionally, because we specified the COMPONENT that the different items we’re installing belong to, we’ll get the option in the installer to install binaries and data_files. If we uncheck binaries in the installer, then everything under the bin folder above will not be installed. Similarly if we uncheck the Data files option in the installer, everything under the data folder will not be installed.

CPack Configuration

The last thing we have to do is give CPack all the information it needs to generate the installer. CPack will pass on all this information to the installer generator (NSIS in our case) and then a install-able package will be generated.

All the CPack configuration is under cmake/CpackConfig.cmake of the demo project. Most of the fields are self explanatory, but I’ll explain the ones that are a bit less obvious:

  • CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL This tells the installer to uninstall the previous version of the program we’re installing first, before proceeding with the current install.
  • CPACK_NSIS_MODIFY_PATH On windows this allows the installer to modify the PATH environment variable.
  • CPACK_PACKAGE_INSTALL_DIRECTORY Tells the installer where to install the program.

In the last section we set up the different components of the installer:

include(CPackComponent)

cpack_add_install_type(Full DISPLAY_NAME "Install Everything")
cpack_add_install_type(Upgrade DISPLAY_NAME "Upgrade Only")

cpack_add_component(binaries
    DISPLAY_NAME "Main application"
    DESCRIPTION "This will install the main application."
    REQUIRED
    INSTALL_TYPES Full Upgrade
)

cpack_add_component(data_files
    DISPLAY_NAME "Data files"
    DESCRIPTION "Fake data files for this example."
    INSTALL_TYPES Full
)

We have 2 components, binaries and data_files. So in the section of cmake above, we set the readable names of the components and a brief description. Then we tell CPack which components to include in the installer as well as what install types the components belong to. This will provide users with the option to do a “full” installation or an “upgrade” installation. By default, with the configuration we have, the main “binaries” will be installed when selecting the “Full” or the “Upgrade” installation type and the data files will be installed when selecting the “Full” install type.

And that’s it! You a fully functional, full featured installer. Obviously this is just a basic installer. If you’re really advanced you can take a gander at writing your own NSIS script instead of the default one that CPack uses. If you have questions please leave a comment below!

Hey there! Thanks for reading this article and visiting my site. Be sure to join the Discord and follow me on Github to ensure you don’t miss out on future content! Feel free to also check out my YouTube channel where I post tutorials and fun programming videos.

Leave a Comment

Your email address will not be published. Required fields are marked *

Loading...