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__':