Table of Contents

  1. Migration Strategy
  2. From Makefiles
  3. From Autotools
  4. From qmake
  5. Migrating Install Rules
  6. Test Migration
  7. Common Pitfalls
  8. Validation Techniques
  9. Conclusion & Next Steps
Back to CMake Mastery Series

Part 28: Porting Projects to CMake

June 4, 2026 Wasil Zafar 40 min read

Migrate existing projects from Makefiles, Autotools, and qmake to modern CMake with incremental strategies, platform check conversions, and thorough validation techniques.

Migration Strategy

Porting a project to CMake is a non-trivial endeavor that requires understanding both the source build system and CMake's modern idioms. The CMake Tutorial covers building from scratch, but migration demands a different approach — preserving existing behavior while modernizing the build infrastructure.

Key Insight: The goal of migration isn't just "make it compile with CMake" — it's to produce a modern, target-based CMake build that correctly exports dependencies, supports multiple generators, and provides proper install rules. A direct 1:1 translation of Makefile variables to CMake variables produces legacy-style CMake that's worse than either system.

Incremental Approach

For large projects, migrate one component at a time while keeping the old build system operational:

Incremental Migration Strategy
        flowchart TD
            A[Existing Build System] --> B[Identify leaf libraries]
            B --> C[Create CMakeLists.txt for leaf]
            C --> D[Verify: same object files produced]
            D --> E{All leaves migrated?}
            E -->|No| B
            E -->|Yes| F[Migrate intermediate libraries]
            F --> G[Migrate executables]
            G --> H[Migrate install/package rules]
            H --> I[Remove old build system]
            I --> J[Modern CMake project]
    
# Phase 1: Parallel builds — verify CMake produces identical artifacts
# Build with old system
make clean && make -j8
find . -name "*.o" | sort > old-objects.txt
nm libfoo.a | sort > old-symbols.txt

# Build with CMake
cmake -S . -B build-cmake -DCMAKE_BUILD_TYPE=Release
cmake --build build-cmake -j8
find build-cmake -name "*.o" | sort > cmake-objects.txt
nm build-cmake/libfoo.a | sort > cmake-symbols.txt

# Compare symbols (order-independent)
diff old-symbols.txt cmake-symbols.txt

Big-Bang Approach

For smaller projects or when the old build system is too broken to maintain, convert everything at once:

# Step 1: Document what the old build system does
make -n 2>&1 | tee old-build-commands.txt
# This prints commands without executing — shows all flags, paths, link lines

# Step 2: Extract compiler flags per target
grep -E "^(gcc|g\+\+|clang)" old-build-commands.txt | \
    awk '{print $NF, $0}' | sort > flags-by-target.txt

# Step 3: Identify link dependencies
grep -E "\-l[a-z]" old-build-commands.txt | \
    grep -oE "\-l[a-zA-Z0-9_]+" | sort -u > link-deps.txt

From Makefiles

Mapping Targets

Every Makefile target maps to a CMake target. The key difference: Makefiles express recipes (how to build), while CMake expresses requirements (what to build).

# Original Makefile
CC = gcc
CFLAGS = -Wall -Wextra -O2 -I./include
LDFLAGS = -lm -lpthread

SRCS = src/main.c src/utils.c src/parser.c
OBJS = $(SRCS:.c=.o)

myapp: $(OBJS)
	$(CC) $(OBJS) $(LDFLAGS) -o $@

libutil.a: src/utils.o src/parser.o
	ar rcs $@ $^

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

clean:
	rm -f $(OBJS) myapp libutil.a
# Equivalent modern CMakeLists.txt
cmake_minimum_required(VERSION 3.21)
project(MyApp LANGUAGES C)

# Library target (replaces libutil.a rule)
add_library(util STATIC
    src/utils.c
    src/parser.c
)
target_include_directories(util PUBLIC include)

# Executable target (replaces myapp rule)
add_executable(myapp src/main.c)
target_link_libraries(myapp PRIVATE
    util
    m        # -lm
    Threads::Threads  # -lpthread (portable)
)

# Compiler warnings (replaces CFLAGS)
target_compile_options(myapp PRIVATE -Wall -Wextra)
target_compile_options(util PRIVATE -Wall -Wextra)

# Find pthreads portably
find_package(Threads REQUIRED)

Converting Compiler Flags

Makefile PatternCMake EquivalentNotes
CFLAGS += -I./includetarget_include_directories(t PUBLIC include)Use PUBLIC for headers others need
CFLAGS += -DVERSION=2target_compile_definitions(t PRIVATE VERSION=2)Scoped to target
LDFLAGS += -L/opt/libtarget_link_directories(t PRIVATE /opt/lib)Prefer find_package instead
LDFLAGS += -lzfind_package(ZLIB REQUIRED) + target_link_libraries(t PRIVATE ZLIB::ZLIB)Imported targets preferred
CFLAGS += -std=c11target_compile_features(t PUBLIC c_std_11)Portable standard selection
CFLAGS += -O2CMAKE_BUILD_TYPE=ReleaseDon't hardcode optimization
Lab Exercise Makefile to CMake Conversion

