comparison 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
comparison
equal deleted inserted replaced
9:4ba6df1b2f97 10:f444739dd8af
1 import argparse 1 import argparse
2 import logging 2 import logging
3 import os.path 3 import os.path
4 import shutil
4 import sys 5 import sys
5 6
6 7
7 if True: # 'vim' in sys.modules: 8 if True: # 'vim' in sys.modules:
8 sys.path.append(os.path.dirname(__file__)) 9 sys.path.append(os.path.dirname(__file__))
9 10
10 11
11 from logutil import setup_logging 12 from logutil import setup_logging
12 from vsutil import SolutionCache 13 from vshelpers import load_vimcrosoft_auto_env, find_vimcrosoft_slncache, find_item_project
14 from vsutil import SolutionCache, ITEM_TYPE_CPP_SRC, ITEM_TYPE_CPP_HDR
13 15
14 16
15 logger = logging.getLogger(__name__) 17 logger = logging.getLogger(__name__)
16 18
17 19
18 def _build_cflags(filename, solution, buildenv=None, slncache=None): 20 def _split_paths_property(val):
19 # Load the solution. 21 if val:
20 if not solution: 22 return val.strip(';').split(';')
21 raise Exception( 23 return []
22 "No solution path was provided in the client data!") 24
23 25
24 cache, loaded = SolutionCache.load_or_rebuild(solution, slncache) 26 def _split_paths_property_and_make_absolute(basedir, val):
25 if not loaded: 27 return [os.path.abspath(os.path.join(basedir, p))
26 cache.build_cache() 28 for p in _split_paths_property(val)]
27 29
30
31 def _get_item_specific_flags(projdir, clcompileitems, filename):
32 logger.debug("Looking through %d items to find: %s" % (len(clcompileitems), filename))
33 filename_lower = filename.lower()
34 for item in clcompileitems:
35 absiteminclude = os.path.normpath(os.path.join(projdir, item.include))
36 if absiteminclude.lower() != filename_lower:
37 continue
38 logger.debug("Found file-specific flags for: %s" % filename)
39 incpaths = _split_paths_property_and_make_absolute(
40 projdir, item.metadata.get('AdditionalIncludeDirectories'))
41 incfiles = _split_paths_property_and_make_absolute(
42 projdir, item.metadata.get('ForcedIncludeFiles'))
43 return (incpaths, incfiles)
44 return ([], [])
45
46
47 def _find_any_possible_item_specific_flags(
48 solution, slncache, projdir, clcompileitems, filename, incpaths, incfiles, *,
49 search_neighbours=True):
50 # First, find any actual flags for this item.
51 item_incpaths, item_incfiles = _get_item_specific_flags(projdir, clcompileitems, filename)
52 if item_incpaths or item_incfiles:
53 incpaths += item_incpaths
54 incfiles += item_incfiles
55 return True
56
57 logger.debug("Requested item didn't have any flags, looking for companion item")
58 from find_companion import _find_companion_item
59 companion_item = _find_companion_item(solution, filename, slncache=slncache)
60 if companion_item:
61 logger.debug("Found companion item: %s" % companion_item)
62 item_incpaths, item_incfiles = _get_item_specific_flags(projdir, clcompileitems, companion_item)
63 incpaths += item_incpaths
64 incfiles += item_incfiles
65 return True
66
67 #logger.debug("No companion item found, see if we can find flags for a neighbour")
68 #os.path.dirname(filename)
69 return False
70
71
72 def _expand_extra_flags_with_solution_extra_flags(solution, extraflags):
73 argfilename = os.path.join(
74 os.path.dirname(solution),
75 '.vimcrosoft',
76 (os.path.basename(solution) + '.flags'))
77 try:
78 with open(argfilename, 'r', encoding='utf8') as fp:
79 lines = fp.readlines()
80 logger.debug("Read extra flags from: %s (%d lines)" % (argfilename, len(lines)))
81 except OSError:
82 return extraflags
83
84 extraflags = extraflags or []
85 for line in lines:
86 if not line.startswith('#'):
87 extraflags.append(line.strip())
88 return extraflags
89
90
91 def _build_cflags(filename, solution, buildenv=None, slncache=None, extraflags=None,
92 force_fwd_slashes=True, short_flags=True):
28 # Find the current file in the solution. 93 # Find the current file in the solution.
29 filename_lower = filename.lower() 94 cache, proj = find_item_project(filename, solution, slncache)
30 projpath = None
31 for pp, pi in cache.index.items():
32 if filename_lower in pi:
33 projpath = pp
34 break
35 else:
36 raise Exception("File doesn't belong to the solution: %s" % filename)
37
38 # Find the project that our file belongs to.
39 proj = cache.slnobj.find_project_by_path(projpath)
40 if not proj:
41 raise Exception("Can't find project in solution: %s" % projpath)
42 logger.debug("Found project %s: %s" % (proj.name, proj.abspath)) 95 logger.debug("Found project %s: %s" % (proj.name, proj.abspath))
43 96
44 # Get the provided config/platform combo, which represent a solution 97 # Get the provided config/platform combo, which represent a solution
45 # configuration, and find the corresponding project configuration. 98 # configuration, and find the corresponding project configuration.
46 # For instance, a solution configuration of "Debug|Win64" could map 99 # For instance, a solution configuration of "Debug|Win64" could map
71 logger.debug("Found configuration type: %s" % cfgtype) 124 logger.debug("Found configuration type: %s" % cfgtype)
72 125
73 # Let's prepare a list of standard stuff for C++. 126 # Let's prepare a list of standard stuff for C++.
74 preproc = [] 127 preproc = []
75 incpaths = [] 128 incpaths = []
129 incfiles = []
76 projdir = os.path.dirname(proj.abspath) 130 projdir = os.path.dirname(proj.abspath)
77 131
78 if cfgtype == 'Makefile': 132 if cfgtype == 'Makefile':
79 # It's a 'Makefile' project, which means we know as little about 133 # It's a 'Makefile' project, which means we know as little about
80 # compiler flags as whatever information was given to VS. As 134 # compiler flags as whatever information was given to VS. As
81 # such, if the solution setup doesn't give enough info, VS 135 # such, if the solution setup doesn't give enough info, VS
82 # intellisense won't work, and neither will YouCompleteMe. 136 # intellisense won't work, and neither will YouCompleteMe.
83 defaultpropgroup = proj.defaultpropertygroup(proj_buildenv) 137 defaultpropgroup = proj.defaultpropertygroup(proj_buildenv)
84 138
85 nmake_preproc = defaultpropgroup.get('NMakePreprocessorDefinitions') 139 nmake_preproc = defaultpropgroup.get('NMakePreprocessorDefinitions')
86 preproc += nmake_preproc.strip(';').split(';') 140 preproc += _split_paths_property(nmake_preproc)
141
142 vs_incpaths = defaultpropgroup.get('IncludePath')
143 if vs_incpaths:
144 incpaths += _split_paths_property_and_make_absolute(
145 projdir, vs_incpaths)
87 146
88 nmake_incpaths = defaultpropgroup.get('NMakeIncludeSearchPath') 147 nmake_incpaths = defaultpropgroup.get('NMakeIncludeSearchPath')
89 incpaths += [os.path.abspath(os.path.join(projdir, p)) 148 if nmake_incpaths:
90 for p in nmake_incpaths.strip(';').split(';')] 149 incpaths += _split_paths_property_and_make_absolute(
150 projdir, nmake_incpaths)
151
152 nmake_forcedincs = defaultpropgroup.get('NMakeForcedIncludes')
153 if nmake_forcedincs:
154 incfiles += _split_paths_property_and_make_absolute(
155 projdir, nmake_forcedincs)
156
157 # Find stuff specific to the file we are working on.
158 defaultitemgroup = proj.defaultitemgroup(proj_buildenv)
159 clcompileitems = list(defaultitemgroup.get_items_of_types([ITEM_TYPE_CPP_SRC, ITEM_TYPE_CPP_HDR]))
160 _find_any_possible_item_specific_flags(
161 solution, slncache, projdir, clcompileitems, filename, incpaths, incfiles)
91 162
92 else: 163 else:
93 # We should definitely support standard VC++ projects here but 164 # We should definitely support standard VC++ projects here but
94 # I don't need it yet :) 165 # I don't need it yet :)
95 raise Exception("Don't know how to handle configuration type: %s" % 166 raise Exception("Don't know how to handle configuration type: %s" %
96 cfgtype) 167 cfgtype)
97 168
169 # We need to duplicate all the forced-included files because they could
170 # have a VS-generated PCH file next to them. Clang then tries to pick it
171 # up and complains that it doesn't use a valid format... :(
172 incfiles = _cache_pch_files(incfiles)
173
98 # Build the clang/YCM flags with what we found. 174 # Build the clang/YCM flags with what we found.
99 flags = ['-x', 'c++'] # TODO: check language type from project file. 175 flags = ['-x', 'c++'] # TODO: check language type from project file.
100 176
101 for symbol in preproc: 177 for symbol in preproc:
102 flags.append('-D%s' % symbol) 178 flags.append('-D%s' % symbol)
103 for path in incpaths: 179 for path in incpaths:
180 flagname = '-I'
104 if path.startswith("C:\\Program Files"): 181 if path.startswith("C:\\Program Files"):
105 flags.append('-isystem') 182 flagname = '-isystem'
183 flagval = path.replace('\\', '/') if force_fwd_slashes else path
184 if short_flags:
185 flags.append('%s%s' % (flagname, flagval))
106 else: 186 else:
107 flags.append('-I') 187 flags.append(flagname)
108 flags.append(path) 188 flags.append(flagval)
189 # For some reason it seems VS applies those in last-to-first order.
190 incfiles = list(reversed(incfiles))
191 for path in incfiles:
192 if force_fwd_slashes:
193 flags.append('--include=%s' % path.replace('\\', '/'))
194 else:
195 flags.append('--include=%s' % path)
196
197 if extraflags:
198 flags += extraflags
109 199
110 return {'flags': flags} 200 return {'flags': flags}
201
202
203 _clang_shadow_pch_suffix = '-for-clang'
204
205
206 def _cache_pch_files(paths):
207 outpaths = []
208 for path in paths:
209 name, ext = os.path.splitext(path)
210 outpath = "%s%s%s" % (name, _clang_shadow_pch_suffix, ext)
211
212 do_cache = False
213 orig_mtime = os.path.getmtime(path)
214 try:
215 out_mtime = os.path.getmtime(outpath)
216 if orig_mtime >= out_mtime:
217 do_cache = True
218 except OSError:
219 do_cache = True
220
221 if do_cache:
222 logger.debug("Creating shadow PCH file: %s" % path)
223 shutil.copy2(path, outpath)
224
225 outpaths.append(outpath)
226 return outpaths
111 227
112 228
113 def _build_env_from_vim(client_data): 229 def _build_env_from_vim(client_data):
114 buildenv = {} 230 buildenv = {}
115 buildenv['Configuration'] = client_data.get('g:vimcrosoft_current_config', '') 231 buildenv['Configuration'] = client_data.get('g:vimcrosoft_current_config', '')
116 buildenv['Platform'] = client_data.get('g:vimcrosoft_current_platform', '') 232 buildenv['Platform'] = client_data.get('g:vimcrosoft_current_platform', '')
117 return buildenv 233 return buildenv
234
235
236 _dump_debug_file = True
237
238
239 def _do_dump_debug_file(args, flags, debug_filename):
240 import pprint
241
242 with open(debug_filename, 'w') as fp:
243 fp.write(" args: \n")
244 fp.write("=========\n")
245 pp = pprint.PrettyPrinter(indent=2, stream=fp)
246 pp.pprint(args)
247 fp.write("\n\n")
248
249 fp.write(" flags: \n")
250 fp.write("==========\n")
251 flags = flags.get('flags')
252 if flags:
253 fp.write(flags[0])
254 for flag in flags[1:]:
255 if flag[0] == '-':
256 fp.write("\n")
257 else:
258 fp.write(" ")
259 fp.write(flag)
260 else:
261 fp.write("<no flags found>\n")
262 fp.write("\n\n")
118 263
119 264
120 def Settings(**kwargs): 265 def Settings(**kwargs):
121 language = kwargs.get('language') 266 language = kwargs.get('language')
122 filename = kwargs.get('filename') 267 filename = kwargs.get('filename')
125 from_cli = kwargs.get('from_cli', False) 270 from_cli = kwargs.get('from_cli', False)
126 if from_cli: 271 if from_cli:
127 solution = client_data.get('solution') 272 solution = client_data.get('solution')
128 slncache = client_data.get('slncache') 273 slncache = client_data.get('slncache')
129 buildenv = client_data.get('env', {}) 274 buildenv = client_data.get('env', {})
275 extraflags = client_data.get('extra_flags')
130 else: 276 else:
277 if not client_data:
278 raise Exception("No client data provided by Vim host!")
131 solution = client_data.get('g:vimcrosoft_current_sln') 279 solution = client_data.get('g:vimcrosoft_current_sln')
132 slncache = client_data.get('g:vimcrosoft_current_sln_cache') 280 slncache = client_data.get('g:vimcrosoft_current_sln_cache')
133 buildenv = _build_env_from_vim(client_data) 281 buildenv = _build_env_from_vim(client_data)
282 extraflags = client_data.get('g:vimcrosoft_extra_clang_args')
283
284 extraflags = _expand_extra_flags_with_solution_extra_flags(solution, extraflags)
134 285
135 flags = None 286 flags = None
136 287
137 if language == 'cfamily': 288 if language == 'cfamily':
138 try: 289 try:
139 flags = _build_cflags(filename, solution, 290 flags = _build_cflags(filename, solution,
140 buildenv=buildenv, slncache=slncache) 291 buildenv=buildenv, slncache=slncache,
292 extraflags=extraflags)
141 except Exception as exc: 293 except Exception as exc:
142 if from_cli: 294 if from_cli:
143 raise 295 raise
144 flags = {'error': str(exc)} 296 flags = {'error': str(exc)}
145 else: 297 else:
146 flags = {'error': f"Unknown language: {language}"} 298 flags = {'error': f"Unknown language: {language}"}
147 299
148 with open("D:\\P4\\DevEditor\\debug.txt", 'w') as fp: 300 if _dump_debug_file:
149 fp.write("kwargs:") 301 debug_filename = os.path.join(
150 fp.write(str(kwargs)) 302 os.path.dirname(solution), '.vimcrosoft', 'debug_flags.txt')
151 fp.write("client_data:") 303 _do_dump_debug_file(kwargs, flags, debug_filename)
152 fp.write(str(list(kwargs['client_data'].items()))) 304
153 fp.write("flags:")
154 fp.write(str(flags))
155 return flags 305 return flags
156 306
157 307
158 languages = { 308 languages = {
159 'cfamily': ['h', 'c', 'hpp', 'cpp', 'inl'] 309 'cfamily': ['h', 'c', 'hpp', 'cpp', 'inl']
173 parser = argparse.ArgumentParser() 323 parser = argparse.ArgumentParser()
174 parser.add_argument('solution', 324 parser.add_argument('solution',
175 help="The solution file") 325 help="The solution file")
176 parser.add_argument('filename', 326 parser.add_argument('filename',
177 help="The filename for which to get flags") 327 help="The filename for which to get flags")
328 parser.add_argument('--no-auto-env',
329 action='store_true',
330 help="Don't read configuration information from Vimcrosoft cache")
178 parser.add_argument('-p', '--property', 331 parser.add_argument('-p', '--property',
179 action="append", 332 action="append",
180 help="Specifies a build property") 333 help="Specifies a build property")
181 parser.add_argument('-c', '--cache', 334 parser.add_argument('-c', '--cache',
182 help="The solution cache to use") 335 help="The solution cache to use")
336 parser.add_argument('--cmdline',
337 action='store_true',
338 help="Output flags in a command-line form")
183 parser.add_argument('-v', '--verbose', 339 parser.add_argument('-v', '--verbose',
184 action='store_true', 340 action='store_true',
185 help="Show debugging information") 341 help="Show debugging information")
186 args = parser.parse_args() 342 args = parser.parse_args()
187 setup_logging(args.verbose) 343 setup_logging(args.verbose)
188 344
189 lang = _get_language(args.filename) 345 lang = _get_language(args.filename)
190 logger.debug(f"Got language {lang} for {args.filename}") 346 logger.debug(f"Got language {lang} for {args.filename}")
191 347
192 build_env = {} 348 build_env = {}
349 slncache = args.cache
350 if not args.no_auto_env:
351 load_vimcrosoft_auto_env(args.solution, build_env)
352 if not slncache:
353 slncache = find_vimcrosoft_slncache(args.solution)
193 if args.property: 354 if args.property:
194 for p in args.property: 355 for p in args.property:
195 pname, pval = p.split('=', 1) 356 pname, pval = p.split('=', 1)
196 build_env[pname] = pval 357 build_env[pname] = pval
197 logger.debug(f"Got build environment: {build_env}") 358 logger.debug(f"Got build environment: {build_env}")
198 client_data = {'solution': args.solution, 359 client_data = {'solution': args.solution,
199 'slncache': args.cache, 360 'slncache': slncache,
200 'env': build_env} 361 'env': build_env}
201 362
202 params = {'from_cli': True, 363 params = {'from_cli': True,
203 'language': lang, 364 'language': lang,
204 'filename': args.filename, 365 'filename': args.filename,
205 'client_data': client_data 366 'client_data': client_data
206 } 367 }
207 flags = Settings(**params) 368 flags = Settings(**params)
208 logger.info("Flags:") 369 if args.cmdline:
209 import pprint 370 import shlex
210 pp = pprint.PrettyPrinter(indent=2) 371 if hasattr(shlex, 'join'):
211 pp.pprint(flags) 372 joinargs = shlex.join
373 else:
374 joinargs = lambda a: ' '.join(a)
375
376 with open('clang_ycm_args.rsp', 'w', encoding='utf8') as fp:
377 fp.write("%s -fsyntax-only \"%s\"" % (
378 joinargs(sanitizeargs(flags['flags'])),
379 args.filename.replace('\\', '/')
380 ))
381 with open('clang_ycm_invoke.cmd', 'w', encoding='utf8') as fp:
382 fp.write("\"c:\\Program Files\\LLVM\\bin\\clang++.exe\" @clang_ycm_args.rsp > clang_ycm_invoke.log 2>&1")
383
384 logger.info("Command line written to: clang_ycm_invoke.cmd")
385 else:
386 logger.info("Flags:")
387 import pprint
388 pp = pprint.PrettyPrinter(indent=2)
389 pp.pprint(flags)
390
391
392 def sanitizeargs(args):
393 for arg in args:
394 if ' ' in arg:
395 yield '"%s"' % arg
396 else:
397 yield arg
212 398
213 399
214 if __name__ == '__main__': 400 if __name__ == '__main__':
215 main() 401 main()