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