Mercurial > vim-crosoft
comparison scripts/vsutil.py @ 15:cfcac4ed7d21 default tip
Improve loading of solution files
- New argument to force a rebuild of the cache
- Gracefully handle missing projects in a solution
- Handle more different xml namespaces
- Support more edge cases
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Tue, 29 Aug 2023 12:59:54 -0700 |
parents | 4ba6df1b2f97 |
children |
comparison
equal
deleted
inserted
replaced
14:0aa61944e518 | 15:cfcac4ed7d21 |
---|---|
197 self.path = path | 197 self.path = path |
198 self.guid = guid | 198 self.guid = guid |
199 self._itemgroups = None | 199 self._itemgroups = None |
200 self._propgroups = None | 200 self._propgroups = None |
201 self._sln = None | 201 self._sln = None |
202 self._missing = False | |
202 | 203 |
203 @property | 204 @property |
204 def is_folder(self): | 205 def is_folder(self): |
205 """ Returns whether this project is actually just a solution | 206 """ Returns whether this project is actually just a solution |
206 folder, used as a container for other projects. | 207 folder, used as a container for other projects. |
298 tree = etree.parse(abspath) | 299 tree = etree.parse(abspath) |
299 except (FileNotFoundError, OSError) as ex: | 300 except (FileNotFoundError, OSError) as ex: |
300 logger.debug(f"Error loading project {self.name}: " + str(ex)) | 301 logger.debug(f"Error loading project {self.name}: " + str(ex)) |
301 self._itemgroups = {} | 302 self._itemgroups = {} |
302 self._propgroups= {} | 303 self._propgroups= {} |
304 self._missing = True | |
303 return | 305 return |
304 | 306 |
305 root = tree.getroot() | 307 root = tree.getroot() |
306 if _strip_ns(root.tag) != 'Project': | 308 if _strip_ns(root.tag) != 'Project': |
307 raise Exception(f"Expected root node 'Project', got '{root.tag}'") | 309 raise Exception(f"Expected root node 'Project', got '{root.tag}'") |
308 | 310 |
311 # Load ItemGroups and PropertyGroups via both namespaced names and raw | |
312 # names because not all types of VS projects use the MS namespaces. | |
309 self._itemgroups = {} | 313 self._itemgroups = {} |
314 for itemgroupnode in root.iterfind('ItemGroup', ns): | |
315 self._load_item_group(itemgroupnode) | |
310 for itemgroupnode in root.iterfind('ms:ItemGroup', ns): | 316 for itemgroupnode in root.iterfind('ms:ItemGroup', ns): |
311 label = itemgroupnode.attrib.get('Label') | 317 self._load_item_group(itemgroupnode) |
312 itemgroup = self._itemgroups.get(label) | |
313 if not itemgroup: | |
314 itemgroup = VSProjectItemGroup(label) | |
315 self._itemgroups[label] = itemgroup | |
316 logger.debug(f"Adding itemgroup '{label}'") | |
317 | |
318 condition = itemgroupnode.attrib.get('Condition') | |
319 if condition: | |
320 itemgroup = itemgroup.get_or_create_conditional(condition) | |
321 | |
322 for itemnode in itemgroupnode: | |
323 incval = itemnode.attrib.get('Include') | |
324 item = VSProjectItem(incval, _strip_ns(itemnode.tag)) | |
325 itemgroup.items.append(item) | |
326 for metanode in itemnode: | |
327 item.metadata[_strip_ns(metanode.tag)] = metanode.text | |
328 | 318 |
329 self._propgroups = {} | 319 self._propgroups = {} |
320 for propgroupnode in root.iterfind('PropertyGroup', ns): | |
321 self._load_property_group(propgroupnode) | |
330 for propgroupnode in root.iterfind('ms:PropertyGroup', ns): | 322 for propgroupnode in root.iterfind('ms:PropertyGroup', ns): |
331 label = propgroupnode.attrib.get('Label') | 323 self._load_property_group(propgroupnode) |
332 propgroup = self._propgroups.get(label) | 324 |
333 if not propgroup: | 325 def _load_item_group(self, itemgroupnode): |
334 propgroup = VSProjectPropertyGroup(label) | 326 label = itemgroupnode.attrib.get('Label') |
335 self._propgroups[label] = propgroup | 327 itemgroup = self._itemgroups.get(label) |
336 logger.debug(f"Adding propertygroup '{label}'") | 328 if not itemgroup: |
337 | 329 itemgroup = VSProjectItemGroup(label) |
338 condition = propgroupnode.attrib.get('Condition') | 330 self._itemgroups[label] = itemgroup |
339 if condition: | 331 logger.debug(f"Adding itemgroup '{label}'") |
340 propgroup = propgroup.get_or_create_conditional(condition) | 332 |
341 | 333 condition = itemgroupnode.attrib.get('Condition') |
342 for propnode in propgroupnode: | 334 if condition: |
343 propgroup.properties.append(VSProjectProperty( | 335 itemgroup = itemgroup.get_or_create_conditional(condition) |
344 _strip_ns(propnode.tag), | 336 |
345 propnode.text)) | 337 for itemnode in itemgroupnode: |
338 incval = itemnode.attrib.get('Include') | |
339 item = VSProjectItem(incval, _strip_ns(itemnode.tag)) | |
340 itemgroup.items.append(item) | |
341 for metanode in itemnode: | |
342 item.metadata[_strip_ns(metanode.tag)] = metanode.text | |
343 | |
344 def _load_property_group(self, propgroupnode): | |
345 label = propgroupnode.attrib.get('Label') | |
346 propgroup = self._propgroups.get(label) | |
347 if not propgroup: | |
348 propgroup = VSProjectPropertyGroup(label) | |
349 self._propgroups[label] = propgroup | |
350 logger.debug(f"Adding propertygroup '{label}'") | |
351 | |
352 condition = propgroupnode.attrib.get('Condition') | |
353 if condition: | |
354 propgroup = propgroup.get_or_create_conditional(condition) | |
355 | |
356 for propnode in propgroupnode: | |
357 propgroup.properties.append(VSProjectProperty( | |
358 _strip_ns(propnode.tag), | |
359 propnode.text)) | |
346 | 360 |
347 | 361 |
348 class MissingVSProjectError(Exception): | 362 class MissingVSProjectError(Exception): |
349 pass | 363 pass |
350 | 364 |
474 | 488 |
475 m = _re_sln_global_section_start.search(line) | 489 m = _re_sln_global_section_start.search(line) |
476 if m: | 490 if m: |
477 # Found the start of a new section. | 491 # Found the start of a new section. |
478 in_global_section = VSGlobalSection(m.group('name')) | 492 in_global_section = VSGlobalSection(m.group('name')) |
479 logging.debug(f" Adding global section {in_global_section.name}") | 493 logging.debug(f" Adding global section {in_global_section.name} (line {i})") |
480 slnobj.sections.append(in_global_section) | 494 slnobj.sections.append(in_global_section) |
481 continue | 495 continue |
482 | 496 |
483 m = _re_sln_global_end.search(line) | 497 m = _re_sln_global_end.search(line) |
484 if m: | 498 if m: |
499 slnobj, | 513 slnobj, |
500 m.group('type'), m.group('name'), m.group('path'), | 514 m.group('type'), m.group('name'), m.group('path'), |
501 m.group('guid')) | 515 m.group('guid')) |
502 except: | 516 except: |
503 raise Exception(f"Error line {i}: unexpected project syntax.") | 517 raise Exception(f"Error line {i}: unexpected project syntax.") |
504 logging.debug(f" Adding project {p.name}") | 518 logging.debug(f" Adding project {p.name} (line {i})") |
505 slnobj.projects.append(p) | 519 slnobj.projects.append(p) |
506 p._sln = slnobj | 520 p._sln = slnobj |
507 | 521 |
508 until = _re_sln_project_decl_end | 522 until = _re_sln_project_decl_end |
509 continue | 523 continue |
521 | 535 |
522 class SolutionCache: | 536 class SolutionCache: |
523 """ A class that contains a VS solution object, along with pre-indexed | 537 """ A class that contains a VS solution object, along with pre-indexed |
524 lists of items. It's meant to be saved on disk. | 538 lists of items. It's meant to be saved on disk. |
525 """ | 539 """ |
526 VERSION = 4 | 540 VERSION = 5 |
527 | 541 |
528 def __init__(self, slnobj): | 542 def __init__(self, slnobj): |
529 self.slnobj = slnobj | 543 self.slnobj = slnobj |
530 self.index = None | 544 self.index = None |
531 self._saved_version = SolutionCache.VERSION | 545 self._saved_version = SolutionCache.VERSION |
541 | 555 |
542 item_cache = set() | 556 item_cache = set() |
543 self.index[proj.abspath] = item_cache | 557 self.index[proj.abspath] = item_cache |
544 | 558 |
545 for item in itemgroup.get_source_items(): | 559 for item in itemgroup.get_source_items(): |
546 item_path = proj.get_abs_item_include(item).lower() | 560 if item.include: |
547 item_cache.add(item_path) | 561 item_path = proj.get_abs_item_include(item).lower() |
562 item_cache.add(item_path) | |
563 # else: it's an item from our shortlist (cpp, cs, etc files) | |
564 # but it somehow doesn't have a path, which can happen with | |
565 # some obscure VS features. | |
548 | 566 |
549 def save(self, path): | 567 def save(self, path): |
550 pathdir = os.path.dirname(path) | 568 pathdir = os.path.dirname(path) |
551 if not os.path.exists(pathdir): | 569 if not os.path.exists(pathdir): |
552 os.makedirs(pathdir) | 570 os.makedirs(pathdir) |
553 with open(path, 'wb') as fp: | 571 with open(path, 'wb') as fp: |
554 pickle.dump(self, fp) | 572 pickle.dump(self, fp) |
555 | 573 |
556 @staticmethod | 574 @staticmethod |
557 def load_or_rebuild(slnpath, cachepath): | 575 def load_or_rebuild(slnpath, cachepath, force_rebuild=False): |
558 if cachepath: | 576 if cachepath and not force_rebuild: |
559 res = _try_load_from_cache(slnpath, cachepath) | 577 res = _try_load_from_cache(slnpath, cachepath) |
560 if res is not None: | 578 if res is not None: |
561 return res | 579 return res |
562 | 580 |
563 slnobj = parse_sln_file(slnpath) | 581 slnobj = parse_sln_file(slnpath) |
586 | 604 |
587 # Our cache is at least valid for the solution stuff. Some of our | 605 # Our cache is at least valid for the solution stuff. Some of our |
588 # projects might be out of date, but at least there can't be any | 606 # projects might be out of date, but at least there can't be any |
589 # added or removed projects from the solution (otherwise the solution | 607 # added or removed projects from the solution (otherwise the solution |
590 # file would have been touched). Let's load the cache. | 608 # file would have been touched). Let's load the cache. |
591 with open(cachepath, 'rb') as fp: | 609 try: |
592 cache = pickle.load(fp) | 610 with open(cachepath, 'rb') as fp: |
611 cache = pickle.load(fp) | |
612 except Exception as ex: | |
613 logger.debug("Error loading solution cache: %s" % ex) | |
614 logger.debug("Deleting cache: %s" % cachepath) | |
615 os.remove(cachepath) | |
616 return None | |
593 | 617 |
594 # Check that the cache version is up-to-date with this code. | 618 # Check that the cache version is up-to-date with this code. |
595 loaded_ver = getattr(cache, '_saved_version', 0) | 619 loaded_ver = getattr(cache, '_saved_version', 0) |
596 if loaded_ver != SolutionCache.VERSION: | 620 if loaded_ver != SolutionCache.VERSION: |
597 logger.debug(f"Cache was saved with older format: {cachepath} " | 621 logger.debug(f"Cache was saved with older format: {cachepath} " |
606 proj_dts = [] | 630 proj_dts = [] |
607 for p in slnobj.projects: | 631 for p in slnobj.projects: |
608 if not p.is_folder: | 632 if not p.is_folder: |
609 try: | 633 try: |
610 proj_dts.append(os.path.getmtime(p.abspath)) | 634 proj_dts.append(os.path.getmtime(p.abspath)) |
635 # The project was missing last time we built the cache, | |
636 # but now it exists. Force a rebuild. | |
637 if p._missing: | |
638 return None | |
611 except OSError: | 639 except OSError: |
612 logger.debug(f"Found missing project: {p.abspath}") | 640 if not p._missing: |
613 return None | 641 logger.debug(f"Found missing project: {p.abspath}") |
642 return None | |
643 # else: it was already missing last time we built the | |
644 # cache, so nothing has changed. | |
614 | 645 |
615 if all([cache_dt > pdt for pdt in proj_dts]): | 646 if all([cache_dt > pdt for pdt in proj_dts]): |
616 logger.debug(f"Cache is up to date: {cachepath}") | 647 logger.debug(f"Cache is up to date: {cachepath}") |
617 return (cache, True) | 648 return (cache, True) |
618 | 649 |