YouCompleteMe and CMake
Tags: programming
libclang
: Walking an … »
I recently updated my vim
configuration to include the fine YouCompleteMe
plugin for C++ development. Since the software project I am mostly working on during my PhD project
uses CMake
, configuring YCM turned out to require some additional steps.
First, I added
SET( CMAKE_EXPORT_COMPILE_COMMANDS ON )
to the main CMakeLists.txt
file of our project. This ensures that CMake
creates the file
compile_commands.json
in the build directory of the project. Next, we need CMake
to provision
the file properly:
IF( EXISTS "${CMAKE_CURRENT_BINARY_DIR}/compile_commands.json" )
EXECUTE_PROCESS( COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_BINARY_DIR}/compile_commands.json
${CMAKE_CURRENT_SOURCE_DIR}/compile_commands.json
)
ENDIF()
This places the file in the source directory, permitting us to place a single configuration file
for YCM there. I have to add that I am not very happen with this solution yet. For one, this removes
the ability to have a debug build in some location and a release build somewhere else because the
build that is configured last gets to copy its compile commands—which of course tend to
differ with different configuration options. Another reason why this solution is not entirely smart
is that the copy is usually performed prior to CMake
updating the compile_commands.json
file.
If you change many compile options for different files, you will thus have to manually run the
cmake
binary twice.
Even with all these quirks, it’s still worth it—YCM is just so amazing to have. Being a previous user of the QtCreator IDE, I am blown away by the speed with which the plugin manages to perform its duties.
As a last step, we need to place a .ycm_extra_conf.py
configuration file in the project folder. By
default, YCM will use a pre-defined list of compilation flags to apply to all files. This is of
course insufficient for most larger projects, so we need to teach it to use the
compile_commands.json
instead.
This is easily achieved with the following configuration file:
import os
import ycm_core
from clang_helpers import PrepareClangFlags
def DirectoryOfThisScript():
return os.path.dirname(os.path.abspath(__file__))
# This is the single most important line in this script. Everything else is just nice to have but
# not strictly necessary.
compilation_database_folder = DirectoryOfThisScript()
# This provides a safe fall-back if no compilation commands are available. You could also add a
# includes relative to your project directory, for example.
flags = [
'-Wall',
'-std=c++11',
'-stdlib=libc++',
'-x',
'c++',
'-I',
'.',
'-isystem', '/usr/local/include',
'-isystem', '/usr/include',
'-I.',
]
if compilation_database_folder:
database = ycm_core.CompilationDatabase(compilation_database_folder)
else:
database = None
SOURCE_EXTENSIONS = [ '.cpp', '.cxx', '.cc', '.c', '.m', '.mm' ]
def MakeRelativePathsInFlagsAbsolute( flags, working_directory ):
if not working_directory:
return list( flags )
new_flags = []
make_next_absolute = False
path_flags = [ '-isystem', '-I', '-iquote', '--sysroot=' ]
for flag in flags:
new_flag = flag
if make_next_absolute:
make_next_absolute = False
if not flag.startswith( '/' ):
new_flag = os.path.join( working_directory, flag )
for path_flag in path_flags:
if flag == path_flag:
make_next_absolute = True
break
if flag.startswith( path_flag ):
path = flag[ len( path_flag ): ]
new_flag = path_flag + os.path.join( working_directory, path )
break
if new_flag:
new_flags.append( new_flag )
return new_flags
def IsHeaderFile( filename ):
extension = os.path.splitext( filename )[ 1 ]
return extension in [ '.h', '.hxx', '.hpp', '.hh' ]
def GetCompilationInfoForFile( filename ):
# The compilation_commands.json file generated by CMake does not have entries
# for header files. So we do our best by asking the db for flags for a
# corresponding source file, if any. If one exists, the flags for that file
# should be good enough.
if IsHeaderFile( filename ):
basename = os.path.splitext( filename )[ 0 ]
for extension in SOURCE_EXTENSIONS:
replacement_file = basename + extension
if os.path.exists( replacement_file ):
compilation_info = database.GetCompilationInfoForFile(
replacement_file )
if compilation_info.compiler_flags_:
return compilation_info
return None
return database.GetCompilationInfoForFile( filename )
def FlagsForFile( filename, **kwargs ):
if database:
# Bear in mind that compilation_info.compiler_flags_ does NOT return a
# python list, but a "list-like" StringVec object
compilation_info = GetCompilationInfoForFile( filename )
if not compilation_info:
return None
final_flags = MakeRelativePathsInFlagsAbsolute(
compilation_info.compiler_flags_,
compilation_info.compiler_working_dir_ )
else:
relative_to = DirectoryOfThisScript()
final_flags = MakeRelativePathsInFlagsAbsolute( flags, relative_to )
return {
'flags': final_flags,
'do_cache': True
}
The single most important line of this script is where you specify the compilation commands data
base, compilation_database_folder = DirectoryOfThisScript()
.
Apart from the that, the script is a modified version of the default YCM configuration
file, which the author
strongly suggests to modify.
A last word of advice: Depending on your YCM configuration, you may want to add let g:ycm_extra_conf_globlist = [ '/path/to/your/project/*' ]
to your .vimrc
. This ensures that YCM
will load the configuration file automatically and not let you confirm anything due to security
reasons.
Happy code completing!