Mercurial > vim-crosoft
annotate scripts/vsutil.py @ 4:ae0fb567f459
Report error but don't crash when a solution points to a missing project.
| author | Ludovic Chabant <ludovic@chabant.com> |
|---|---|
| date | Thu, 24 Oct 2019 11:15:37 -0700 |
| parents | 949c4f536f26 |
| children | 4ba6df1b2f97 |
| rev | line source |
|---|---|
| 0 | 1 import copy |
| 2 import logging | |
| 3 import os.path | |
| 4 import pickle | |
| 5 import re | |
| 6 import xml.etree.ElementTree as etree | |
| 7 | |
| 8 | |
| 9 # Known VS project types. | |
| 10 PROJ_TYPE_FOLDER = '2150E333-8FDC-42A3-9474-1A3956D46DE8' | |
| 11 PROJ_TYPE_NMAKE = '8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942' | |
| 12 PROJ_TYPE_CSHARP = 'FAE04EC0-301F-11D3-BF4B-00C04F79EFBC' | |
| 13 | |
| 14 PROJ_TYPE_NAMES = { | |
| 15 PROJ_TYPE_FOLDER: 'folder', | |
| 16 PROJ_TYPE_NMAKE: 'nmake', | |
| 17 PROJ_TYPE_CSHARP: 'csharp' | |
| 18 } | |
| 19 | |
| 20 # Known VS item types. | |
| 21 ITEM_TYPE_CPP_SRC = 'ClCompile' | |
| 22 ITEM_TYPE_CPP_HDR = 'ClInclude' | |
| 23 | |
| 24 ITEM_TYPE_CS_REF = 'Reference' | |
| 25 ITEM_TYPE_CS_PROJREF = 'ProjectReference' | |
| 26 ITEM_TYPE_CS_SRC = 'Compile' | |
| 27 | |
| 28 ITEM_TYPE_NONE = 'None' | |
| 29 | |
| 30 ITEM_TYPE_SOURCE_FILES = (ITEM_TYPE_CPP_SRC, ITEM_TYPE_CPP_HDR, | |
|
3
949c4f536f26
Add `None` file items to source solution files.
Ludovic Chabant <ludovic@chabant.com>
parents:
0
diff
changeset
|
31 ITEM_TYPE_CS_SRC, ITEM_TYPE_NONE) |
| 0 | 32 |
| 33 | |
| 34 # Known VS properties. | |
| 35 PROP_CONFIGURATION_TYPE = 'ConfigurationType' | |
| 36 PROP_NMAKE_PREPROCESSOR_DEFINITIONS = 'NMakePreprocessorDefinitions' | |
| 37 PROP_NMAKE_INCLUDE_SEARCH_PATH = 'NMakeIncludeSearchPath' | |
| 38 | |
| 39 | |
| 40 logger = logging.getLogger(__name__) | |
| 41 | |
| 42 | |
| 43 def _strip_ns(tag): | |
| 44 """ Remove the XML namespace from a tag name. """ | |
| 45 if tag[0] == '{': | |
| 46 i = tag.index('}') | |
| 47 return tag[i+1:] | |
| 48 return tag | |
| 49 | |
| 50 | |
| 51 re_msbuild_var = re.compile(r'\$\((?P<var>[\w\d_]+)\)') | |
| 52 | |
| 53 | |
| 54 def _resolve_value(val, env): | |
| 55 """ Expands MSBuild property values given a build environment. """ | |
| 56 def _repl_vars(m): | |
| 57 varname = m.group('var') | |
| 58 varval = env.get(varname, '') | |
| 59 return varval | |
| 60 | |
| 61 if not val: | |
| 62 return val | |
| 63 return re_msbuild_var.sub(_repl_vars, val) | |
| 64 | |
| 65 | |
| 66 def _evaluate_condition(cond, env): | |
| 67 """ Expands MSBuild property values in a condition and evaluates it. """ | |
| 68 left, right = _resolve_value(cond, env).split('==') | |
| 69 return left == right | |
| 70 | |
| 71 | |
| 72 class VSBaseGroup: | |
| 73 """ Base class for VS project stuff that has conditional stuff inside. | |
| 74 | |
| 75 For instance, a property group called 'Blah' might have some common | |
| 76 (always valid) stuff, but a bunch of other stuff that should only | |
| 77 be considered when the solution configuration is Debug, Release, or | |
| 78 whatever else. In that case, each 'conditional' (i.e. values for Debug, | |
| 79 values for Release, etc.) is listed and tracked separately until | |
| 80 we are asked to 'resolve' ourselves based on a given build environment. | |
| 81 """ | |
| 82 def __init__(self, label): | |
| 83 self.label = label | |
| 84 self.conditionals = {} | |
| 85 | |
| 86 def get_conditional(self, condition): | |
| 87 """ Adds a conditional sub-group. """ | |
| 88 return self.conditionals.get(condition) | |
| 89 | |
| 90 def get_or_create_conditional(self, condition): | |
| 91 """ Gets or creates a new conditional sub-group. """ | |
| 92 c = self.get_conditional(condition) | |
| 93 if not c: | |
| 94 c = self.__class__(self.label) | |
| 95 self.conditionals[condition] = c | |
| 96 return c | |
| 97 | |
| 98 def resolve(self, env): | |
| 99 """ Resolves this group by evaluating each conditional sub-group | |
| 100 based on the given build environment. Returns a 'flattened' | |
| 101 version of ourselves. | |
| 102 """ | |
| 103 c = self.__class__(self.label) | |
| 104 c._collapse_child(self, env) | |
| 105 | |
| 106 for cond, child in self.conditionals.items(): | |
| 107 if _evaluate_condition(cond, env): | |
| 108 c._collapse_child(child, env) | |
| 109 | |
| 110 return c | |
| 111 | |
| 112 | |
| 113 class VSProjectItem: | |
| 114 """ A VS project item, like a source code file. """ | |
| 115 def __init__(self, include, itemtype=None): | |
| 116 self.include = include | |
| 117 self.itemtype = itemtype | |
| 118 self.metadata = {} | |
| 119 | |
| 120 def resolve(self, env): | |
| 121 c = VSProjectItem(_resolve_value(self.include), self.itemtype) | |
| 122 c.metadata = {k: _resolve_value(v, env) | |
| 123 for k, v in self.metadata.items()} | |
| 124 return c | |
| 125 | |
| 126 def __str__(self): | |
| 127 return "(%s)%s" % (self.itemtype, self.include) | |
| 128 | |
| 129 | |
| 130 class VSProjectItemGroup(VSBaseGroup): | |
| 131 """ A VS project item group, like a list of source code files, | |
| 132 or a list of resources. | |
| 133 """ | |
| 134 def __init__(self, label): | |
| 135 super().__init__(label) | |
| 136 self.items = [] | |
| 137 | |
| 138 def get_source_items(self): | |
|
3
949c4f536f26
Add `None` file items to source solution files.
Ludovic Chabant <ludovic@chabant.com>
parents:
0
diff
changeset
|
139 return self.get_items_of_types(ITEM_TYPE_SOURCE_FILES) |
|
949c4f536f26
Add `None` file items to source solution files.
Ludovic Chabant <ludovic@chabant.com>
parents:
0
diff
changeset
|
140 |
|
949c4f536f26
Add `None` file items to source solution files.
Ludovic Chabant <ludovic@chabant.com>
parents:
0
diff
changeset
|
141 def get_items_of_types(self, *itemtypes): |
|
949c4f536f26
Add `None` file items to source solution files.
Ludovic Chabant <ludovic@chabant.com>
parents:
0
diff
changeset
|
142 typeset = set(*itemtypes) |
| 0 | 143 for i in self.items: |
|
3
949c4f536f26
Add `None` file items to source solution files.
Ludovic Chabant <ludovic@chabant.com>
parents:
0
diff
changeset
|
144 if i.itemtype in typeset: |
| 0 | 145 yield i |
| 146 | |
| 147 def _collapse_child(self, child, env): | |
| 148 self.items += [i.resolve(env) for i in child.items] | |
| 149 | |
| 150 | |
| 151 class VSProjectProperty: | |
| 152 """ A VS project property, like an include path or compiler flag. """ | |
| 153 def __init__(self, name, value): | |
| 154 self.name = name | |
| 155 self.value = value | |
| 156 | |
| 157 def resolve(self, env): | |
| 158 c = VSProjectProperty(self.name, _resolve_value(self.value, env)) | |
| 159 return c | |
| 160 | |
| 161 def __str__(self): | |
| 162 return "%s=%s" % (self.name, self.value) | |
| 163 | |
| 164 | |
| 165 class VSProjectPropertyGroup(VSBaseGroup): | |
| 166 """ A VS project property group, such as compiler macros or flags. """ | |
| 167 def __init__(self, label): | |
| 168 super().__init__(label) | |
| 169 self.properties = [] | |
| 170 | |
| 171 def get(self, propname): | |
| 172 try: | |
| 173 return self[propname] | |
| 174 except IndexError: | |
| 175 return None | |
| 176 | |
| 177 def __getitem__(self, propname): | |
| 178 for p in self.properties: | |
| 179 if p.name == propname: | |
| 180 return p.value | |
| 181 raise IndexError() | |
| 182 | |
| 183 def _collapse_child(self, child, env): | |
| 184 self.properties += [p.resolve(env) for p in child.properties] | |
| 185 | |
| 186 | |
| 187 class VSProject: | |
| 188 """ A VS project. """ | |
| 189 def __init__(self, projtype, name, path, guid): | |
| 190 self.type = projtype | |
| 191 self.name = name | |
| 192 self.path = path | |
| 193 self.guid = guid | |
| 194 self._itemgroups = None | |
| 195 self._propgroups = None | |
| 196 self._sln = None | |
| 197 | |
| 198 @property | |
| 199 def is_folder(self): | |
| 200 """ Returns whether this project is actually just a solution | |
| 201 folder, used as a container for other projects. | |
| 202 """ | |
| 203 return self.type == PROJ_TYPE_FOLDER | |
| 204 | |
| 205 @property | |
| 206 def abspath(self): | |
| 207 abspath = self.path | |
| 208 if self._sln and self._sln.path: | |
| 209 abspath = os.path.join(self._sln.dirpath, self.path) | |
| 210 return abspath | |
| 211 | |
| 212 @property | |
| 213 def absdirpath(self): | |
| 214 return os.path.dirname(self.abspath) | |
| 215 | |
| 216 @property | |
| 217 def itemgroups(self): | |
| 218 self._ensure_loaded() | |
| 219 return self._itemgroups.values() | |
| 220 | |
| 221 @property | |
| 222 def propertygroups(self): | |
| 223 self._ensure_loaded() | |
| 224 return self._propgroups.values() | |
| 225 | |
| 226 def itemgroup(self, label, resolved_with=None): | |
| 227 self._ensure_loaded() | |
| 228 ig = self._itemgroups.get(label) | |
| 229 if resolved_with is not None and ig is not None: | |
| 230 logger.debug("Resolving item group '%s'." % ig.label) | |
| 231 ig = ig.resolve(resolved_with) | |
| 232 return ig | |
| 233 | |
| 234 def defaultitemgroup(self, resolved_with=None): | |
| 235 return self.itemgroup(None, resolved_with=resolved_with) | |
| 236 | |
| 237 def propertygroup(self, label, resolved_with=None): | |
| 238 self._ensure_loaded() | |
| 239 pg = self._propgroups.get(label) | |
| 240 if resolved_with is not None and pg is not None: | |
| 241 logger.debug("Resolving property group '%s'." % pg.label) | |
| 242 pg = pg.resolve(resolved_with) | |
| 243 return pg | |
| 244 | |
| 245 def defaultpropertygroup(self, resolved_with=None): | |
| 246 return self.propertygroup(None, resolved_with=resolved_with) | |
| 247 | |
| 248 def get_abs_item_include(self, item): | |
| 249 return os.path.abspath(os.path.join(self.absdirpath, item.include)) | |
| 250 | |
| 251 def resolve(self, env): | |
| 252 self._ensure_loaded() | |
| 253 | |
| 254 propgroups = list(self._propgroups) | |
| 255 itemgroups = list(self._itemgroups) | |
| 256 self._propgroups[:] = [] | |
| 257 self._itemgroups[:] = [] | |
| 258 | |
| 259 for pg in propgroups: | |
| 260 rpg = pg.resolve(env) | |
| 261 self._propgroups.append(rpg) | |
| 262 | |
| 263 for ig in itemgroups: | |
| 264 rig = ig.resolve(env) | |
| 265 self._itemgroups.append(rig) | |
| 266 | |
| 267 def _ensure_loaded(self): | |
| 268 if self._itemgroups is None or self._propgroups is None: | |
| 269 self._load() | |
| 270 | |
| 271 def _load(self): | |
| 272 if not self.path: | |
| 273 raise Exception("The current project has no path.") | |
| 274 if self.is_folder: | |
| 275 logger.debug(f"Skipping folder project {self.name}") | |
| 276 self._itemgroups = {} | |
| 277 self._propgroups = {} | |
| 278 return | |
| 279 | |
| 280 ns = {'ms': 'http://schemas.microsoft.com/developer/msbuild/2003'} | |
| 281 | |
| 282 abspath = self.abspath | |
| 283 logger.debug(f"Loading project {self.name} ({self.path}) from: {abspath}") | |
|
4
ae0fb567f459
Report error but don't crash when a solution points to a missing project.
Ludovic Chabant <ludovic@chabant.com>
parents:
3
diff
changeset
|
284 try: |
|
ae0fb567f459
Report error but don't crash when a solution points to a missing project.
Ludovic Chabant <ludovic@chabant.com>
parents:
3
diff
changeset
|
285 tree = etree.parse(abspath) |
|
ae0fb567f459
Report error but don't crash when a solution points to a missing project.
Ludovic Chabant <ludovic@chabant.com>
parents:
3
diff
changeset
|
286 except (FileNotFoundError, OSError) as ex: |
|
ae0fb567f459
Report error but don't crash when a solution points to a missing project.
Ludovic Chabant <ludovic@chabant.com>
parents:
3
diff
changeset
|
287 logger.debug(f"Error loading project {self.name}: " + str(ex)) |
|
ae0fb567f459
Report error but don't crash when a solution points to a missing project.
Ludovic Chabant <ludovic@chabant.com>
parents:
3
diff
changeset
|
288 self._itemgroups = {} |
|
ae0fb567f459
Report error but don't crash when a solution points to a missing project.
Ludovic Chabant <ludovic@chabant.com>
parents:
3
diff
changeset
|
289 self._propgroups= {} |
|
ae0fb567f459
Report error but don't crash when a solution points to a missing project.
Ludovic Chabant <ludovic@chabant.com>
parents:
3
diff
changeset
|
290 return |
|
ae0fb567f459
Report error but don't crash when a solution points to a missing project.
Ludovic Chabant <ludovic@chabant.com>
parents:
3
diff
changeset
|
291 |
| 0 | 292 root = tree.getroot() |
| 293 if _strip_ns(root.tag) != 'Project': | |
| 294 raise Exception(f"Expected root node 'Project', got '{root.tag}'") | |
| 295 | |
| 296 self._itemgroups = {} | |
| 297 for itemgroupnode in root.iterfind('ms:ItemGroup', ns): | |
| 298 label = itemgroupnode.attrib.get('Label') | |
| 299 itemgroup = self._itemgroups.get(label) | |
| 300 if not itemgroup: | |
| 301 itemgroup = VSProjectItemGroup(label) | |
| 302 self._itemgroups[label] = itemgroup | |
| 303 logger.debug(f"Adding itemgroup '{label}'") | |
| 304 | |
| 305 condition = itemgroupnode.attrib.get('Condition') | |
| 306 if condition: | |
| 307 itemgroup = itemgroup.get_or_create_conditional(condition) | |
| 308 | |
| 309 for itemnode in itemgroupnode: | |
| 310 incval = itemnode.attrib.get('Include') | |
| 311 item = VSProjectItem(incval, _strip_ns(itemnode.tag)) | |
| 312 itemgroup.items.append(item) | |
| 313 for metanode in itemnode: | |
| 314 item.metadata[_strip_ns(metanode.tag)] = metanode.text | |
| 315 | |
| 316 self._propgroups = {} | |
| 317 for propgroupnode in root.iterfind('ms:PropertyGroup', ns): | |
| 318 label = propgroupnode.attrib.get('Label') | |
| 319 propgroup = self._propgroups.get(label) | |
| 320 if not propgroup: | |
| 321 propgroup = VSProjectPropertyGroup(label) | |
| 322 self._propgroups[label] = propgroup | |
| 323 logger.debug(f"Adding propertygroup '{label}'") | |
| 324 | |
| 325 condition = propgroupnode.attrib.get('Condition') | |
| 326 if condition: | |
| 327 propgroup = propgroup.get_or_create_conditional(condition) | |
| 328 | |
| 329 for propnode in propgroupnode: | |
| 330 propgroup.properties.append(VSProjectProperty( | |
| 331 _strip_ns(propnode.tag), | |
| 332 propnode.text)) | |
| 333 | |
| 334 | |
| 335 class MissingVSProjectError(Exception): | |
| 336 pass | |
| 337 | |
| 338 | |
| 339 class VSGlobalSectionEntry: | |
| 340 """ An entry in a VS solution's global section. """ | |
| 341 def __init__(self, name, value): | |
| 342 self.name = name | |
| 343 self.value = value | |
| 344 | |
| 345 | |
| 346 class VSGlobalSection: | |
| 347 """ A global section in a VS solution. """ | |
| 348 def __init__(self, name): | |
| 349 self.name = name | |
| 350 self.entries = [] | |
| 351 | |
| 352 | |
| 353 class VSSolution: | |
| 354 """ A VS solution. """ | |
| 355 def __init__(self, path=None): | |
| 356 self.path = path | |
| 357 self.projects = [] | |
| 358 self.sections = [] | |
| 359 | |
| 360 @property | |
| 361 def dirpath(self): | |
| 362 return os.path.dirname(self.path) | |
| 363 | |
| 364 def find_project_by_name(self, name, missing_ok=True): | |
| 365 for p in self.projects: | |
| 366 if p.name == name: | |
| 367 return p | |
| 368 if missing_ok: | |
| 369 return None | |
| 370 return MissingVSProjectError(f"Can't find project with name: {name}") | |
| 371 | |
| 372 def find_project_by_path(self, path, missing_ok=True): | |
| 373 for p in self.projects: | |
| 374 if p.abspath == path: | |
| 375 return p | |
| 376 if missing_ok: | |
| 377 return None | |
| 378 raise MissingVSProjectError(f"Can't find project with path: {path}") | |
| 379 | |
| 380 def find_project_by_guid(self, guid, missing_ok=True): | |
| 381 for p in self.projects: | |
| 382 if p.guid == guid: | |
| 383 return p | |
| 384 if missing_ok: | |
| 385 return None | |
| 386 raise MissingVSProjectError(f"Can't find project for guid: {guid}") | |
| 387 | |
| 388 def globalsection(self, name): | |
| 389 for sec in self.sections: | |
| 390 if sec.name == name: | |
| 391 return sec | |
| 392 return None | |
| 393 | |
| 394 def find_project_configuration(self, proj_guid, sln_config): | |
| 395 configs = self.globalsection('ProjectConfigurationPlatforms') | |
| 396 if not configs: | |
| 397 return None | |
| 398 | |
| 399 entry_name = '{%s}.%s.Build.0' % (proj_guid, sln_config) | |
| 400 for entry in configs.entries: | |
| 401 if entry.name == entry_name: | |
| 402 return entry.value | |
| 403 return None | |
| 404 | |
| 405 | |
| 406 _re_sln_project_decl_start = re.compile( | |
| 407 r'^Project\("\{(?P<type>[A-Z0-9\-]+)\}"\) \= ' | |
| 408 r'"(?P<name>[^"]+)", "(?P<path>[^"]+)", "\{(?P<guid>[A-Z0-9\-]+)\}"$') | |
| 409 _re_sln_project_decl_end = re.compile( | |
| 410 r'^EndProject$') | |
| 411 | |
| 412 _re_sln_global_start = re.compile(r'^Global$') | |
| 413 _re_sln_global_end = re.compile(r'^EndGlobal$') | |
| 414 _re_sln_global_section_start = re.compile( | |
| 415 r'^\s*GlobalSection\((?P<name>\w+)\) \= (?P<step>\w+)$') | |
| 416 _re_sln_global_section_end = re.compile(r'^\s*EndGlobalSection$') | |
| 417 | |
| 418 | |
| 419 def parse_sln_file(slnpath): | |
| 420 """ Parses a solution file, returns a solution object. | |
| 421 The projects are not loaded (they will be lazily loaded upon | |
| 422 first access to their items/properties/etc.). | |
| 423 """ | |
| 424 logging.debug(f"Reading {slnpath}") | |
| 425 slnobj = VSSolution(slnpath) | |
| 426 with open(slnpath, 'r') as fp: | |
| 427 lines = fp.readlines() | |
| 428 _parse_sln_file_text(slnobj, lines) | |
| 429 return slnobj | |
| 430 | |
| 431 | |
| 432 def _parse_sln_file_text(slnobj, lines): | |
| 433 until = None | |
| 434 in_global = False | |
| 435 in_global_section = None | |
| 436 | |
| 437 for i, line in enumerate(lines): | |
| 438 if until: | |
| 439 # We need to parse something until a given token, so let's | |
| 440 # do that and ignore everything else. | |
| 441 m = until.search(line) | |
| 442 if m: | |
| 443 until = None | |
| 444 continue | |
| 445 | |
| 446 if in_global: | |
| 447 # We're in the 'global' part of the solution. It should contain | |
| 448 # a bunch of 'global sections' that we need to parse individually. | |
| 449 if in_global_section: | |
| 450 # Keep parsing the current section until we reach the end. | |
| 451 m = _re_sln_global_section_end.search(line) | |
| 452 if m: | |
| 453 in_global_section = None | |
| 454 continue | |
| 455 | |
| 456 ename, evalue = line.strip().split('=') | |
| 457 in_global_section.entries.append(VSGlobalSectionEntry( | |
| 458 ename.strip(), | |
| 459 evalue.strip())) | |
| 460 continue | |
| 461 | |
| 462 m = _re_sln_global_section_start.search(line) | |
| 463 if m: | |
| 464 # Found the start of a new section. | |
| 465 in_global_section = VSGlobalSection(m.group('name')) | |
| 466 logging.debug(f" Adding global section {in_global_section.name}") | |
| 467 slnobj.sections.append(in_global_section) | |
| 468 continue | |
| 469 | |
| 470 m = _re_sln_global_end.search(line) | |
| 471 if m: | |
| 472 # Found the end of the 'global' part. | |
| 473 in_global = False | |
| 474 continue | |
| 475 | |
| 476 # We're not in a specific part of the solution, so do high-level | |
| 477 # parsing. First, ignore root-level comments. | |
| 478 if not line or line[0] == '#': | |
| 479 continue | |
| 480 | |
| 481 m = _re_sln_project_decl_start.search(line) | |
| 482 if m: | |
| 483 # Found the start of a project declaration. | |
| 484 try: | |
| 485 p = VSProject( | |
| 486 m.group('type'), m.group('name'), m.group('path'), | |
| 487 m.group('guid')) | |
| 488 except: | |
| 489 raise Exception(f"Error line {i}: unexpected project syntax.") | |
| 490 logging.debug(f" Adding project {p.name}") | |
| 491 slnobj.projects.append(p) | |
| 492 p._sln = slnobj | |
| 493 | |
| 494 until = _re_sln_project_decl_end | |
| 495 continue | |
| 496 | |
| 497 m = _re_sln_global_start.search(line) | |
| 498 if m: | |
| 499 # Reached the start of the 'global' part, where global sections | |
| 500 # are defined. | |
| 501 in_global = True | |
| 502 continue | |
| 503 | |
| 504 # Ignore the rest (like visual studio version flags). | |
| 505 continue | |
| 506 | |
| 507 | |
| 508 class SolutionCache: | |
| 509 """ A class that contains a VS solution object, along with pre-indexed | |
| 510 lists of items. It's meant to be saved on disk. | |
| 511 """ | |
| 512 VERSION = 3 | |
| 513 | |
| 514 def __init__(self, slnobj): | |
| 515 self.slnobj = slnobj | |
| 516 self.index = None | |
| 517 | |
| 518 def build_cache(self): | |
| 519 self.index = {} | |
| 520 for proj in self.slnobj.projects: | |
| 521 if proj.is_folder: | |
| 522 continue | |
| 523 itemgroup = proj.defaultitemgroup() | |
| 524 if not itemgroup: | |
| 525 continue | |
| 526 | |
| 527 item_cache = set() | |
| 528 self.index[proj.abspath] = item_cache | |
| 529 | |
| 530 for item in itemgroup.get_source_items(): | |
| 531 item_path = proj.get_abs_item_include(item).lower() | |
| 532 item_cache.add(item_path) | |
| 533 | |
| 534 def save(self, path): | |
| 535 pathdir = os.path.dirname(path) | |
| 536 if not os.path.exists(pathdir): | |
| 537 os.makedirs(pathdir) | |
| 538 with open(path, 'wb') as fp: | |
| 539 pickle.dump(self, fp) | |
| 540 | |
| 541 @staticmethod | |
| 542 def load_or_rebuild(slnpath, cachepath): | |
| 543 if cachepath: | |
| 544 res = _try_load_from_cache(slnpath, cachepath) | |
| 545 if res is not None: | |
| 546 return res | |
| 547 | |
| 548 slnobj = parse_sln_file(slnpath) | |
| 549 cache = SolutionCache(slnobj) | |
| 550 | |
| 551 if cachepath: | |
| 552 logger.debug(f"Regenerating cache: {cachepath}") | |
| 553 cache.build_cache() | |
| 554 cache.save(cachepath) | |
| 555 | |
| 556 return (cache, False) | |
| 557 | |
| 558 | |
| 559 def _try_load_from_cache(slnpath, cachepath): | |
| 560 try: | |
| 561 sln_dt = os.path.getmtime(slnpath) | |
| 562 cache_dt = os.path.getmtime(cachepath) | |
| 563 except OSError: | |
| 564 logger.debug("Can't read solution or cache files.") | |
| 565 return None | |
| 566 | |
| 567 # If the solution file is newer, bail out. | |
| 568 if sln_dt >= cache_dt: | |
| 569 logger.debug("Solution is newer than cache.") | |
| 570 return None | |
| 571 | |
| 572 # Our cache is at least valid for the solution stuff. Some of our | |
| 573 # projects might be out of date, but at least there can't be any | |
| 574 # added or removed projects from the solution (otherwise the solution | |
| 575 # file would have been touched). Let's load the cache. | |
| 576 with open(cachepath, 'rb') as fp: | |
| 577 cache = pickle.load(fp) | |
| 578 | |
| 579 # Check that the cache version is up-to-date with this code. | |
| 580 loaded_ver = getattr(cache, 'VERSION') | |
| 581 if loaded_ver != SolutionCache.VERSION: | |
| 582 logger.debug(f"Cache was saved with older format: {cachepath}") | |
| 583 return None | |
| 584 | |
| 585 slnobj = cache.slnobj | |
| 586 | |
| 587 # Check that none of the project files in the solution are newer | |
| 588 # than this cache. | |
| 589 proj_dts = [] | |
| 590 for p in slnobj.projects: | |
| 591 if not p.is_folder: | |
| 592 try: | |
| 593 proj_dts.append(os.path.getmtime(p.abspath)) | |
| 594 except OSError: | |
| 595 logger.debug(f"Found missing project: {p.abspath}") | |
| 596 return None | |
| 597 | |
| 598 if all([cache_dt > pdt for pdt in proj_dts]): | |
| 599 logger.debug(f"Cache is up to date: {cachepath}") | |
| 600 return (cache, True) | |
| 601 | |
| 602 logger.debug("Cache has outdated projects.") | |
| 603 return None |
