Skip to content

Instantly share code, notes, and snippets.

@arichardson
Forked from nevkontakte/CLion_Ninja.md
Last active January 25, 2024 15:23
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save arichardson/3de8adaebec0d488c13911edd2e0fe63 to your computer and use it in GitHub Desktop.
Save arichardson/3de8adaebec0d488c13911edd2e0fe63 to your computer and use it in GitHub Desktop.
Ninja support for CLion IDE

Ninja support for CLion IDE (working with newer versions of clion)

This script enables Ninja-powered builds in CLion IDE by wrapping around CMake, which it uses. See my blog post for details.

Disclaimer

This script is provided AS IS with no guarantees given or responsibilities taken by the author. This script relies on undocumented features of CLion IDE and may lead to instability of build and/or IDE. Use it on your own risk under WTFPL terms.

Getting started

Supported OS: I've tested this scipt under OS X and python 3 as well as Ubuntu 16.04 with python3. Windows might work too.

  1. Make sure you have python 3 installed.
  2. Download cmake_ninja_wrapper.py and give it execution permission.
  3. Edit REAL_CMAKE variable at the beginning of the script to point at CLion's bundled CMake binary (recommended) or at system CMake.
  4. In CLion, go to Settings → Build, Execution, Deployment → Toolchains
  5. Under "CMake executable" select cmake_ninja_wrapper.py script.

Make sure that CLion successfully detects make program (it should be still /usr/bin/make) and C/C++ compiler. At this point you are done. Go, reload you CMake project and try to build it.

Troubleshooting

In case of any troubles try setting TRACING variable at the beginning of the file to True and re-parsing CMake project in IDE. After that look at /tmp/cmake_wrapper.log for any insights :-)

#!/usr/bin/env python3
import sys
import os
import subprocess
import shutil
import re
import select
# ---------------------- Configuration section ------------------------------
REAL_CMAKE = '/usr/local/bin/cmake'
if not os.path.isfile(REAL_CMAKE):
REAL_CMAKE = '/usr/bin/cmake'
if not os.path.isfile(REAL_CMAKE):
REAL_CMAKE = shutil.which('cmake')
if not os.path.isfile(REAL_CMAKE):
sys.exit('Could not find cmake!')
NINJA_PATH = shutil.which('ninja')
if not os.path.isfile(NINJA_PATH):
sys.exit('Could not find ninja!')
TRACING = False
# --------------------------- Code section ----------------------------------
def trace(message, argv = []):
if not TRACING:
return
with open('/tmp/cmake_wrapper.log', 'a') as log:
if not argv == []:
log.write("\n\n")
log.write(message)
if not argv == []:
argv = '"%s"' % ('" "'.join(argv))
log.write("\n\n\t%s\n\tat: %s\n" % (argv, os.getcwd()))
def call_cmake(passing_args):
"""Call real cmake as a subprocess passing it's output both to stdout and trace file."""
passing_args = [REAL_CMAKE] + passing_args
trace("Calling real cmake:", passing_args)
proc = subprocess.Popen(passing_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0)
while True:
reads = [proc.stdout.fileno(), proc.stderr.fileno()]
ret = select.select(reads, [], [])
for fd in ret[0]:
if fd == proc.stdout.fileno():
line = proc.stdout.readline()
sys.stdout.buffer.write(line)
sys.stdout.flush()
trace(line)
if fd == proc.stderr.fileno():
line = proc.stderr.readline()
sys.stderr.buffer.write(line)
sys.stderr.flush()
trace(line)
if proc.poll() != None:
break
for line in proc.stdout:
sys.stdout.buffer.write(line)
trace(line)
for line in proc.stderr:
sys.stderr.buffer.write(line)
trace(line)
return proc.poll()
def is_real_project():
"""Detect if called inside clion private directory."""
cwd = os.getcwd()
# return "clion" in cwd and "cmake" in cwd and "generated" in cwd
return True
class CMakeCache(object):
"""CMake cache management utility"""
def __init__(self, path):
super(CMakeCache, self).__init__()
self.path = path
def alter(self, variable, value):
"""
Change a variable value in CMake cache.
TODO: Add variable if it doesn't already exist
"""
if not os.path.isfile(self.path):
return
with open(self.path, 'r') as cache_file:
cache_data = cache_file.read()
pattern = '%s=.*' % re.escape(variable)
replacement = '%s=%s' % (variable, value)
cache_data = re.sub(pattern, replacement, cache_data)
with open(self.path, 'w') as cache_file:
cache_file.write(cache_data)
def ninjafy(self):
self.alter('CMAKE_GENERATOR:INTERNAL', 'Ninja')
self.alter('CMAKE_MAKE_PROGRAM:FILEPATH', NINJA_PATH)
def makefy(self):
self.alter('CMAKE_GENERATOR:INTERNAL', 'Unix Makefiles')
self.alter('CMAKE_MAKE_PROGRAM:FILEPATH', '/usr/bin/make')
def ninjafy_argv(original):
"""Replace Unix Makefiles generator with Ninja"""
processed = []
next_g = False
for a in original:
if a == '-G':
next_g = True
elif next_g and 'Unix Makefiles' in a:
a = a.replace('Unix Makefiles', 'Ninja')
processed.append(a)
return processed
trace('Originally called:', sys.argv)
# Enable wrapping logic only when called inside clion private directory.
if not is_real_project():
sys.exit(call_cmake(sys.argv[1:]))
# Check if generator argument was specified
if '-G' in sys.argv:
# Generate Makefile artifacts required by CLion
cache = CMakeCache('CMakeCache.txt')
cache.makefy()
exit_code = call_cmake(sys.argv[1:])
if exit_code != 0:
sys.exit(exit_code)
# Generate Ninja artifacts for actual build
passing_args = ninjafy_argv(sys.argv[1:])
cache.ninjafy()
sys.exit(call_cmake(passing_args))
else:
sys.exit(call_cmake(sys.argv[1:]))
@long1eu
Copy link

