OLD | NEW |
| (Empty) |
1 #!/usr/bin/python2.4 | |
2 # Copyright 2008, Google Inc. | |
3 # All rights reserved. | |
4 # | |
5 # Redistribution and use in source and binary forms, with or without | |
6 # modification, are permitted provided that the following conditions are | |
7 # met: | |
8 # | |
9 # * Redistributions of source code must retain the above copyright | |
10 # notice, this list of conditions and the following disclaimer. | |
11 # * Redistributions in binary form must reproduce the above | |
12 # copyright notice, this list of conditions and the following disclaimer | |
13 # in the documentation and/or other materials provided with the | |
14 # distribution. | |
15 # * Neither the name of Google Inc. nor the names of its | |
16 # contributors may be used to endorse or promote products derived from | |
17 # this software without specific prior written permission. | |
18 # | |
19 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
20 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
21 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
22 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
23 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
24 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
25 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
26 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
27 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
28 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
29 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
30 | |
31 """Visual Studio solution output of component targets for SCons.""" | |
32 | |
33 import copy | |
34 import md5 | |
35 import os | |
36 import sys | |
37 import xml.dom | |
38 import xml.dom.minidom | |
39 import SCons | |
40 | |
41 | |
42 #------------------------------------------------------------------------------ | |
43 | |
44 | |
45 def MakeGuid(name, seed='component_targets_msvs'): | |
46 """Returns a GUID for the specified target name. | |
47 | |
48 Args: | |
49 name: Target name. | |
50 seed: Seed for MD5 hash. | |
51 Returns: | |
52 A GUID-line string calculated from the name and seed. | |
53 | |
54 This generates something which looks like a GUID, but depends only on the | |
55 name and seed. This means the same name/seed will always generate the same | |
56 GUID, so that projects and solutions which refer to each other can explicitly | |
57 determine the GUID to refer to explicitly. It also means that the GUID will | |
58 not change when the project for a target is rebuilt. | |
59 """ | |
60 # Calculate a MD5 signature for the seed and name. | |
61 d = md5.new(str(seed) + str(name)).hexdigest().upper() | |
62 # Convert most of the signature to GUID form (discard the rest) | |
63 guid = ('{' + d[:8] + '-' + d[8:12] + '-' + d[12:16] + '-' + d[16:20] | |
64 + '-' + d[20:32] + '}') | |
65 return guid | |
66 | |
67 #------------------------------------------------------------------------------ | |
68 | |
69 | |
70 def GetGuidAndNameFromVSProject(project_path): | |
71 """Reads the GUID from a Visual Studio project file. | |
72 | |
73 Args: | |
74 project_path: Path to the Visual Studio project file. | |
75 | |
76 Returns: | |
77 The GUID string from the file. | |
78 The project name from the file. | |
79 """ | |
80 doc = xml.dom.minidom.parse(project_path) | |
81 try: | |
82 n_root = doc.documentElement | |
83 if n_root.nodeName != 'VisualStudioProject': | |
84 raise SCons.Errors.UserError('%s is not a Visual Studio project.' % | |
85 project_path) | |
86 return ( | |
87 str(n_root.attributes['ProjectGUID'].nodeValue), | |
88 str(n_root.attributes['Name'].nodeValue), | |
89 ) | |
90 finally: | |
91 # Clean up doc | |
92 doc.unlink() | |
93 | |
94 #------------------------------------------------------------------------------ | |
95 | |
96 | |
97 class VSProjectWriter(object): | |
98 """Visual Studio XML project writer.""" | |
99 | |
100 def __init__(self, project_path): | |
101 """Initializes the project. | |
102 | |
103 Args: | |
104 project_path: Path to the project file. | |
105 """ | |
106 self.project_path = project_path | |
107 self.doc = None | |
108 | |
109 def Create(self, name): | |
110 """Creates the project document. | |
111 | |
112 Args: | |
113 name: Name of the project. | |
114 """ | |
115 self.name = name | |
116 self.guid = MakeGuid(name) | |
117 | |
118 # Create XML doc | |
119 xml_impl = xml.dom.getDOMImplementation() | |
120 self.doc = xml_impl.createDocument(None, 'VisualStudioProject', None) | |
121 | |
122 # Add attributes to root element | |
123 self.n_root = self.doc.documentElement | |
124 self.n_root.setAttribute('ProjectType', 'Visual C++') | |
125 self.n_root.setAttribute('Version', '8.00') | |
126 self.n_root.setAttribute('Name', self.name) | |
127 self.n_root.setAttribute('ProjectGUID', self.guid) | |
128 self.n_root.setAttribute('RootNamespace', self.name) | |
129 self.n_root.setAttribute('Keyword', 'MakeFileProj') | |
130 | |
131 # Add platform list | |
132 n_platform = self.doc.createElement('Platforms') | |
133 self.n_root.appendChild(n_platform) | |
134 n = self.doc.createElement('Platform') | |
135 n.setAttribute('Name', 'Win32') | |
136 n_platform.appendChild(n) | |
137 | |
138 # Add empty ToolFiles section | |
139 self.n_root.appendChild(self.doc.createElement('ToolFiles')) | |
140 | |
141 # Add configurations section | |
142 self.n_configs = self.doc.createElement('Configurations') | |
143 self.n_root.appendChild(self.n_configs) | |
144 | |
145 # Add files section | |
146 self.n_files = self.doc.createElement('Files') | |
147 self.n_root.appendChild(self.n_files) | |
148 | |
149 # Add empty Globals section | |
150 self.n_root.appendChild(self.doc.createElement('Globals')) | |
151 | |
152 def AddConfig(self, name, attrs, tool_attrs): | |
153 """Adds a configuration to the project. | |
154 | |
155 Args: | |
156 name: Configuration name. | |
157 attrs: Dict of configuration attributes. | |
158 tool_attrs: Dict of tool attributes. | |
159 """ | |
160 # Add configuration node | |
161 n_config = self.doc.createElement('Configuration') | |
162 n_config.setAttribute('Name', '%s|Win32' % name) | |
163 n_config.setAttribute('ConfigurationType', '0') | |
164 for k, v in attrs.items(): | |
165 n_config.setAttribute(k, v) | |
166 self.n_configs.appendChild(n_config) | |
167 | |
168 # Add tool node | |
169 n_tool = self.doc.createElement('Tool') | |
170 n_tool.setAttribute('Name', 'VCNMakeTool') | |
171 n_tool.setAttribute('IncludeSearchPath', '') | |
172 n_tool.setAttribute('ForcedIncludes', '') | |
173 n_tool.setAttribute('AssemblySearchPath', '') | |
174 n_tool.setAttribute('ForcedUsingAssemblies', '') | |
175 n_tool.setAttribute('CompileAsManaged', '') | |
176 n_tool.setAttribute('PreprocessorDefinitions', '') | |
177 for k, v in tool_attrs.items(): | |
178 n_tool.setAttribute(k, v) | |
179 n_config.appendChild(n_tool) | |
180 | |
181 def _WalkFolders(self, folder_dict, parent): | |
182 """Recursively walks the folder tree. | |
183 | |
184 Args: | |
185 folder_dict: Dict of folder entries. Entry is | |
186 either subdir_name:subdir_dict or relative_path_to_file:None. | |
187 parent: Parent node (folder node for that dict) | |
188 """ | |
189 entries = folder_dict.keys() | |
190 entries.sort() | |
191 for e in entries: | |
192 if folder_dict[e]: | |
193 # Folder | |
194 n_subfolder = self.doc.createElement('Filter') | |
195 n_subfolder.setAttribute('Name', e) | |
196 parent.appendChild(n_subfolder) | |
197 self._WalkFolders(folder_dict[e], n_subfolder) | |
198 else: | |
199 # File | |
200 n_file = self.doc.createElement('File') | |
201 n_file.setAttribute('RelativePath', e) | |
202 parent.appendChild(n_file) | |
203 | |
204 def AddFiles(self, name, files_dict): | |
205 """Adds files to the project. | |
206 | |
207 Args: | |
208 name: Name of the folder. If None, files/folders will be added directly | |
209 to the files list. | |
210 files_dict: A dict of files / folders. | |
211 | |
212 Within the files_dict: | |
213 * A file entry is relative_path:None | |
214 * A folder entry is folder_name:files_dict, where files_dict is another | |
215 dict of this form. | |
216 """ | |
217 # Create subfolder if necessary | |
218 if name: | |
219 n_folder = self.doc.createElement('Filter') | |
220 n_folder.setAttribute('Name', name) | |
221 self.n_files.appendChild(n_folder) | |
222 else: | |
223 n_folder = self.n_files | |
224 | |
225 # Recursively add files to the folder | |
226 self._WalkFolders(files_dict, n_folder) | |
227 | |
228 def Write(self): | |
229 """Writes the project file.""" | |
230 f = open(self.project_path, 'wt') | |
231 self.doc.writexml(f, encoding='Windows-1252', addindent=' ', newl='\n') | |
232 f.close() | |
233 | |
234 #------------------------------------------------------------------------------ | |
235 | |
236 | |
237 def ComponentVSProjectBuilder(target, source, env): | |
238 """Visual Studio project builder. | |
239 | |
240 Args: | |
241 target: Destination file. | |
242 source: List of sources to be added to the target. | |
243 env: Environment context. | |
244 | |
245 Returns: | |
246 Zero if successful. | |
247 """ | |
248 source = source # Silence gpylint | |
249 | |
250 target_name = env['TARGET_NAME'] | |
251 project_file = target[0].path | |
252 project_to_main = env.RelativePath(target[0].dir, env.Dir('$MAIN_DIR'), | |
253 sep='/') | |
254 | |
255 env_hammer_bat = env.Clone(VS_PROJECT_TO_MAIN_DIR=project_to_main) | |
256 hammer_bat = env_hammer_bat.subst('$COMPONENT_VS_PROJECT_SCRIPT_PATH', raw=1) | |
257 | |
258 vsp = VSProjectWriter(project_file) | |
259 vsp.Create(target_name) | |
260 | |
261 # Add configuration per build mode supported by this target | |
262 target_path = env['TARGET_PATH'] | |
263 for mode, path in target_path.items(): | |
264 attrs = {} | |
265 attrs['OutputDirectory'] = '$(ProjectDir)/%s/%s/out' % (mode, target_name) | |
266 attrs['IntermediateDirectory'] = ('$(ProjectDir)/%s/%s/tmp' % | |
267 (mode, target_name)) | |
268 | |
269 tool_attrs = {} | |
270 if path: | |
271 tool_attrs['Output'] = env.RelativePath(target[0].dir, | |
272 env.Entry(path), sep='/') | |
273 build_cmd = '%s --mode=%s %s' % (hammer_bat, mode, target_name) | |
274 clean_cmd = '%s --mode=%s -c %s' % (hammer_bat, mode, target_name) | |
275 tool_attrs['BuildCommandLine'] = build_cmd | |
276 tool_attrs['CleanCommandLine'] = clean_cmd | |
277 tool_attrs['ReBuildCommandLine'] = clean_cmd + ' && ' + build_cmd | |
278 | |
279 vsp.AddConfig(mode, attrs, tool_attrs) | |
280 | |
281 # TODO(rspangler): Fill in files - at least, the .scons file invoking the | |
282 # target. | |
283 | |
284 # Write project | |
285 vsp.Write() | |
286 return 0 | |
287 | |
288 | |
289 def ComponentVSProject(self, target_name, **kwargs): | |
290 """Visual Studio project pseudo-builder for the specified target. | |
291 | |
292 Args: | |
293 self: Environment context. | |
294 target_name: Name of the target. | |
295 kwargs: Optional keyword arguments override environment variables in the | |
296 derived environment used to create the project. | |
297 | |
298 Returns: | |
299 A list of output nodes. | |
300 """ | |
301 # Builder only works on Windows | |
302 if sys.platform not in ('win32', 'cygwin'): | |
303 return [] | |
304 | |
305 # Clone environment and add keyword args | |
306 env = self.Clone() | |
307 for k, v in kwargs.items(): | |
308 env[k] = v | |
309 | |
310 # Save the target name | |
311 env['TARGET_NAME'] = target_name | |
312 | |
313 # Extract the target properties and save in the environment for use by the | |
314 # real builder. | |
315 t = GetTargets().get(target_name) | |
316 env['TARGET_PATH'] = {} | |
317 if t: | |
318 for mode, mode_properties in t.mode_properties.items(): | |
319 # Since the target path is what Visual Studio will run, use the EXE | |
320 # property in preference to TARGET_PATH. | |
321 target_path = mode_properties.get('EXE', | |
322 mode_properties.get('TARGET_PATH')) | |
323 env.Append(TARGET_PATH={mode: target_path}) | |
324 else: | |
325 # No modes declared for this target. Could be a custom alias created by | |
326 # a SConscript, rather than a component builder. Assume it can be built in | |
327 # all modes, but produces no output. | |
328 for mode in GetTargetModes(): | |
329 env.Append(TARGET_PATH={mode: None}) | |
330 | |
331 # Call the real builder | |
332 return env.ComponentVSProjectBuilder( | |
333 '$COMPONENT_VS_PROJECT_DIR/${TARGET_NAME}', []) | |
334 | |
335 #------------------------------------------------------------------------------ | |
336 | |
337 | |
338 class SourceWalker(object): | |
339 """Iterator for walking a node tree and collecting sources. | |
340 | |
341 This is depth-first, children are visited before the parent. The object | |
342 can be initialized with any node, and returns the next node on the descent | |
343 with each Next() call. | |
344 | |
345 This class does not get caught in node cycles caused, for example, by C | |
346 header file include loops. | |
347 | |
348 Based on SCons.Node.Walker. | |
349 """ | |
350 | |
351 def __init__(self, node, seen, print_interval = 1000): | |
352 """Initializes the source walker. | |
353 | |
354 Args: | |
355 node: Node to start walk from. | |
356 seen: Set to add seen nodes to, if not None. | |
357 print_interval: Interval in nodes examined at which to print a progress | |
358 indicator. | |
359 """ | |
360 self.interval = print_interval | |
361 # Put node on stack | |
362 self.stack = [node] | |
363 # Scan for node's children, then copy them to node.wkids | |
364 node.wkids = copy.copy(node.children(scan=1)) | |
365 # Keep track of nodes we've seen (returned) | |
366 if seen is None: | |
367 seen = set() | |
368 self.seen = seen | |
369 # Add node to history for cycle detection | |
370 self.history = set() | |
371 self.history.add(node) | |
372 # We've seen one node now | |
373 self.nodes_examined = 1 | |
374 self.unique_nodes = 1 | |
375 | |
376 | |
377 def Next(self): | |
378 """Get the next node for this walk of the tree. | |
379 | |
380 Returns: | |
381 The next node, or None if no more nodes. | |
382 | |
383 This function is intentionally iterative, not recursive, to sidestep any | |
384 issues of stack size limitations. | |
385 """ | |
386 while self.stack: | |
387 if self.stack[-1].wkids: | |
388 # Node has children we haven't examined, so iterate into the first | |
389 # child | |
390 node = self.stack[-1].wkids.pop(0) | |
391 if not self.stack[-1].wkids: | |
392 # No more children of this node | |
393 self.stack[-1].wkids = None | |
394 self.nodes_examined += 1 | |
395 if self.interval and not self.nodes_examined % self.interval: | |
396 self.PrintProgress() | |
397 if (node not in self.history) and (node not in self.seen): | |
398 # Haven't hit a cycle or a node we've already seen | |
399 node.wkids = copy.copy(node.children(scan=1)) | |
400 self.stack.append(node) | |
401 self.history.add(node) | |
402 else: | |
403 # Coming back from iterating, so return the next node on the stack. | |
404 node = self.stack.pop() | |
405 self.history.remove(node) | |
406 self.seen.add(node) | |
407 self.unique_nodes += 1 | |
408 return node | |
409 return None | |
410 | |
411 def PrintProgress(self): | |
412 """Prints a progress indicator.""" | |
413 print ' Examined %d nodes, found %d unique...' % ( | |
414 self.nodes_examined, self.unique_nodes) | |
415 | |
416 def WalkAll(self): | |
417 """Walks all nodes in the the tree.""" | |
418 while self.Next(): | |
419 pass | |
420 if self.interval: | |
421 self.PrintProgress() | |
422 | |
423 | |
424 def ComponentVSSourceProjectBuilder(target, source, env): | |
425 """Visual Studio source project builder. | |
426 | |
427 Args: | |
428 target: Destination file. | |
429 source: List of sources to be added to the target. | |
430 env: Environment context. | |
431 | |
432 Returns: | |
433 Zero if successful. | |
434 """ | |
435 source = source # Silence gpylint | |
436 | |
437 target_name = env['PROJECT_NAME'] | |
438 project_file = target[0].path | |
439 project_dir = target[0].dir | |
440 | |
441 # Get list of suffixes to include | |
442 suffixes = env.SubstList2('$COMPONENT_VS_SOURCE_SUFFIXES') | |
443 | |
444 # Convert source folders to absolute paths | |
445 folders = [] | |
446 for f in env['COMPONENT_VS_SOURCE_FOLDERS']: | |
447 # (folder name, folder abspath, dict of contents) | |
448 folders.append((f[0], env.Dir(f[1]).abspath, {})) | |
449 | |
450 # TODO(rspangler): Additional enhancements: | |
451 # * Should be able to specify paths in folder name (i.e., foo/bar) and | |
452 # create the nested folder nodes ('foo' and 'bar') | |
453 # * Should be tolerant of a folder being specified more than once with | |
454 # the same name (probably necessary to support nested folder nodes anyway) | |
455 # Can probably accomplish both of those by creating a parent fodler dict and | |
456 # calling WalkFolders() only once. | |
457 # Create a temporary solution alias to point to all the targets, so we can | |
458 # make a single call to SourceWalker() | |
459 tmp_alias = env.Alias('vs_source_project_' + target_name, | |
460 map(env.Alias, env['COMPONENT_VS_SOURCE_TARGETS'])) | |
461 | |
462 # Scan all targets and add unique nodes to set of sources | |
463 print ' Scanning dependency tree ...' | |
464 all_srcs = set() | |
465 walker = SourceWalker(tmp_alias[0], all_srcs) | |
466 walker.WalkAll() | |
467 | |
468 # Walk all sources and build directory trees | |
469 print ' Building source tree...' | |
470 for n in all_srcs: | |
471 if not hasattr(n, 'isfile') or not n.isfile(): | |
472 continue # Not a file | |
473 if n.has_builder(): | |
474 continue # Not a leaf node | |
475 if n.suffix not in suffixes: | |
476 continue # Not a file type we include | |
477 | |
478 path = n.rfile().abspath | |
479 for f in folders: | |
480 if path.startswith(f[1]): | |
481 if f[0] is None: | |
482 # Folder name of None is a filter | |
483 break | |
484 relpath = path[len(f[1]) + 1:].split(os.sep) | |
485 folder_dict = f[2] | |
486 # Recursively add subdirs | |
487 for pathseg in relpath[:-1]: | |
488 if pathseg not in folder_dict: | |
489 folder_dict[pathseg] = {} | |
490 folder_dict = folder_dict[pathseg] | |
491 # Add file to last subdir. No dict, since this isn't a subdir | |
492 folder_dict[env.RelativePath(project_dir, path)] = None | |
493 break | |
494 | |
495 print ' Writing project file...' | |
496 | |
497 vsp = VSProjectWriter(project_file) | |
498 vsp.Create(target_name) | |
499 | |
500 # One configuration for all build modes | |
501 vsp.AddConfig('all', {}, {}) | |
502 | |
503 # Add files | |
504 for f in folders: | |
505 if f[0] is None: | |
506 continue # Skip filters | |
507 vsp.AddFiles(f[0], f[2]) | |
508 | |
509 vsp.Write() | |
510 return 0 | |
511 | |
512 | |
513 def ComponentVSSourceProject(self, project_name, target_names, **kwargs): | |
514 """Visual Studio source project pseudo-builder. | |
515 | |
516 Args: | |
517 self: Environment context. | |
518 project_name: Name of the project. | |
519 target_names: List of target names to include source for. | |
520 kwargs: Optional keyword arguments override environment variables in the | |
521 derived environment used to create the project. | |
522 | |
523 Returns: | |
524 A list of output nodes. | |
525 """ | |
526 # Builder only works on Windows | |
527 if sys.platform not in ('win32', 'cygwin'): | |
528 return [] | |
529 | |
530 # Clone environment and add keyword args | |
531 env = self.Clone() | |
532 for k, v in kwargs.items(): | |
533 env[k] = v | |
534 | |
535 # Save the project name and targets | |
536 env['PROJECT_NAME'] = project_name | |
537 env['COMPONENT_VS_SOURCE_TARGETS'] = target_names | |
538 | |
539 # Call the real builder | |
540 return env.ComponentVSSourceProjectBuilder( | |
541 '$COMPONENT_VS_PROJECT_DIR/${PROJECT_NAME}', []) | |
542 | |
543 #------------------------------------------------------------------------------ | |
544 | |
545 | |
546 def FindSources(env, dest, source, suffixes=None): | |
547 """Recursively finds sources and adds them to the destination set. | |
548 | |
549 Args: | |
550 env: Environment context. | |
551 dest: Set to add source nodes to. | |
552 source: Source file(s) to find. May be a string, Node, or a list of | |
553 mixed strings or Nodes. Strings will be passed through env.Glob() to | |
554 evaluate wildcards. If a source evaluates to a directory, the entire | |
555 directory will be recursively added. | |
556 suffixes: List of suffixes to include. If not None, only files with these | |
557 suffixes will be added to dest. | |
558 """ | |
559 for source_entry in env.Flatten(source): | |
560 if type(source_entry) == str: | |
561 # Search for matches for each source entry | |
562 source_nodes = env.Glob(source_entry) | |
563 else: | |
564 # Source entry is already a file or directory node; no need to glob it | |
565 source_nodes = [source_entry] | |
566 for s in source_nodes: | |
567 if str(s.__class__) == 'SCons.Node.FS.Dir': | |
568 # Recursively search subdir. Since glob('*') doesn't match dot files, | |
569 # also glob('.*'). | |
570 FindSources(env, dest, [s.abspath + '/*', s.abspath + '/.*'], suffixes) | |
571 elif suffixes and s.suffix in suffixes: | |
572 dest.add(s) | |
573 | |
574 | |
575 def ComponentVSDirProjectBuilder(target, source, env): | |
576 """Visual Studio directory project builder. | |
577 | |
578 Args: | |
579 target: Destination file. | |
580 source: List of sources to be added to the target. | |
581 env: Environment context. | |
582 | |
583 Returns: | |
584 Zero if successful. | |
585 """ | |
586 source = source # Silence gpylint | |
587 | |
588 target_name = env['PROJECT_NAME'] | |
589 project_file = target[0].path | |
590 project_dir = target[0].dir | |
591 | |
592 # Convert source folders to absolute paths | |
593 folders = [] | |
594 for f in env['COMPONENT_VS_SOURCE_FOLDERS']: | |
595 # (folder name, folder abspath, dict of contents) | |
596 folders.append((f[0], env.Dir(f[1]).abspath, {})) | |
597 | |
598 # Recursively scan source directories | |
599 print ' Scanning directories for source...' | |
600 all_srcs = set() | |
601 FindSources(env, all_srcs, env['PROJECT_SOURCES'], | |
602 suffixes=env.SubstList2('$COMPONENT_VS_SOURCE_SUFFIXES')) | |
603 | |
604 # Walk all sources and build directory trees | |
605 print ' Building source tree...' | |
606 for n in all_srcs: | |
607 # Map addRepository'd source to its real location. | |
608 path = n.rfile().abspath | |
609 for f in folders: | |
610 if path.startswith(f[1]): | |
611 if f[0] is None: | |
612 # Folder name of None is a filter | |
613 break | |
614 relpath = path[len(f[1]) + 1:].split(os.sep) | |
615 folder_dict = f[2] | |
616 # Recursively add subdirs | |
617 for pathseg in relpath[:-1]: | |
618 if pathseg not in folder_dict: | |
619 folder_dict[pathseg] = {} | |
620 folder_dict = folder_dict[pathseg] | |
621 # Add file to last subdir. No dict, since this isn't a subdir | |
622 folder_dict[env.RelativePath(project_dir, path)] = None | |
623 break | |
624 | |
625 print ' Writing project file...' | |
626 | |
627 vsp = VSProjectWriter(project_file) | |
628 vsp.Create(target_name) | |
629 | |
630 # One configuration for all build modes | |
631 vsp.AddConfig('all', {}, {}) | |
632 | |
633 # Add files | |
634 for f in folders: | |
635 if f[0] is None: | |
636 continue # Skip filters | |
637 vsp.AddFiles(f[0], f[2]) | |
638 | |
639 vsp.Write() | |
640 return 0 | |
641 | |
642 | |
643 def ComponentVSDirProject(self, project_name, source, **kwargs): | |
644 """Visual Studio directory project pseudo-builder. | |
645 | |
646 Args: | |
647 self: Environment context. | |
648 project_name: Name of the project. | |
649 source: List of source files and/or directories. | |
650 kwargs: Optional keyword arguments override environment variables in the | |
651 derived environment used to create the project. | |
652 | |
653 Returns: | |
654 A list of output nodes. | |
655 """ | |
656 # Builder only works on Windows | |
657 if sys.platform not in ('win32', 'cygwin'): | |
658 return [] | |
659 | |
660 # Clone environment and add keyword args | |
661 env = self.Clone() | |
662 for k, v in kwargs.items(): | |
663 env[k] = v | |
664 | |
665 # Save the project name and sources | |
666 env['PROJECT_NAME'] = project_name | |
667 env['PROJECT_SOURCES'] = source | |
668 | |
669 # Call the real builder | |
670 return env.ComponentVSDirProjectBuilder( | |
671 '$COMPONENT_VS_PROJECT_DIR/${PROJECT_NAME}', []) | |
672 | |
673 #------------------------------------------------------------------------------ | |
674 | |
675 | |
676 def ComponentVSSolutionBuilder(target, source, env): | |
677 """Visual Studio solution builder. | |
678 | |
679 Args: | |
680 target: Destination file. | |
681 source: List of sources to be added to the target. | |
682 env: Environment context. | |
683 | |
684 Returns: | |
685 Zero if successful. | |
686 """ | |
687 source = source # Silence gpylint | |
688 | |
689 solution_file = target[0].path | |
690 projects = env['SOLUTION_PROJECTS'] | |
691 folders = env['SOLUTION_FOLDERS'] | |
692 | |
693 # Scan externally-generated projects | |
694 external_projects = [] | |
695 for p in source: | |
696 guid, name = GetGuidAndNameFromVSProject(p.abspath) | |
697 external_projects.append((p, name, guid)) | |
698 | |
699 f = open(solution_file, 'wt') | |
700 f.write('Microsoft Visual Studio Solution File, Format Version 9.00\n') | |
701 f.write('# Visual Studio 2005\n') | |
702 | |
703 # Projects generated by ComponentVSSolution() | |
704 for p in projects: | |
705 project_file = env.File( | |
706 '$COMPONENT_VS_PROJECT_DIR/%s$COMPONENT_VS_PROJECT_SUFFIX' % p) | |
707 f.write('Project("%s") = "%s", "%s", "%s"\nEndProject\n' % ( | |
708 '{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}', # External makefile GUID | |
709 p, # Project name | |
710 env.RelativePath(target[0].dir, project_file), # Path to project file | |
711 MakeGuid(p), # Project GUID | |
712 )) | |
713 | |
714 # Projects generated elsewhere | |
715 for p, name, guid in external_projects: | |
716 f.write('Project("%s") = "%s", "%s", "%s"\nEndProject\n' % ( | |
717 # TODO(rspangler): What if this project isn't type external makefile? | |
718 # How to tell what type it is? | |
719 '{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}', # External makefile GUID | |
720 name, # Project name | |
721 env.RelativePath(target[0].dir, p), # Path to project file | |
722 guid, # Project GUID | |
723 )) | |
724 | |
725 # Folders from build groups | |
726 # TODO(rspangler): Currently no way to add external project (specified in | |
727 # sources) to a solution folder. | |
728 for folder in folders: | |
729 f.write('Project("%s") = "%s", "%s", "%s"\nEndProject\n' % ( | |
730 '{2150E333-8FDC-42A3-9474-1A3956D46DE8}', # Solution folder GUID | |
731 folder, # Folder name | |
732 folder, # Folder name (again) | |
733 # Use a different seed so the folder won't get the same GUID as a | |
734 # project. | |
735 MakeGuid(folder, seed='folder'), # Project GUID | |
736 )) | |
737 | |
738 f.write('\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n') | |
739 for mode in GetTargetModes(): | |
740 f.write('\t\t%s|Win32 = %s|Win32\n' % (mode, mode)) | |
741 f.write('\tEndGlobalSection\n') | |
742 | |
743 # Determine which projects should be enabled | |
744 # TODO(rspangler): This is somewhat clunky. DEFAULT_TARGETS is global, and | |
745 # what we really need is something mode-specific. In theory we could make | |
746 # this a mode-specific dict rather than a list, but that'd also be a pain to | |
747 # populate. | |
748 # These variable names are also getting REALLY long. Perhaps we should | |
749 # define shorter ones (with the default value redirecting to the longer | |
750 # ones for legacy compatibility). | |
751 enable_projects = env.SubstList2('$COMPONENT_VS_ENABLED_PROJECTS') | |
752 | |
753 f.write('\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n') | |
754 | |
755 # Projects generated by ComponentVSSolution() | |
756 for p in projects: | |
757 for mode in GetTargetModes(): | |
758 f.write('\t\t%s.%s|Win32.ActiveCfg = %s|Win32\n' % ( | |
759 MakeGuid(p), # Project GUID | |
760 mode, # Solution build configuration | |
761 mode, # Project build config for that solution config | |
762 )) | |
763 | |
764 t = GetTargets().get(p) | |
765 | |
766 # Determine if project should be enabled in this mode | |
767 enabled = t and mode in t.mode_properties | |
768 if enable_projects and p not in enable_projects: | |
769 # Enable list specified, but target isn't in it | |
770 # TODO(rspangler): Since we env.Default(scons-out) elsewhere, this | |
771 # likely causes all projects to be disabled by default. But that's | |
772 # realistically better than enabling them all... | |
773 enabled = False | |
774 | |
775 if enabled: | |
776 # Target can be built in this mode | |
777 f.write('\t\t%s.%s|Win32.Build.0 = %s|Win32\n' % ( | |
778 MakeGuid(p), # Project GUID | |
779 mode, # Solution build configuration | |
780 mode, # Project build config for that solution config | |
781 )) | |
782 | |
783 # Projects generated elsewhere | |
784 for p, name, guid in external_projects: | |
785 for mode in GetTargetModes(): | |
786 f.write('\t\t%s.%s|Win32.ActiveCfg = %s|Win32\n' % ( | |
787 guid, # Project GUID | |
788 mode, # Solution build configuration | |
789 mode, # Project build config for that solution config | |
790 )) | |
791 | |
792 if name in enable_projects or not enable_projects: | |
793 # Build target in this mode | |
794 f.write('\t\t%s.%s|Win32.Build.0 = %s|Win32\n' % ( | |
795 guid, # Project GUID | |
796 mode, # Solution build configuration | |
797 mode, # Project build config for that solution config | |
798 )) | |
799 | |
800 f.write('\tEndGlobalSection\n') | |
801 | |
802 f.write('\tGlobalSection(SolutionProperties) = preSolution\n') | |
803 f.write('\t\tHideSolutionNode = FALSE\n') | |
804 f.write('\tEndGlobalSection\n') | |
805 | |
806 if folders: | |
807 f.write('\tGlobalSection(NestedProjects) = preSolution\n') | |
808 for p, folder in projects.items(): | |
809 f.write('\t\t%s = %s\n' % (MakeGuid(p), MakeGuid(folder, seed='folder'))) | |
810 f.write('\tEndGlobalSection\n') | |
811 | |
812 f.write('EndGlobal\n') | |
813 f.close() | |
814 | |
815 return 0 | |
816 | |
817 | |
818 def ComponentVSSolution(self, solution_name, target_names, projects=None, | |
819 **kwargs): | |
820 """Visual Studio solution pseudo-builder. | |
821 | |
822 Args: | |
823 self: Environment context. | |
824 solution_name: Name of the solution. | |
825 target_names: Names of targets or target groups to include in the solution. | |
826 This will automatically build projects for them. | |
827 projects: List of aditional projects not generated by this solution to | |
828 include in the solution. | |
829 kwargs: Optional keyword arguments override environment variables in the | |
830 derived environment used to create the solution. | |
831 | |
832 Returns: | |
833 The list of output nodes. | |
834 """ | |
835 # TODO(rspangler): Should have option to build source project also. Perhaps | |
836 # select using a --source_project option, since it needs to use gather_inputs | |
837 # to scan the DAG and will blow up the null build time. | |
838 # TODO(rspangler): Should also have autobuild_projects option. If false, | |
839 # don't build them. | |
840 # TODO(rspangler): Should also be able to specify a target group directly | |
841 # (i.e. 'run_all_tests') | |
842 | |
843 # Builder only works on Windows | |
844 if sys.platform not in ('win32', 'cygwin'): | |
845 return [] | |
846 | |
847 # Clone environment and add keyword args | |
848 env = self.Clone() | |
849 for k, v in kwargs.items(): | |
850 env[k] = v | |
851 | |
852 # Save the target name | |
853 env['SOLUTION_NAME'] = solution_name | |
854 | |
855 # Get list of targets to make projects for. At this point we haven't | |
856 # determined whether they're groups or targets. | |
857 target_names = env.SubstList2(target_names) | |
858 env['SOLUTION_TARGETS'] = target_names | |
859 | |
860 # Save the default targets list as an environment variable | |
861 env['COMPONENT_VS_SCONS_DEFAULT_TARGETS'] = SCons.Script.DEFAULT_TARGETS | |
862 | |
863 # Expand target_names into project names, and create project-to-folder | |
864 # mappings | |
865 project_names = {} | |
866 folders = [] | |
867 if target_names: | |
868 # Expand target_names into project names | |
869 for target in target_names: | |
870 if target in GetTargetGroups(): | |
871 # Add target to folders | |
872 folders.append(target) | |
873 # Add all project_names in the group | |
874 for t in GetTargetGroups()[target].GetTargetNames(): | |
875 project_names[t] = target | |
876 elif target in GetTargets(): | |
877 # Just a target name | |
878 project_names[target] = None | |
879 else: | |
880 print 'Warning: ignoring unknown target "%s"' % target | |
881 else: | |
882 # No target names specified, so use all projects | |
883 for t in GetTargets(): | |
884 project_names[t] = None | |
885 | |
886 env['SOLUTION_FOLDERS'] = folders | |
887 env['SOLUTION_PROJECTS'] = project_names | |
888 | |
889 # Call the real builder | |
890 out_nodes = env.ComponentVSSolutionBuilder( | |
891 '$COMPONENT_VS_SOLUTION_DIR/${SOLUTION_NAME}', projects or []) | |
892 | |
893 # Call the real builder for the projects we generate | |
894 for p in project_names: | |
895 out_nodes += env.ComponentVSProject(p) | |
896 | |
897 # Add the solution target | |
898 # TODO(rspangler): Should really defer the rest of the work above, since | |
899 # otherwise we can't build a solution which has a target to rebuild itself. | |
900 env.Alias('all_solutions', env.Alias(solution_name, out_nodes)) | |
901 | |
902 # TODO(rspangler): To rebuild the solution properly, need to override its | |
903 # project configuration so it only has '--mode=all' (or some other way of | |
904 # setting the subset of modes which it should use to rebuild itself). | |
905 # Rebuilding with the property below will strip it down to just the current | |
906 # build mode, which isn't what we want. | |
907 # Let component_targets know this target is available in the current mode | |
908 #self.SetTargetProperty(solution_name, TARGET_PATH=out_nodes[0]) | |
909 | |
910 return out_nodes | |
911 | |
912 #------------------------------------------------------------------------------ | |
913 | |
914 | |
915 def generate(env): | |
916 # NOTE: SCons requires the use of this name, which fails gpylint. | |
917 """SCons entry point for this tool.""" | |
918 | |
919 # Add pseudo-builders to set up the project and solution builders. These | |
920 # need to be available on all platforms so that SConscripts which reference | |
921 # them will work. | |
922 env.AddMethod(ComponentVSDirProject) | |
923 env.AddMethod(ComponentVSProject) | |
924 env.AddMethod(ComponentVSSolution) | |
925 env.AddMethod(ComponentVSSourceProject) | |
926 | |
927 # If not on Windows, don't do anything else | |
928 if sys.platform not in ('win32', 'cygwin'): | |
929 return | |
930 | |
931 # Include tools we need | |
932 env.Tool('gather_inputs') | |
933 | |
934 env.SetDefault( | |
935 COMPONENT_VS_PROJECT_DIR='$COMPONENT_VS_SOLUTION_DIR/projects', | |
936 COMPONENT_VS_PROJECT_SCRIPT_NAME = 'hammer.bat', | |
937 COMPONENT_VS_PROJECT_SCRIPT_PATH = ( | |
938 '$$(ProjectDir)/$VS_PROJECT_TO_MAIN_DIR/' | |
939 '$COMPONENT_VS_PROJECT_SCRIPT_NAME'), | |
940 COMPONENT_VS_PROJECT_SUFFIX='.vcproj', | |
941 | |
942 COMPONENT_VS_SOLUTION_DIR='$DESTINATION_ROOT/solution', | |
943 COMPONENT_VS_SOLUTION_SUFFIX='.sln', | |
944 COMPONENT_VS_ENABLED_PROJECTS=['$COMPONENT_VS_SCONS_DEFAULT_TARGETS'], | |
945 | |
946 COMPONENT_VS_SOURCE_SUFFIXES=['$CPPSUFFIXES', '.rc', '.scons'], | |
947 COMPONENT_VS_SOURCE_FOLDERS=[('source', '$MAIN_DIR')], | |
948 ) | |
949 | |
950 AddTargetGroup('all_solutions', 'solutions can be built') | |
951 | |
952 # Add builders | |
953 vcprojaction = SCons.Script.Action(ComponentVSProjectBuilder, varlist=[ | |
954 'COMPONENT_VS_PROJECT_SCRIPT_PATH', | |
955 'TARGET_NAME', | |
956 'TARGET_PATH', | |
957 ]) | |
958 vcprojbuilder = SCons.Script.Builder( | |
959 action=vcprojaction, | |
960 suffix='$COMPONENT_VS_PROJECT_SUFFIX') | |
961 | |
962 source_vcproj_action = SCons.Script.Action( | |
963 ComponentVSSourceProjectBuilder, varlist=[ | |
964 'COMPONENT_VS_SOURCE_FOLDERS', | |
965 'COMPONENT_VS_SOURCE_SUFFIXES', | |
966 'COMPONENT_VS_SOURCE_TARGETS', | |
967 ]) | |
968 source_vcproj_builder = SCons.Script.Builder( | |
969 action=source_vcproj_action, | |
970 suffix='$COMPONENT_VS_PROJECT_SUFFIX') | |
971 | |
972 dir_vcproj_action = SCons.Script.Action( | |
973 ComponentVSDirProjectBuilder, varlist=[ | |
974 'COMPONENT_VS_SOURCE_FOLDERS', | |
975 'COMPONENT_VS_SOURCE_SUFFIXES', | |
976 'PROJECT_SOURCES', | |
977 ]) | |
978 dir_vcproj_builder = SCons.Script.Builder( | |
979 action=dir_vcproj_action, | |
980 suffix='$COMPONENT_VS_PROJECT_SUFFIX') | |
981 | |
982 slnaction = SCons.Script.Action(ComponentVSSolutionBuilder, varlist=[ | |
983 'COMPONENT_VS_ENABLED_PROJECTS', | |
984 'SOLUTION_FOLDERS', | |
985 'SOLUTION_PROJECTS', | |
986 'SOLUTION_TARGETS', | |
987 ]) | |
988 slnbuilder = SCons.Script.Builder( | |
989 action=slnaction, | |
990 suffix='$COMPONENT_VS_SOLUTION_SUFFIX') | |
991 | |
992 env.Append(BUILDERS={ | |
993 'ComponentVSDirProjectBuilder': dir_vcproj_builder, | |
994 'ComponentVSProjectBuilder': vcprojbuilder, | |
995 'ComponentVSSolutionBuilder': slnbuilder, | |
996 'ComponentVSSourceProjectBuilder': source_vcproj_builder, | |
997 }) | |
OLD | NEW |