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