Mercurial > vim-crosoft
diff scripts/ycm_extra_conf.py @ 10:f444739dd8af
Improvements to YCM dynamic flags.
- Fallback to a "companion" item (e.g. header/source) or a nearby item
when no flags are found for an item.
- Finding a "companion" is also exposed as a standalone script.
- Ability to pass extra clang flags, including some from a special
file found in the .vimcrosoft directory.
- Add support for PCH and other forced-include files.
- Add options for short/long args, or forcing forward slashes.
- Debugging/troubleshooting options, including dumping a batch file and
response file to run clang directly, and the ability to auto-load a
solution's last known environment when running in command line.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Thu, 24 Sep 2020 23:02:16 -0700 |
parents | 5d2c0db51914 |
children | 096e80c13781 |
line wrap: on
line diff
--- a/scripts/ycm_extra_conf.py Thu Sep 24 22:57:50 2020 -0700 +++ b/scripts/ycm_extra_conf.py Thu Sep 24 23:02:16 2020 -0700 @@ -1,6 +1,7 @@ import argparse import logging import os.path +import shutil import sys @@ -9,36 +10,88 @@ from logutil import setup_logging -from vsutil import SolutionCache +from vshelpers import load_vimcrosoft_auto_env, find_vimcrosoft_slncache, find_item_project +from vsutil import SolutionCache, ITEM_TYPE_CPP_SRC, ITEM_TYPE_CPP_HDR logger = logging.getLogger(__name__) -def _build_cflags(filename, solution, buildenv=None, slncache=None): - # Load the solution. - if not solution: - raise Exception( - "No solution path was provided in the client data!") +def _split_paths_property(val): + if val: + return val.strip(';').split(';') + return [] + - cache, loaded = SolutionCache.load_or_rebuild(solution, slncache) - if not loaded: - cache.build_cache() +def _split_paths_property_and_make_absolute(basedir, val): + return [os.path.abspath(os.path.join(basedir, p)) + for p in _split_paths_property(val)] - # Find the current file in the solution. + +def _get_item_specific_flags(projdir, clcompileitems, filename): + logger.debug("Looking through %d items to find: %s" % (len(clcompileitems), filename)) filename_lower = filename.lower() - projpath = None - for pp, pi in cache.index.items(): - if filename_lower in pi: - projpath = pp - break - else: - raise Exception("File doesn't belong to the solution: %s" % filename) + for item in clcompileitems: + absiteminclude = os.path.normpath(os.path.join(projdir, item.include)) + if absiteminclude.lower() != filename_lower: + continue + logger.debug("Found file-specific flags for: %s" % filename) + incpaths = _split_paths_property_and_make_absolute( + projdir, item.metadata.get('AdditionalIncludeDirectories')) + incfiles = _split_paths_property_and_make_absolute( + projdir, item.metadata.get('ForcedIncludeFiles')) + return (incpaths, incfiles) + return ([], []) + + +def _find_any_possible_item_specific_flags( + solution, slncache, projdir, clcompileitems, filename, incpaths, incfiles, *, + search_neighbours=True): + # First, find any actual flags for this item. + item_incpaths, item_incfiles = _get_item_specific_flags(projdir, clcompileitems, filename) + if item_incpaths or item_incfiles: + incpaths += item_incpaths + incfiles += item_incfiles + return True - # Find the project that our file belongs to. - proj = cache.slnobj.find_project_by_path(projpath) - if not proj: - raise Exception("Can't find project in solution: %s" % projpath) + logger.debug("Requested item didn't have any flags, looking for companion item") + from find_companion import _find_companion_item + companion_item = _find_companion_item(solution, filename, slncache=slncache) + if companion_item: + logger.debug("Found companion item: %s" % companion_item) + item_incpaths, item_incfiles = _get_item_specific_flags(projdir, clcompileitems, companion_item) + incpaths += item_incpaths + incfiles += item_incfiles + return True + + #logger.debug("No companion item found, see if we can find flags for a neighbour") + #os.path.dirname(filename) + return False + + +def _expand_extra_flags_with_solution_extra_flags(solution, extraflags): + argfilename = os.path.join( + os.path.dirname(solution), + '.vimcrosoft', + (os.path.basename(solution) + '.flags')) + try: + with open(argfilename, 'r', encoding='utf8') as fp: + lines = fp.readlines() + logger.debug("Read extra flags from: %s (%d lines)" % (argfilename, len(lines))) + except OSError: + return extraflags + + extraflags = extraflags or [] + for line in lines: + if not line.startswith('#'): + extraflags.append(line.strip()) + return extraflags + + +def _build_cflags(filename, solution, buildenv=None, slncache=None, extraflags=None, + force_fwd_slashes=True, short_flags=True): + # Find the current file in the solution. + cache, proj = find_item_project(filename, solution, slncache) logger.debug("Found project %s: %s" % (proj.name, proj.abspath)) # Get the provided config/platform combo, which represent a solution @@ -73,6 +126,7 @@ # Let's prepare a list of standard stuff for C++. preproc = [] incpaths = [] + incfiles = [] projdir = os.path.dirname(proj.abspath) if cfgtype == 'Makefile': @@ -83,11 +137,28 @@ defaultpropgroup = proj.defaultpropertygroup(proj_buildenv) nmake_preproc = defaultpropgroup.get('NMakePreprocessorDefinitions') - preproc += nmake_preproc.strip(';').split(';') + preproc += _split_paths_property(nmake_preproc) + + vs_incpaths = defaultpropgroup.get('IncludePath') + if vs_incpaths: + incpaths += _split_paths_property_and_make_absolute( + projdir, vs_incpaths) nmake_incpaths = defaultpropgroup.get('NMakeIncludeSearchPath') - incpaths += [os.path.abspath(os.path.join(projdir, p)) - for p in nmake_incpaths.strip(';').split(';')] + if nmake_incpaths: + incpaths += _split_paths_property_and_make_absolute( + projdir, nmake_incpaths) + + nmake_forcedincs = defaultpropgroup.get('NMakeForcedIncludes') + if nmake_forcedincs: + incfiles += _split_paths_property_and_make_absolute( + projdir, nmake_forcedincs) + + # Find stuff specific to the file we are working on. + defaultitemgroup = proj.defaultitemgroup(proj_buildenv) + clcompileitems = list(defaultitemgroup.get_items_of_types([ITEM_TYPE_CPP_SRC, ITEM_TYPE_CPP_HDR])) + _find_any_possible_item_specific_flags( + solution, slncache, projdir, clcompileitems, filename, incpaths, incfiles) else: # We should definitely support standard VC++ projects here but @@ -95,21 +166,66 @@ raise Exception("Don't know how to handle configuration type: %s" % cfgtype) + # We need to duplicate all the forced-included files because they could + # have a VS-generated PCH file next to them. Clang then tries to pick it + # up and complains that it doesn't use a valid format... :( + incfiles = _cache_pch_files(incfiles) + # Build the clang/YCM flags with what we found. flags = ['-x', 'c++'] # TODO: check language type from project file. for symbol in preproc: flags.append('-D%s' % symbol) for path in incpaths: + flagname = '-I' if path.startswith("C:\\Program Files"): - flags.append('-isystem') + flagname = '-isystem' + flagval = path.replace('\\', '/') if force_fwd_slashes else path + if short_flags: + flags.append('%s%s' % (flagname, flagval)) else: - flags.append('-I') - flags.append(path) + flags.append(flagname) + flags.append(flagval) + # For some reason it seems VS applies those in last-to-first order. + incfiles = list(reversed(incfiles)) + for path in incfiles: + if force_fwd_slashes: + flags.append('--include=%s' % path.replace('\\', '/')) + else: + flags.append('--include=%s' % path) + + if extraflags: + flags += extraflags return {'flags': flags} +_clang_shadow_pch_suffix = '-for-clang' + + +def _cache_pch_files(paths): + outpaths = [] + for path in paths: + name, ext = os.path.splitext(path) + outpath = "%s%s%s" % (name, _clang_shadow_pch_suffix, ext) + + do_cache = False + orig_mtime = os.path.getmtime(path) + try: + out_mtime = os.path.getmtime(outpath) + if orig_mtime >= out_mtime: + do_cache = True + except OSError: + do_cache = True + + if do_cache: + logger.debug("Creating shadow PCH file: %s" % path) + shutil.copy2(path, outpath) + + outpaths.append(outpath) + return outpaths + + def _build_env_from_vim(client_data): buildenv = {} buildenv['Configuration'] = client_data.get('g:vimcrosoft_current_config', '') @@ -117,6 +233,35 @@ return buildenv +_dump_debug_file = True + + +def _do_dump_debug_file(args, flags, debug_filename): + import pprint + + with open(debug_filename, 'w') as fp: + fp.write(" args: \n") + fp.write("=========\n") + pp = pprint.PrettyPrinter(indent=2, stream=fp) + pp.pprint(args) + fp.write("\n\n") + + fp.write(" flags: \n") + fp.write("==========\n") + flags = flags.get('flags') + if flags: + fp.write(flags[0]) + for flag in flags[1:]: + if flag[0] == '-': + fp.write("\n") + else: + fp.write(" ") + fp.write(flag) + else: + fp.write("<no flags found>\n") + fp.write("\n\n") + + def Settings(**kwargs): language = kwargs.get('language') filename = kwargs.get('filename') @@ -127,17 +272,24 @@ solution = client_data.get('solution') slncache = client_data.get('slncache') buildenv = client_data.get('env', {}) + extraflags = client_data.get('extra_flags') else: + if not client_data: + raise Exception("No client data provided by Vim host!") solution = client_data.get('g:vimcrosoft_current_sln') slncache = client_data.get('g:vimcrosoft_current_sln_cache') buildenv = _build_env_from_vim(client_data) + extraflags = client_data.get('g:vimcrosoft_extra_clang_args') + + extraflags = _expand_extra_flags_with_solution_extra_flags(solution, extraflags) flags = None if language == 'cfamily': try: flags = _build_cflags(filename, solution, - buildenv=buildenv, slncache=slncache) + buildenv=buildenv, slncache=slncache, + extraflags=extraflags) except Exception as exc: if from_cli: raise @@ -145,13 +297,11 @@ else: flags = {'error': f"Unknown language: {language}"} - with open("D:\\P4\\DevEditor\\debug.txt", 'w') as fp: - fp.write("kwargs:") - fp.write(str(kwargs)) - fp.write("client_data:") - fp.write(str(list(kwargs['client_data'].items()))) - fp.write("flags:") - fp.write(str(flags)) + if _dump_debug_file: + debug_filename = os.path.join( + os.path.dirname(solution), '.vimcrosoft', 'debug_flags.txt') + _do_dump_debug_file(kwargs, flags, debug_filename) + return flags @@ -175,11 +325,17 @@ help="The solution file") parser.add_argument('filename', help="The filename for which to get flags") + parser.add_argument('--no-auto-env', + action='store_true', + help="Don't read configuration information from Vimcrosoft cache") parser.add_argument('-p', '--property', action="append", help="Specifies a build property") parser.add_argument('-c', '--cache', help="The solution cache to use") + parser.add_argument('--cmdline', + action='store_true', + help="Output flags in a command-line form") parser.add_argument('-v', '--verbose', action='store_true', help="Show debugging information") @@ -190,13 +346,18 @@ logger.debug(f"Got language {lang} for {args.filename}") build_env = {} + slncache = args.cache + if not args.no_auto_env: + load_vimcrosoft_auto_env(args.solution, build_env) + if not slncache: + slncache = find_vimcrosoft_slncache(args.solution) if args.property: for p in args.property: pname, pval = p.split('=', 1) build_env[pname] = pval logger.debug(f"Got build environment: {build_env}") client_data = {'solution': args.solution, - 'slncache': args.cache, + 'slncache': slncache, 'env': build_env} params = {'from_cli': True, @@ -205,10 +366,35 @@ 'client_data': client_data } flags = Settings(**params) - logger.info("Flags:") - import pprint - pp = pprint.PrettyPrinter(indent=2) - pp.pprint(flags) + if args.cmdline: + import shlex + if hasattr(shlex, 'join'): + joinargs = shlex.join + else: + joinargs = lambda a: ' '.join(a) + + with open('clang_ycm_args.rsp', 'w', encoding='utf8') as fp: + fp.write("%s -fsyntax-only \"%s\"" % ( + joinargs(sanitizeargs(flags['flags'])), + args.filename.replace('\\', '/') + )) + with open('clang_ycm_invoke.cmd', 'w', encoding='utf8') as fp: + fp.write("\"c:\\Program Files\\LLVM\\bin\\clang++.exe\" @clang_ycm_args.rsp > clang_ycm_invoke.log 2>&1") + + logger.info("Command line written to: clang_ycm_invoke.cmd") + else: + logger.info("Flags:") + import pprint + pp = pprint.PrettyPrinter(indent=2) + pp.pprint(flags) + + +def sanitizeargs(args): + for arg in args: + if ' ' in arg: + yield '"%s"' % arg + else: + yield arg if __name__ == '__main__':