blob: 385e967b804b8145597da4917d7aaf8a2b83c9d8 [file] [log] [blame]
#!/usr/bin/python
# Copyright 2017 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
# This .ycm_extra_conf will be picked up automatically for code completion using
# YouCompleteMe.
#
# See https://valloric.github.io/YouCompleteMe/ for instructions on setting up
# YouCompleteMe using Vim. This .ycm_extra_conf file also works with any other
# completion engine that uses YCMD (https://github.com/Valloric/ycmd).
#
# Code completion depends on a Clang compilation database. This is placed in a
# file named `compile_commands.json` in your execution root path. I.e. it will
# be at the path returned by `bazel info execution_root`.
#
# If the compilation database isn't available, this script will generate one
# using tools/cpp/generate_compilation_database.sh. This process can be slow if
# you haven't built the sources yet. It's always a good idea to run
# generate_compilation_database.sh manually so that you can see the build output
# including any errors encountered during compile command generation.
# ==============================================================================
import json
import os
import shlex
import subprocess
import time
# If all else fails, then return this list of flags.
DEFAULT_FLAGS = []
CANONICAL_SOURCE_FILE = 'kythe/cxx/extractor/cxx_extractor_main.cc'
# Full path to directory containing compilation database. This is usually
# |execution_root|/compile_commands.json.
COMPILATION_DATABASE_PATH = None
# Workspace path.
WORKSPACE_PATH = None
# The compilation database. This is a mapping from the absolute normalized path
# of the source file to it's compile command broken down into an array.
COMPILATION_DATABASE = {}
# If loading the compilation database failed for some reason,
# LAST_INIT_FAILURE_TIME contains the value of time.clock() at the time the
# failure was encountered.
LAST_INIT_FAILURE_TIME = None
# If this many seconds have passed since the last failure, then try to generate
# the compilation database again.
RETRY_TIMEOUT_SECONDS = 120
HEADER_EXTENSIONS = ['.h', '.hpp', '.hh', '.hxx']
SOURCE_EXTENSIONS = ['.cc', '.cpp', '.c', '.m', '.mm', '.cxx']
NORMALIZE_PATH = 1
REMOVE = 2
# List of clang options and what to do with them. Use the '-foo' form for flags
# that could be used as '-foo <arg>' and '-foo=<arg>' forms, and use '-foo=' for
# flags that can only be used as '-foo=<arg>'.
#
# Mapping a flag to NORMALIZE_PATH causes its argument to be normalized against
# the build directory via ExpandAndNormalizePath(). REMOVE causes both the flag
# and its value to be removed.
CLANG_OPTION_DISPOSITION = {
'-I': NORMALIZE_PATH,
'-MF': REMOVE,
'-cxx-isystem': NORMALIZE_PATH,
'-dependency-dot': REMOVE,
'-dependency-file': REMOVE,
'-fbuild-session-file': REMOVE,
'-fmodule-file': NORMALIZE_PATH,
'-fmodule-map-file': NORMALIZE_PATH,
'-foptimization-record-file': REMOVE,
'-fprebuilt-module-path': NORMALIZE_PATH,
'-fprofile-generate=': REMOVE,
'-fprofile-instrument-generate=': REMOVE,
'-fprofile-user=': REMOVE,
'-gcc-tollchain=': NORMALIZE_PATH,
'-idirafter': NORMALIZE_PATH,
'-iframework': NORMALIZE_PATH,
'-imacros': NORMALIZE_PATH,
'-include': NORMALIZE_PATH,
'-include-pch': NORMALIZE_PATH,
'-iprefix': NORMALIZE_PATH,
'-iquote': NORMALIZE_PATH,
'-isysroot': NORMALIZE_PATH,
'-isystem': NORMALIZE_PATH,
'-isystem-after': NORMALIZE_PATH,
'-ivfsoverlay': NORMALIZE_PATH,
'-iwithprefixbefore': NORMALIZE_PATH,
'-iwithsysroot': NORMALIZE_PATH,
'-o': REMOVE,
'-working-directory': NORMALIZE_PATH,
}
def ProcessOutput(args):
"""Run the program described by |args| and return its stdout as a stream.
|stderr| and |stdin| will be set to /dev/null. Will raise CalledProcessError
if the subprocess doesn't complete successfully.
"""
output = ''
with open(os.devnull, 'w') as err:
with open(os.devnull, 'r') as inp:
output = subprocess.check_output(args, stderr=err, stdin=inp)
return str(output).strip()
def InitBazelConfig():
"""Initialize globals based on Bazel configuration.
Initialize COMPILATION_DATABASE_PATH, WORKSPACE_PATH, and
CANONICAL_SOURCE_FILE based on Bazel. These values are not expected to change
during the session."""
global COMPILATION_DATABASE_PATH
global WORKSPACE_PATH
global CANONICAL_SOURCE_FILE
execution_root = ProcessOutput(['bazel', 'info', 'execution_root'])
COMPILATION_DATABASE_PATH = os.path.join(execution_root,
'compile_commands.json')
WORKSPACE_PATH = ProcessOutput(['bazel', 'info', 'workspace'])
CANONICAL_SOURCE_FILE = ExpandAndNormalizePath(CANONICAL_SOURCE_FILE,
WORKSPACE_PATH)
def GenerateCompilationDatabaseSlowly():
"""Generate compilation database. May take a while."""
script_path = os.path.join(WORKSPACE_PATH, 'tools', 'cpp',
'generate_compilation_database.sh')
ProcessOutput(script_path)
def ExpandAndNormalizePath(filename, basepath=WORKSPACE_PATH):
"""Resolves |filename| relative to |basepath| and expands symlinks."""
if not os.path.isabs(filename) and basepath:
filename = os.path.join(basepath, filename)
filename = os.path.realpath(filename)
return str(filename)
def PrepareCompileFlags(compile_command, basepath):
flags = shlex.split(compile_command)
flags_to_return = []
use_next_flag_as_value_for = None
def HandleFlag(name, value, combine):
disposition = CLANG_OPTION_DISPOSITION.get(name, None)
if disposition is None and combine:
disposition = CLANG_OPTION_DISPOSITION.get(name + '=', None)
if disposition == REMOVE:
return
if disposition == NORMALIZE_PATH:
value = ExpandAndNormalizePath(value, basepath)
if combine:
flags_to_return.append('{}={}'.format(name, value))
else:
flags_to_return.extend([name, value])
for flag in flags:
if use_next_flag_as_value_for is not None:
name = use_next_flag_as_value_for
use_next_flag_as_value_for = None
HandleFlag(name, flag, combine=False)
continue
if '=' in flag: # -foo=bar
name, value = flag.split('=', 1)
HandleFlag(name, value, combine=True)
continue
if flag in CLANG_OPTION_DISPOSITION:
use_next_flag_as_value_for = flag
continue
if flag.startswith('-I'):
HandleFlag('-I', flags[3:], combine=False)
continue
flags_to_return.append(flag)
return flags_to_return
def LoadCompilationDatabase():
if not os.path.exists(COMPILATION_DATABASE_PATH):
GenerateCompilationDatabaseSlowly()
with open(COMPILATION_DATABASE_PATH, 'r') as database:
database_dict = json.load(database)
global COMPILATION_DATABASE
COMPILATION_DATABASE = {}
for entry in database_dict:
filename = ExpandAndNormalizePath(entry['file'], WORKSPACE_PATH)
directory = entry['directory']
command = entry['command']
COMPILATION_DATABASE[filename] = {
'command': command,
'directory': directory
}
def IsHeaderFile(filename):
extension = os.path.splitext(filename)[1]
return extension in HEADER_EXTENSIONS
def FindAlternateFile(filename):
if IsHeaderFile(filename):
basename = os.path.splitext(filename)[0]
for extension in SOURCE_EXTENSIONS:
new_filename = basename + extension
if new_filename in COMPILATION_DATABASE:
return new_filename
# Try something in the same directory.
directory = os.path.dirname(filename)
for key in COMPILATION_DATABASE.iterkeys():
if key.startswith(directory) and os.path.dirname(key) == directory:
return key
return CANONICAL_SOURCE_FILE
# Entrypoint for YouCompleteMe.
def FlagsForFile(filename, **kwargs):
global LAST_INIT_FAILURE_TIME
if len(COMPILATION_DATABASE) == 0:
if LAST_INIT_FAILURE_TIME is not None and time.clock(
) - LAST_INIT_FAILURE_TIME < RETRY_TIMEOUT_SECONDS:
return {'flags': DEFAULT_FLAGS}
try:
InitBazelConfig()
LoadCompilationDatabase()
except Exception as e:
LAST_INIT_FAILURE_TIME = time.clock()
return {'flags': DEFAULT_FLAGS}
filename = str(os.path.realpath(filename))
if filename not in COMPILATION_DATABASE:
filename = FindAlternateFile(filename)
if filename not in COMPILATION_DATABASE:
return {'flags': DEFAULT_FLAGS}
result_dict = COMPILATION_DATABASE[filename]
return {
'flags':
PrepareCompileFlags(result_dict['command'], result_dict['directory'])
}
# For testing.
if __name__ == '__main__':
import sys
print FlagsForFile(sys.argv[1])