Objective: Convert a real open-source project's Makefile to CMake.

Clone a small C project that uses plain Makefiles (e.g., jq, suckless tools, or htop). Run make -n to capture all build commands, then create a CMakeLists.txt that produces the same binary. Verify with diff on the stripped executables and run the project's test suite with both builds.

Makefile migration real-world

From Autotools

configure.ac Mapping

Autotools uses configure.ac (M4 macros) and Makefile.am (automake templates). The mapping to CMake is systematic but requires understanding both systems. Reference: cmake-modules(7) for all check modules.

# Autotools configure.ac (typical)
AC_PREREQ([2.69])
AC_INIT([myproject], [1.0.0])
AM_INIT_AUTOMAKE([foreign subdir-objects])
AC_PROG_CC
AC_PROG_CXX

# Feature checks
AC_CHECK_HEADERS([sys/epoll.h sys/event.h])
AC_CHECK_FUNCS([mmap posix_fadvise])
AC_CHECK_LIB([pthread], [pthread_create])
AC_CHECK_LIB([z], [deflate])

# Conditional compilation
AC_ARG_ENABLE([ssl],
    [AS_HELP_STRING([--enable-ssl], [Enable SSL support])],
    [enable_ssl=$enableval], [enable_ssl=no])
if test "x$enable_ssl" = xyes; then
    PKG_CHECK_MODULES([OPENSSL], [openssl >= 1.1])
fi

AC_CONFIG_FILES([Makefile src/Makefile])
AC_OUTPUT
# Equivalent CMakeLists.txt
cmake_minimum_required(VERSION 3.21)
project(myproject VERSION 1.0.0 LANGUAGES C CXX)

# Feature checks (replace AC_CHECK_HEADERS)
include(CheckIncludeFile)
check_include_file(sys/epoll.h HAVE_SYS_EPOLL_H)
check_include_file(sys/event.h HAVE_SYS_EVENT_H)

# Function checks (replace AC_CHECK_FUNCS)
include(CheckFunctionExists)
check_function_exists(mmap HAVE_MMAP)
check_function_exists(posix_fadvise HAVE_POSIX_FADVISE)

# Library checks (replace AC_CHECK_LIB)
find_package(Threads REQUIRED)
find_package(ZLIB REQUIRED)

# Optional feature (replace AC_ARG_ENABLE)
option(ENABLE_SSL "Enable SSL support" OFF)
if(ENABLE_SSL)
    find_package(OpenSSL 1.1 REQUIRED)
endif()

# Generate config header (replace AC_CONFIG_HEADERS)
configure_file(config.h.in config.h @ONLY)

add_executable(myproject src/main.c src/network.c)
target_link_libraries(myproject PRIVATE
    Threads::Threads
    ZLIB::ZLIB
    $<$:OpenSSL::SSL>
)
target_include_directories(myproject PRIVATE ${CMAKE_BINARY_DIR})

Platform Checks

Autotools MacroCMake ModuleCMake Command
AC_CHECK_HEADERSCheckIncludeFilecheck_include_file(header.h VAR)
AC_CHECK_FUNCSCheckFunctionExistscheck_function_exists(func VAR)
AC_CHECK_LIBfind_package / find_libraryfind_package(Pkg REQUIRED)
AC_CHECK_SIZEOFCheckTypeSizecheck_type_size("int" SIZEOF_INT)
AC_CHECK_MEMBERSCheckStructHasMembercheck_struct_has_member(...)
PKG_CHECK_MODULESPkgConfigpkg_check_modules(... IMPORTED_TARGET)

From qmake

Qt's qmake uses .pro files. Since Qt 6, CMake is the officially supported build system. The mapping is straightforward:

# Original qmake .pro file
QT += core gui widgets network
TARGET = myqtapp
TEMPLATE = app

SOURCES += main.cpp mainwindow.cpp network.cpp
HEADERS += mainwindow.h network.h
FORMS += mainwindow.ui
RESOURCES += resources.qrc

CONFIG += c++17
LIBS += -lcurl
# Equivalent CMakeLists.txt for Qt 6
cmake_minimum_required(VERSION 3.21)
project(myqtapp LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)

find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Network)
find_package(CURL REQUIRED)

add_executable(myqtapp
    main.cpp
    mainwindow.cpp
    mainwindow.h
    network.cpp
    network.h
    mainwindow.ui
    resources.qrc
)

target_link_libraries(myqtapp PRIVATE
    Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Network
    CURL::libcurl
)
Tip: Qt provides a conversion tool: qmake2cmake (part of qt-tools). It generates a starting CMakeLists.txt from your .pro file. The output often needs manual refinement but saves significant time on large projects.

