cmakemakefileninja

How can I still use `make` while building with `ninja` when I use cmake to build a project?


Ninja as a build tool is much more advanced and helpful to debug than Make.

However typing ninja is much slower due to the right-right-right-right-left pattern while make is super fast with its right-left-right-left patter. I want to type make while it builds with ninja.

I have several projects which for historical reasons I have to use either make or ninja and I would like to normalize all to make.

I thought about using some shell tricks like alias make='/bin/make || ninja' but there are some cons:

  1. They always depend on setting an environment variable, either by hand or on .bashrc. I don't want to do that.

  2. An alias depends on knowing the location of the make and ninja binaries

So I was thinking about the easiest way to add something to my CMakeLists.txt files so it drops a Makefile, something like drop_makefile(). Even if the cost is to add that line into each of them in a project.

It would be easy enough to drop something like

all:
    ninja

which works fine if all you do is make.

However if I wanted to make a specific target that exists within ninja it would not work like make tests, which should translate into ninja tests.


Solution

  • If what you want is to generate a Makefile containing all the targets known to CMake, you can use the BUILDSYSTEM_TARGETS property. As this is a directory property, you will need to traverse all directories of your project. Luckily, the answers to Programmatically get all targets in a CMake project already show how this can be done. I tested with user ilya1725's function get_all_targets.

    All that's left is to iterate over the list and create Makefile syntax that calls ninja like this:

    function(drop_makefile)
        # Bail if CMake is already generating Makefiles
        if(CMAKE_GENERATOR STREQUAL "Unix Makefiles")
            return()
        endif()
    
        # Function get_all_targets taken from https://stackoverflow.com/questions/60211516/programmatically-get-all-targets-in-a-cmake-project
        get_all_targets(_all_targets .)
    
        # Make magic "all" target the first one
        list(PREPEND _all_targets "all")
        foreach(_target IN LISTS _all_targets)
            string(APPEND _makefile ".PHONY: ${_target}\n${_target}:\n\tninja ${_target}\n")
        endforeach()
        file(WRITE ${CMAKE_BINARY_DIR}/Makefile ${_makefile})
    endfunction(drop_makefile)
    

    Call the function in your project's root directory and the result will be a Makefile containing all (non-imported) targets known to your project. I can't shake off the feeling this kind of defeats the purpose of CMake.

    By the way, make will evaluate its options but not forward them to ninja. Something like make -j32 will spawn 32 jobs but won't call ninja -j32 under the hood.

    Furthermore, CMake provides its own abstraction for the build tool as cmake --build. This will call the right tool and digests some common options like --jobs. In this question, keystrokes are a factor and regarding them, this is right out of the question.

    A much more simplistic solution would be to give each project its own configuration file alias_make.sh containing one of the following lines:

    alias make=make              # for a native Makefile project
    alias make=ninja             # for a native ninja project
    alias make='cmake --build'   # for all CMake projects
    

    When starting to work on a project, all you need to do is source alias_make.sh and you can use make in blissful ignorance of actually calling ninja (well, unless it breaks because they're not the same, of course).

    By the way, I frequently use mak for such aliases or shell functions because it saves yet another keystroke. Consider dropping the last letter if you think you can overcome your muscle memory.

    In closing, let me say that make and ninja are not the same so every attempt to masquerade one as the other will result in a leaky abstraction.