304 lines
12 KiB
Python
304 lines
12 KiB
Python
# global_conf.py --- ycmd global configuration file for Spacemacs
|
|
#
|
|
# Copyright (c) 2012-2021 Sylvain Benner & Contributors
|
|
#
|
|
# Author: Thomas de Beauchene <thomas.de.beauchene@gmail.com>
|
|
# URL: https://github.com/syl20bnr/spacemacs
|
|
#
|
|
# This file is not part of GNU Emacs.
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
# This script tries to get the compilation flags for a translation unit using
|
|
# the following logic:
|
|
#
|
|
# 1) If there is a compile_commands.json in a parent directory:
|
|
# a) If the file is a header file:
|
|
# - search for the header file itself in db
|
|
# - search for a sibling source file in the same directory (i.e. a source
|
|
# file with the same name but different extension)
|
|
# - search for a source file that includes our header's path
|
|
# - search for the nearest source file in db
|
|
#
|
|
# b) If the file is a source file:
|
|
# - search for the source file itself
|
|
# - search for the nearest source file in db
|
|
#
|
|
# 2) If no compile_commands.json, search for a .clang_complete:
|
|
# - get flags from .clang_complete
|
|
#
|
|
# 3) Always try to add extra flags from a .ycm_extra_flags file in a parent
|
|
# directory. (like --sysroot="/path/to/your/toolchain/libc" if you are cross-compiling)
|
|
#
|
|
# Thanks to Jonas Devlieghere and Gabor Marton for their work on which this code is based.
|
|
# https://jonasdevlieghere.com/a-better-youcompleteme-config/
|
|
# https://github.com/martong/ycm_extra_conf.jsondb
|
|
|
|
import itertools
|
|
import json
|
|
import logging
|
|
import os
|
|
import os.path
|
|
import re
|
|
import ycm_core
|
|
|
|
# From pyhon 3 on, zip is already a generator and there is no izip in
|
|
# itertools.
|
|
try:
|
|
from itertools import izip as zip
|
|
except ImportError:
|
|
pass
|
|
|
|
SOURCE_EXTENSIONS = ['.cpp', '.cxx', '.cc', '.c', '.m', '.mm',
|
|
'.CPP', '.CXX', '.CC', '.C', '.M', '.MM']
|
|
HEADER_EXTENSIONS = ['.h', '.hxx', '.hpp', '.hh'
|
|
'.H', '.HXX', '.HPP', '.HH']
|
|
SUBDIRS = ['build']
|
|
|
|
# This function is called by ycmd.
|
|
def FlagsForFile(filename):
|
|
logging.info("%s: Getting flags for %s" % (__file__, filename))
|
|
root = os.path.realpath(filename)
|
|
flags = FlagsFromCompilationDatabase(root, filename)
|
|
if not flags:
|
|
flags = FlagsFromClangComplete(root, filename)
|
|
extra_flags = GetUserExtraFlags(filename)
|
|
if extra_flags:
|
|
if flags:
|
|
flags += extra_flags
|
|
else:
|
|
flags = extra_flags
|
|
if flags:
|
|
flags = [ flag for flag in flags if not flag.startswith("-m") ] # strip -m flags
|
|
logging.info("%s: Flags = [\n\t\t%s\n]"
|
|
% (os.path.basename(filename), "\n\t\t".join(flags)))
|
|
else:
|
|
flags = []
|
|
logging.error("%s: No flags were found !" % (os.path.basename(filename)))
|
|
return { 'flags': flags, 'do_cache': True }
|
|
|
|
def FlagsFromClangComplete(root, filename):
|
|
try:
|
|
clang_complete_path = FindNearest(root, '.clang_complete', filename)
|
|
clang_complete_flags = open(clang_complete_path, 'r').read().splitlines()
|
|
logging.info("%s: Using %s" % (os.path.basename(filename), clang_complete_path))
|
|
return MakeRelativePathsInFlagsAbsolute(clang_complete_flags,
|
|
os.path.dirname(clang_complete_path))
|
|
except:
|
|
return None
|
|
|
|
def FlagsFromCompilationDatabase(root, filename):
|
|
try:
|
|
database_path = FindNearest(root, 'compile_commands.json', filename, subdirs=SUBDIRS)
|
|
database = ycm_core.CompilationDatabase(os.path.dirname(database_path))
|
|
if not database:
|
|
logging.info("%s: Compilation database file found but unable to load"
|
|
% os.path.basename(filename))
|
|
return None
|
|
extension = os.path.splitext(filename)[1]
|
|
if extension in HEADER_EXTENSIONS:
|
|
flags = GetFlagsForHeader(database_path, database, filename)
|
|
else:
|
|
flags = GetFlagsForSourceFile(database_path, database, filename)
|
|
if not flags:
|
|
logging.info("%s: No compilation info for %s in compilation database"
|
|
% (os.path.basename(filename), filename))
|
|
return None
|
|
return MakeRelativePathsInFlagsAbsolute(flags.compiler_flags_,
|
|
flags.compiler_working_dir_)
|
|
except Exception as e:
|
|
logging.info("%s: Could not get compilation flags from db: %s"
|
|
% (os.path.basename(filename), e))
|
|
return None
|
|
|
|
def GetFlagsForHeader(database_path, database, filename):
|
|
flags = FindFileInDb(database, filename)
|
|
if flags:
|
|
return flags
|
|
flags = FindSiblingFileForHeader(database, filename)
|
|
if flags:
|
|
return flags
|
|
flags = SearchForTranslationUnitWhichIncludesPath(database_path,
|
|
database,
|
|
os.path.dirname(filename),
|
|
filename)
|
|
if flags:
|
|
return flags
|
|
return FindNearestSourceFileInDb(database_path, database, filename)
|
|
|
|
def GetFlagsForSourceFile(database_path, database, filename):
|
|
flags = FindFileInDb(database, filename)
|
|
if flags:
|
|
return flags
|
|
return FindNearestSourceFileInDb(database_path, database, filename)
|
|
|
|
def FindNearest(path, target, filename, subdirs=[]):
|
|
candidates = [os.path.join(path, target)]
|
|
for subdir in subdirs:
|
|
candidates.append(os.path.join(path, subdir, target))
|
|
for candidate in candidates:
|
|
if(os.path.isfile(candidate) or os.path.isdir(candidate)):
|
|
logging.info("%s: Found nearest %s at %s"
|
|
% (os.path.basename(filename), target, candidate))
|
|
return candidate
|
|
parent = os.path.dirname(os.path.abspath(path))
|
|
if(parent == path):
|
|
raise RuntimeError("could not find %s" % target)
|
|
return FindNearest(parent, target, filename, subdirs)
|
|
|
|
def FindFileInDb(database, filename):
|
|
logging.info("%s: Trying to find file in database..."
|
|
% (os.path.basename(filename)))
|
|
flags = database.GetCompilationInfoForFile(filename)
|
|
if flags.compiler_flags_:
|
|
logging.info("%s: Found file in database."
|
|
% (os.path.basename(filename)))
|
|
return flags
|
|
logging.info("%s: File not found in compilation db."
|
|
% (os.path.basename(filename)))
|
|
return None
|
|
|
|
def FindSiblingFileForHeader(database, filename):
|
|
logging.info("%s: Trying to find a sibling source file for that header in database..."
|
|
% (os.path.basename(filename)))
|
|
basename = os.path.splitext(filename)[0]
|
|
for extension in SOURCE_EXTENSIONS:
|
|
replacement_file = basename + extension
|
|
if os.path.exists(replacement_file):
|
|
flags = database.GetCompilationInfoForFile(replacement_file)
|
|
if flags.compiler_flags_:
|
|
logging.info("%s: Found sibling source file: %s"
|
|
% (os.path.basename(filename), replacement_file))
|
|
return flags
|
|
logging.info("%s: Did not find sibling source file."
|
|
% (os.path.basename(filename)))
|
|
return None
|
|
|
|
def FindNearestSourceFileInDb(database_path, database, srcfile):
|
|
logging.info("%s: Trying to find nearest source file in database..."
|
|
% (srcfile))
|
|
filename, flags = DoFindNearestSourceFileInDb(database_path, database, srcfile, None)
|
|
if flags:
|
|
logging.info("%s: Found nearest source file from %s: %s"
|
|
% (os.path.basename(srcfile), srcfile, filename))
|
|
return flags
|
|
logging.info("%s: Could not find nearest source file from %s in compilation db."
|
|
% (srcfile, srcfile))
|
|
return None
|
|
|
|
# Search subdirectories recursively, then do the same recursively for parent
|
|
# directories until a file was found or we have searched the database's directory
|
|
def DoFindNearestSourceFileInDb(database_path, database, directory, skip):
|
|
for root, dirnames, filenames in os.walk(directory):
|
|
if os.path.basename(skip) in dirnames:
|
|
dirnames.remove(os.path.basename(skip))
|
|
for filename in filenames:
|
|
if filename.endswith(tuple(SOURCE_EXTENSIONS)):
|
|
flags = database.GetCompilationInfoForFile(os.path.join(root, filename))
|
|
if flags.compiler_flags_:
|
|
return os.path.join(root, filename), flags
|
|
if database_path == directory or os.path.dirname(directory) == directory:
|
|
return None, None
|
|
return DoFindNearestSourceFileInDb(database_path, database, os.path.dirname(directory), directory)
|
|
|
|
def Pairwise(iterable):
|
|
"s -> (s0,s1), (s1,s2), (s2, s3), ..."
|
|
a, b = itertools.tee(iterable)
|
|
next(b, None)
|
|
return zip(a, b)
|
|
|
|
def RemoveClosingSlash(path):
|
|
if path.endswith('/'):
|
|
path = path[:-1]
|
|
return path
|
|
|
|
def SearchForTranslationUnitWhichIncludesPath(database_path, database, path, filename):
|
|
logging.info("%s: Trying to find a translation unit that includes our header's path..."
|
|
% (os.path.basename(filename)))
|
|
with open(database_path, 'r') as f:
|
|
jsonDb = json.load(f)
|
|
path = RemoveClosingSlash(os.path.abspath(path))
|
|
found = []
|
|
for translationUnit in jsonDb:
|
|
buildDir = translationUnit["directory"]
|
|
switches = translationUnit["command"].split()
|
|
for currentSwitch, nextSwitch in Pairwise(switches):
|
|
matchObj = re.match(r'(-I|-isystem)(.*)', currentSwitch)
|
|
includeDir = ""
|
|
isIncFlag = False
|
|
if currentSwitch == "-I" or currentSwitch == "-isystem":
|
|
includeDir = nextSwitch
|
|
isIncFlag = True
|
|
elif matchObj:
|
|
includeDir = matchObj.group(2)
|
|
isIncFlag = True
|
|
if not isIncFlag:
|
|
continue
|
|
includeDir = RemoveClosingSlash(os.path.abspath(os.path.join(buildDir, includeDir)))
|
|
# Check all the parent dirs in path
|
|
pathCopy = path
|
|
distance = 0
|
|
while pathCopy != os.path.abspath(os.sep):
|
|
if includeDir == pathCopy:
|
|
found.append((distance, str(translationUnit["file"])))
|
|
distance += 1
|
|
pathCopy, tail = os.path.split(pathCopy)
|
|
found.sort()
|
|
if len(found) == 0:
|
|
logging.info("%s: Did not find translation unit which includes path %s"
|
|
% (os.path.basename(filename), path))
|
|
return None
|
|
else:
|
|
result = found[0][1]
|
|
logging.info("%s: Found best source file which includes path: %s"
|
|
% (os.path.basename(filename), result))
|
|
return database.GetCompilationInfoForFile(result)
|
|
|
|
def GetUserExtraFlags(filename):
|
|
try:
|
|
extra_flags_file = FindNearest(os.path.dirname(filename), ".ycm_extra_flags", filename)
|
|
except:
|
|
logging.info("%s: No extra flags."
|
|
% (os.path.basename(filename)))
|
|
return None
|
|
with open(extra_flags_file, 'r') as f:
|
|
lines = f.readlines()
|
|
lines = [ line[0:line.find("#")].split() for line in lines ]
|
|
lines = list(itertools.chain.from_iterable(lines))
|
|
logging.info("%s: Extra flags = [\n\t\t%s\n]"
|
|
% (os.path.basename(filename), "\n\t\t".join(lines)))
|
|
return lines
|
|
|
|
def MakeRelativePathsInFlagsAbsolute(flags, working_directory):
|
|
if not working_directory:
|
|
return list(flags)
|
|
new_flags = []
|
|
make_next_absolute = False
|
|
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 [ '-isystem', '-I', '-iquote', '--sysroot=' ]:
|
|
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
|