Migrating Install Rules

# Modern CMake install rules (replace make install targets)
include(GNUInstallDirs)

install(TARGETS myapp mylib
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)

install(DIRECTORY include/
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/myproject
    FILES_MATCHING PATTERN "*.h"
)

install(FILES LICENSE README.md
    DESTINATION ${CMAKE_INSTALL_DOCDIR}
)

# Generate and install CMake config files for downstream consumers
install(TARGETS mylib EXPORT MyProjectTargets)
install(EXPORT MyProjectTargets
    FILE MyProjectTargets.cmake
    NAMESPACE MyProject::
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyProject
)

Test Migration

# Convert test targets to CTest
enable_testing()

# Simple test (replaces: make test or make check)
add_executable(test_parser tests/test_parser.cpp)
target_link_libraries(test_parser PRIVATE util GTest::gtest_main)
add_test(NAME parser_tests COMMAND test_parser)

# Test with environment variables (common in Autotools)
add_test(NAME integration_test COMMAND ${CMAKE_SOURCE_DIR}/tests/run_integration.sh)
set_tests_properties(integration_test PROPERTIES
    ENVIRONMENT "TEST_DATA_DIR=${CMAKE_SOURCE_DIR}/tests/data"
    TIMEOUT 120
    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)
Lab Exercise Autotools to CMake Migration

Objective: Port an Autotools project to CMake while maintaining test compatibility.

Choose a small Autotools project (e.g., libconfig or jansson). Run ./configure && make check to establish a baseline. Create CMakeLists.txt with equivalent checks, builds, and tests. Verify all tests pass identically. Compare config.h output from both systems.

Autotools migration testing

Common Pitfalls

Warning — Common Migration Mistakes:
  • Variable pollution: Using CMAKE_CXX_FLAGS globally instead of per-target target_compile_options()
  • GLOB abuse: Using file(GLOB ...) for source files — CMake can't detect new files without reconfigure
  • Missing dependencies: Not declaring link dependencies that "worked" due to link order luck
  • Hardcoded paths: Embedding /usr/local/lib instead of using find_package()
  • Ignoring visibility: Making everything PUBLIC when PRIVATE would be correct
# BAD: Variable pollution (legacy style)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra")
include_directories(${PROJECT_SOURCE_DIR}/include)
link_directories(/opt/custom/lib)

# GOOD: Target-scoped properties (modern style)
target_compile_options(mylib PRIVATE -Wall -Wextra)
target_include_directories(mylib PUBLIC
    $
    $
)
find_library(CUSTOM_LIB custom PATHS /opt/custom/lib)
target_link_libraries(mylib PRIVATE ${CUSTOM_LIB})
Migration Validation Pipeline
        flowchart LR
            A[Old Build] --> B[Capture Artifacts]
            C[CMake Build] --> D[Capture Artifacts]
            B --> E{Compare}
            D --> E
            E -->|Symbols match| F[Run Tests]
            E -->|Mismatch| G[Debug Differences]
            F -->|All pass| H[Migration Complete]
            F -->|Failures| G
            G --> I[Fix CMakeLists.txt]
            I --> C
    

Validation Techniques

# Compare compile commands between old and new build systems
# CMake generates compile_commands.json with:
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -S . -B build

# Extract flags per file and compare
jq -r '.[] | "\(.file): \(.command)"' build/compile_commands.json | sort > cmake-flags.txt

# From old Makefile (using make -n):
make -n 2>&1 | grep -E "^(gcc|g\+\+)" | sort > make-flags.txt

# Diff to find missing/extra flags
diff make-flags.txt cmake-flags.txt
# Verify link lines match
# CMake: check link.txt files in build tree
find build -name "link.txt" -exec cat {} \;

# Compare shared library ABI
nm -D libfoo.so | awk '{print $3}' | sort > cmake-exports.txt
nm -D old-build/libfoo.so | awk '{print $3}' | sort > old-exports.txt
diff old-exports.txt cmake-exports.txt
Lab Exercise Validation Script

Objective: Create an automated validation script for your migration.

Write a shell script that builds with both systems, compares binary sizes (within 5% tolerance), verifies symbol tables match, runs all tests with both builds, and checks that installed files are in the same locations. This becomes your regression test during the migration period.

validation automation testing

Conclusion & Next Steps

Porting to CMake is an investment that pays dividends in cross-platform support, IDE integration, and modern dependency management. Whether migrating incrementally from a working Makefile or converting a complex Autotools project, the key is validation — compare artifacts, run tests, and verify install layouts at every step.

Next in the Series

In Part 29: Creating Reproducible Build Environments, we'll ensure that the same source always produces the same binary by pinning toolchains, dependencies, and configurations with CMake Presets, Docker, and deterministic compiler flags.