long1eu commented Jul 6, 2019

Screenshot 2019-07-06 at 15 47 31
It it still asking me to select the CMakeLists.txt

@flaviut
Copy link

flaviut commented Aug 13, 2019

CLion defaults to some dumb -j arguments on my machine, so I've replaced the last line with

    new_args = sys.argv[1:]
    if '-j' in new_args:
        j_idx = new_args.index('-j')
        try:
            int(new_args[j_idx + 1])
            new_args = new_args[0:j_idx] + new_args[j_idx+2:-1]
        except ValueError:
            pass
    sys.exit(call_cmake(new_args))

this strips out the -j argument and uses the much better ninja default.

@Vxider
Copy link

Vxider commented Aug 19, 2019

Not work when build the ClickHouse project, will encounter the following exception

CMake Error: Error: generator : Unix Makefiles
Does not match the generator used previously: Ninja
Either remove the CMakeCache.txt file and CMakeFiles directory or choose a different binary directory.

@jsantos42
Copy link

jsantos42 commented Mar 18, 2022

The script does allow Clion to find Cmake, Makefile and CC. But I get an error saying it cannot find build.ninja. I have already changed the location of the script to be inside the root of the repo, and in the same directory of build.ninja, yet I cannot make it work. Any ideas?
ninja

@arichardson
Copy link
Author

@jsantos42 This script should no longer be necessary with recent CLion builds. You can just pass -G Ninja since 2019: https://blog.jetbrains.com/clion/2019/10/clion-2019-3-eap-ninja-cmake-generators/
Also in CLion 2021.3 there is a UI to select the CMake generator: https://www.jetbrains.com/clion/whatsnew/#scope-2021-3-cmake.

@jsantos42
Copy link

You're right, it did work! Thanks @arichardson

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment