Area Man (Kinda) Figures out How to Use CPack

Lately I have been working on a C++11 project that uses CMake. I find CMake to be approachable, useful, and sometimes somewhat weird. But it gets the job done.

I suppose I should document what I have managed to learn so far (which is: just enough to be useful), and I will do that in due time, in a regularly scheduled future weblog update (haha).

For now here are some notes about a thing I struggled with, for longer than I should have: making an RPM package from ye olde CMake project.

CMake ships with a companion program, CPack, which makes creating binary packages easy. All you need is a line like this in your top-level CMakeLists.txt:

include(CPack)

Our project generates the good ole Unix Makefiles, so I should have been able to do just run some commands and get on with my day:

$ cd $BUILDDIR
$ cmake $SOURCEDIR
$ make package # or just run 'cpack'

As we know nothing in computering land works that way.

With the basic configuration, the above produced three packages: a self-extracting shell script, a tar.gz package, and a Unix compressed .Z package. But here's a problem:

$ ls -l project-0.1.1-Linux.*
-rwxrwxrwx 1 sajith sajith 3818 Sep  8 10:35 project-0.1.1-Linux.sh
-rw-rw-r-- 1 sajith sajith   29 Sep  8 10:35 project-0.1.1-Linux.tar.gz
-rw-rw-r-- 1 sajith sajith   54 Sep  8 10:35 project-0.1.1-Linux.tar.Z

They are all empty! Also I still do not have that RPM package I needed. So I set the CPACK_GENERATOR variable before the include(CPack) line:

set(CPACK_GENERATOR "RPM")
include(CPack)

In order to have the intended artifacts (binaries, documentation, etc) inside the archives, I also needed some install lines in the appropriate CMakeLists.txt files. (We have not been "make install"-ing our project, so we had omitted the install lines. CMake-built binary packages get their contents from the install step, and it took a bit of figuring out.)

install(TARGETS binary DESTINATION bin)
install(DIRECTORY project-config-files DESTINATION etc)
install(FILES README.txt DESTINATION share/doc/project)

With that, I have a basic RPM file. For further customization, there are a bunch of extra variables that we can set:

set(CPACK_GENERATOR        "RPM")
set(CPACK_PACKAGE_VERSION  ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR})
set(CPACK_PACKAGE_RELEASE  1)

set(CPACK_PACKAGE_NAME                 "usual-widgets")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY  "Some usual widgets for day-to-day use")
set(CPACK_PACKAGE_VENDOR               "Usual Business Corp")
set(CPACK_PACKAGE_CONTACT              "Your Humble Packager <packager@example.net>")

set(CPACK_PACKAGING_INSTALL_PREFIX  ${CMAKE_INSTALL_PREFIX})
set(CPACK_PACKAGE_FILE_NAME         "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-${CPACK_PACKAGE_RELEASE}.${CMAKE_SYSTEM_PROCESSOR}")

set(CPACK_RPM_PACKAGE_LICENSE  "GPLv3")
set(CPACK_RPM_PACKAGE_GROUP    "Applications/Internet")
set(CPACK_RPM_PACKAGE_URL      "https://project.example.net/packages")

We can also instruct CPack to generate additional kinds of packages:

set(CPACK_GENERATOR  "RPM;DEB;TGZ")

And there are more knobs to turn:

set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "amd64")
set(CPACK_DEBIAN_PACKAGE_VERSION      1)
set(CPACK_DEBIAN_PACKAGE_SECTION      "Network")
set(CPACK_DEBIAN_PACKAGE_HOMEPAGE     "https://project.example.net/packages")

Another thing I figured out is that I can set CMAKE_INSTALL_PREFIX to somewhere other than the default /usr/, when running cmake:

$ cmake -DCMAKE_INSTALL_PREFIX=/opt/UsualBusinessCorp/ $SOURCEDIR

I still do not know how to sign the packages, etc., but then again, this is just enough to be useful, and that is all I need.

Extra: verbose output

What if I need a little more verbose output from CPack?

cpack -V -G RPM -D CPACK_RPM_PACKAGE_DEBUG=1
cpack -V -G DEB -D CPACK_DEBIAN_PACKAGE_DEBUG=1

Extra: the dreaded comma

Here's a frustrating but really silly thing I dealt with: I inserted an extra comma in one of the set() directives by mistake, and then spent an inordinate amount of time figuring out why things weren't working the way I thought they should be working.

CMake silently ignores the directive when you do set(VAR, VAL): it has to be set(VAR VAL) or CMake will do its thing and not the thing you're asking it to do. You do not want this to happen.

(Posted on 8 September 2017.)