| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365 |
- # Copyright (c) 2012 Google Inc. All rights reserved.
- # Use of this source code is governed by a BSD-style license that can be
- # found in the LICENSE file.
- """New implementation of Visual Studio project generation."""
- import hashlib
- import os
- import random
- from operator import attrgetter
- import gyp.common
- def cmp(x, y):
- return (x > y) - (x < y)
- # Initialize random number generator
- random.seed()
- # GUIDs for project types
- ENTRY_TYPE_GUIDS = {
- "project": "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}",
- "folder": "{2150E333-8FDC-42A3-9474-1A3956D46DE8}",
- }
- # ------------------------------------------------------------------------------
- # Helper functions
- def MakeGuid(name, seed="msvs_new"):
- """Returns a GUID for the specified target name.
- Args:
- name: Target name.
- seed: Seed for MD5 hash.
- Returns:
- A GUID-line string calculated from the name and seed.
- This generates something which looks like a GUID, but depends only on the
- name and seed. This means the same name/seed will always generate the same
- GUID, so that projects and solutions which refer to each other can explicitly
- determine the GUID to refer to explicitly. It also means that the GUID will
- not change when the project for a target is rebuilt.
- """
- # Calculate a MD5 signature for the seed and name.
- d = hashlib.md5((str(seed) + str(name)).encode("utf-8")).hexdigest().upper()
- # Convert most of the signature to GUID form (discard the rest)
- guid = (
- "{"
- + d[:8]
- + "-"
- + d[8:12]
- + "-"
- + d[12:16]
- + "-"
- + d[16:20]
- + "-"
- + d[20:32]
- + "}"
- )
- return guid
- # ------------------------------------------------------------------------------
- class MSVSSolutionEntry:
- def __cmp__(self, other):
- # Sort by name then guid (so things are in order on vs2008).
- return cmp((self.name, self.get_guid()), (other.name, other.get_guid()))
- class MSVSFolder(MSVSSolutionEntry):
- """Folder in a Visual Studio project or solution."""
- def __init__(self, path, name=None, entries=None, guid=None, items=None):
- """Initializes the folder.
- Args:
- path: Full path to the folder.
- name: Name of the folder.
- entries: List of folder entries to nest inside this folder. May contain
- Folder or Project objects. May be None, if the folder is empty.
- guid: GUID to use for folder, if not None.
- items: List of solution items to include in the folder project. May be
- None, if the folder does not directly contain items.
- """
- if name:
- self.name = name
- else:
- # Use last layer.
- self.name = os.path.basename(path)
- self.path = path
- self.guid = guid
- # Copy passed lists (or set to empty lists)
- self.entries = sorted(entries or [], key=attrgetter("path"))
- self.items = list(items or [])
- self.entry_type_guid = ENTRY_TYPE_GUIDS["folder"]
- def get_guid(self):
- if self.guid is None:
- # Use consistent guids for folders (so things don't regenerate).
- self.guid = MakeGuid(self.path, seed="msvs_folder")
- return self.guid
- # ------------------------------------------------------------------------------
- class MSVSProject(MSVSSolutionEntry):
- """Visual Studio project."""
- def __init__(
- self,
- path,
- name=None,
- dependencies=None,
- guid=None,
- spec=None,
- build_file=None,
- config_platform_overrides=None,
- fixpath_prefix=None,
- ):
- """Initializes the project.
- Args:
- path: Absolute path to the project file.
- name: Name of project. If None, the name will be the same as the base
- name of the project file.
- dependencies: List of other Project objects this project is dependent
- upon, if not None.
- guid: GUID to use for project, if not None.
- spec: Dictionary specifying how to build this project.
- build_file: Filename of the .gyp file that the vcproj file comes from.
- config_platform_overrides: optional dict of configuration platforms to
- used in place of the default for this target.
- fixpath_prefix: the path used to adjust the behavior of _fixpath
- """
- self.path = path
- self.guid = guid
- self.spec = spec
- self.build_file = build_file
- # Use project filename if name not specified
- self.name = name or os.path.splitext(os.path.basename(path))[0]
- # Copy passed lists (or set to empty lists)
- self.dependencies = list(dependencies or [])
- self.entry_type_guid = ENTRY_TYPE_GUIDS["project"]
- if config_platform_overrides:
- self.config_platform_overrides = config_platform_overrides
- else:
- self.config_platform_overrides = {}
- self.fixpath_prefix = fixpath_prefix
- self.msbuild_toolset = None
- def set_dependencies(self, dependencies):
- self.dependencies = list(dependencies or [])
- def get_guid(self):
- if self.guid is None:
- # Set GUID from path
- # TODO(rspangler): This is fragile.
- # 1. We can't just use the project filename sans path, since there could
- # be multiple projects with the same base name (for example,
- # foo/unittest.vcproj and bar/unittest.vcproj).
- # 2. The path needs to be relative to $SOURCE_ROOT, so that the project
- # GUID is the same whether it's included from base/base.sln or
- # foo/bar/baz/baz.sln.
- # 3. The GUID needs to be the same each time this builder is invoked, so
- # that we don't need to rebuild the solution when the project changes.
- # 4. We should be able to handle pre-built project files by reading the
- # GUID from the files.
- self.guid = MakeGuid(self.name)
- return self.guid
- def set_msbuild_toolset(self, msbuild_toolset):
- self.msbuild_toolset = msbuild_toolset
- # ------------------------------------------------------------------------------
- class MSVSSolution:
- """Visual Studio solution."""
- def __init__(
- self, path, version, entries=None, variants=None, websiteProperties=True
- ):
- """Initializes the solution.
- Args:
- path: Path to solution file.
- version: Format version to emit.
- entries: List of entries in solution. May contain Folder or Project
- objects. May be None, if the folder is empty.
- variants: List of build variant strings. If none, a default list will
- be used.
- websiteProperties: Flag to decide if the website properties section
- is generated.
- """
- self.path = path
- self.websiteProperties = websiteProperties
- self.version = version
- # Copy passed lists (or set to empty lists)
- self.entries = list(entries or [])
- if variants:
- # Copy passed list
- self.variants = variants[:]
- else:
- # Use default
- self.variants = ["Debug|Win32", "Release|Win32"]
- # TODO(rspangler): Need to be able to handle a mapping of solution config
- # to project config. Should we be able to handle variants being a dict,
- # or add a separate variant_map variable? If it's a dict, we can't
- # guarantee the order of variants since dict keys aren't ordered.
- # TODO(rspangler): Automatically write to disk for now; should delay until
- # node-evaluation time.
- self.Write()
- def Write(self, writer=gyp.common.WriteOnDiff):
- """Writes the solution file to disk.
- Raises:
- IndexError: An entry appears multiple times.
- """
- # Walk the entry tree and collect all the folders and projects.
- all_entries = set()
- entries_to_check = self.entries[:]
- while entries_to_check:
- e = entries_to_check.pop(0)
- # If this entry has been visited, nothing to do.
- if e in all_entries:
- continue
- all_entries.add(e)
- # If this is a folder, check its entries too.
- if isinstance(e, MSVSFolder):
- entries_to_check += e.entries
- all_entries = sorted(all_entries, key=attrgetter("path"))
- # Open file and print header
- f = writer(self.path)
- f.write(
- "Microsoft Visual Studio Solution File, "
- "Format Version %s\r\n" % self.version.SolutionVersion()
- )
- f.write("# %s\r\n" % self.version.Description())
- # Project entries
- sln_root = os.path.split(self.path)[0]
- for e in all_entries:
- relative_path = gyp.common.RelativePath(e.path, sln_root)
- # msbuild does not accept an empty folder_name.
- # use '.' in case relative_path is empty.
- folder_name = relative_path.replace("/", "\\") or "."
- f.write(
- 'Project("%s") = "%s", "%s", "%s"\r\n'
- % (
- e.entry_type_guid, # Entry type GUID
- e.name, # Folder name
- folder_name, # Folder name (again)
- e.get_guid(), # Entry GUID
- )
- )
- # TODO(rspangler): Need a way to configure this stuff
- if self.websiteProperties:
- f.write(
- "\tProjectSection(WebsiteProperties) = preProject\r\n"
- '\t\tDebug.AspNetCompiler.Debug = "True"\r\n'
- '\t\tRelease.AspNetCompiler.Debug = "False"\r\n'
- "\tEndProjectSection\r\n"
- )
- if isinstance(e, MSVSFolder) and e.items:
- f.write("\tProjectSection(SolutionItems) = preProject\r\n")
- for i in e.items:
- f.write(f"\t\t{i} = {i}\r\n")
- f.write("\tEndProjectSection\r\n")
- if isinstance(e, MSVSProject) and e.dependencies:
- f.write("\tProjectSection(ProjectDependencies) = postProject\r\n")
- for d in e.dependencies:
- f.write(f"\t\t{d.get_guid()} = {d.get_guid()}\r\n")
- f.write("\tEndProjectSection\r\n")
- f.write("EndProject\r\n")
- # Global section
- f.write("Global\r\n")
- # Configurations (variants)
- f.write("\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\r\n")
- for v in self.variants:
- f.write(f"\t\t{v} = {v}\r\n")
- f.write("\tEndGlobalSection\r\n")
- # Sort config guids for easier diffing of solution changes.
- config_guids = []
- config_guids_overrides = {}
- for e in all_entries:
- if isinstance(e, MSVSProject):
- config_guids.append(e.get_guid())
- config_guids_overrides[e.get_guid()] = e.config_platform_overrides
- config_guids.sort()
- f.write("\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\r\n")
- for g in config_guids:
- for v in self.variants:
- nv = config_guids_overrides[g].get(v, v)
- # Pick which project configuration to build for this solution
- # configuration.
- f.write(
- "\t\t%s.%s.ActiveCfg = %s\r\n"
- % (
- g, # Project GUID
- v, # Solution build configuration
- nv, # Project build config for that solution config
- )
- )
- # Enable project in this solution configuration.
- f.write(
- "\t\t%s.%s.Build.0 = %s\r\n"
- % (
- g, # Project GUID
- v, # Solution build configuration
- nv, # Project build config for that solution config
- )
- )
- f.write("\tEndGlobalSection\r\n")
- # TODO(rspangler): Should be able to configure this stuff too (though I've
- # never seen this be any different)
- f.write("\tGlobalSection(SolutionProperties) = preSolution\r\n")
- f.write("\t\tHideSolutionNode = FALSE\r\n")
- f.write("\tEndGlobalSection\r\n")
- # Folder mappings
- # Omit this section if there are no folders
- if any(e.entries for e in all_entries if isinstance(e, MSVSFolder)):
- f.write("\tGlobalSection(NestedProjects) = preSolution\r\n")
- for e in all_entries:
- if not isinstance(e, MSVSFolder):
- continue # Does not apply to projects, only folders
- for subentry in e.entries:
- f.write(f"\t\t{subentry.get_guid()} = {e.get_guid()}\r\n")
- f.write("\tEndGlobalSection\r\n")
- f.write("EndGlobal\r\n")
- f.close()
|