Mercurial > vim-crosoft
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() |