# Copyright (c) 2013 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. # This file copies the logic from GYP to find the MSVC configuration. It's not # currently used because it is too slow. We will probably build this # functionality into the C++ code in the future. """Handle version information related to Visual Stuio.""" import errno import os import re import subprocess import sys class VisualStudioVersion(object): """Information regarding a version of Visual Studio.""" def __init__(self, short_name, description, solution_version, project_version, flat_sln, uses_vcxproj, path, sdk_based, default_toolset=None): self.short_name = short_name self.description = description self.solution_version = solution_version self.project_version = project_version self.flat_sln = flat_sln self.uses_vcxproj = uses_vcxproj self.path = path self.sdk_based = sdk_based self.default_toolset = default_toolset def ShortName(self): return self.short_name def Description(self): """Get the full description of the version.""" return self.description def SolutionVersion(self): """Get the version number of the sln files.""" return self.solution_version def ProjectVersion(self): """Get the version number of the vcproj or vcxproj files.""" return self.project_version def FlatSolution(self): return self.flat_sln def UsesVcxproj(self): """Returns true if this version uses a vcxproj file.""" return self.uses_vcxproj def ProjectExtension(self): """Returns the file extension for the project.""" return self.uses_vcxproj and '.vcxproj' or '.vcproj' def Path(self): """Returns the path to Visual Studio installation.""" return self.path def ToolPath(self, tool): """Returns the path to a given compiler tool. """ return os.path.normpath(os.path.join(self.path, "VC/bin", tool)) def DefaultToolset(self): """Returns the msbuild toolset version that will be used in the absence of a user override.""" return self.default_toolset def SetupScript(self, target_arch): """Returns a command (with arguments) to be used to set up the environment.""" # Check if we are running in the SDK command line environment and use # the setup script from the SDK if so. |target_arch| should be either # 'x86' or 'x64'. assert target_arch in ('x86', 'x64') sdk_dir = os.environ.get('WindowsSDKDir') if self.sdk_based and sdk_dir: return [os.path.normpath(os.path.join(sdk_dir, 'Bin/SetEnv.Cmd')), '/' + target_arch] else: # We don't use VC/vcvarsall.bat for x86 because vcvarsall calls # vcvars32, which it can only find if VS??COMNTOOLS is set, which it # isn't always. if target_arch == 'x86': return [os.path.normpath( os.path.join(self.path, 'Common7/Tools/vsvars32.bat'))] else: assert target_arch == 'x64' arg = 'x86_amd64' if (os.environ.get('PROCESSOR_ARCHITECTURE') == 'AMD64' or os.environ.get('PROCESSOR_ARCHITEW6432') == 'AMD64'): # Use the 64-on-64 compiler if we can. arg = 'amd64' return [os.path.normpath( os.path.join(self.path, 'VC/vcvarsall.bat')), arg] def _RegistryQueryBase(sysdir, key, value): """Use reg.exe to read a particular key. While ideally we might use the win32 module, we would like gyp to be python neutral, so for instance cygwin python lacks this module. Arguments: sysdir: The system subdirectory to attempt to launch reg.exe from. key: The registry key to read from. value: The particular value to read. Return: stdout from reg.exe, or None for failure. """ # Skip if not on Windows or Python Win32 setup issue if sys.platform not in ('win32', 'cygwin'): return None # Setup params to pass to and attempt to launch reg.exe cmd = [os.path.join(os.environ.get('WINDIR', ''), sysdir, 'reg.exe'), 'query', key] if value: cmd.extend(['/v', value]) p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Obtain the stdout from reg.exe, reading to the end so p.returncode is valid # Note that the error text may be in [1] in some cases text = p.communicate()[0] # Check return code from reg.exe; officially 0==success and 1==error if p.returncode: return None return text def _RegistryQuery(key, value=None): """Use reg.exe to read a particular key through _RegistryQueryBase. First tries to launch from %WinDir%\Sysnative to avoid WoW64 redirection. If that fails, it falls back to System32. Sysnative is available on Vista and up and available on Windows Server 2003 and XP through KB patch 942589. Note that Sysnative will always fail if using 64-bit python due to it being a virtual directory and System32 will work correctly in the first place. KB 942589 - http://support.microsoft.com/kb/942589/en-us. Arguments: key: The registry key. value: The particular registry value to read (optional). Return: stdout from reg.exe, or None for failure. """ text = None try: text = _RegistryQueryBase('Sysnative', key, value) except OSError, e: if e.errno == errno.ENOENT: text = _RegistryQueryBase('System32', key, value) else: raise return text def _RegistryGetValue(key, value): """Use reg.exe to obtain the value of a registry key. Args: key: The registry key. value: The particular registry value to read. Return: contents of the registry key's value, or None on failure. """ text = _RegistryQuery(key, value) if not text: return None # Extract value. match = re.search(r'REG_\w+\s+([^\r]+)\r\n', text) if not match: return None return match.group(1) def _RegistryKeyExists(key): """Use reg.exe to see if a key exists. Args: key: The registry key to check. Return: True if the key exists """ if not _RegistryQuery(key): return False return True def _CreateVersion(name, path, sdk_based=False): """Sets up MSVS project generation. Setup is based off the GYP_MSVS_VERSION environment variable or whatever is autodetected if GYP_MSVS_VERSION is not explicitly specified. If a version is passed in that doesn't match a value in versions python will throw a error. """ if path: path = os.path.normpath(path) versions = { '2013': VisualStudioVersion('2013', 'Visual Studio 2013', solution_version='13.00', project_version='4.0', flat_sln=False, uses_vcxproj=True, path=path, sdk_based=sdk_based, default_toolset='v110'), '2013e': VisualStudioVersion('2013e', 'Visual Studio 2013', solution_version='13.00', project_version='4.0', flat_sln=True, uses_vcxproj=True, path=path, sdk_based=sdk_based, default_toolset='v110'), '2012': VisualStudioVersion('2012', 'Visual Studio 2012', solution_version='12.00', project_version='4.0', flat_sln=False, uses_vcxproj=True, path=path, sdk_based=sdk_based, default_toolset='v110'), '2012e': VisualStudioVersion('2012e', 'Visual Studio 2012', solution_version='12.00', project_version='4.0', flat_sln=True, uses_vcxproj=True, path=path, sdk_based=sdk_based, default_toolset='v110'), '2010': VisualStudioVersion('2010', 'Visual Studio 2010', solution_version='11.00', project_version='4.0', flat_sln=False, uses_vcxproj=True, path=path, sdk_based=sdk_based), '2010e': VisualStudioVersion('2010e', 'Visual Studio 2010', solution_version='11.00', project_version='4.0', flat_sln=True, uses_vcxproj=True, path=path, sdk_based=sdk_based), '2008': VisualStudioVersion('2008', 'Visual Studio 2008', solution_version='10.00', project_version='9.00', flat_sln=False, uses_vcxproj=False, path=path, sdk_based=sdk_based), '2008e': VisualStudioVersion('2008e', 'Visual Studio 2008', solution_version='10.00', project_version='9.00', flat_sln=True, uses_vcxproj=False, path=path, sdk_based=sdk_based), '2005': VisualStudioVersion('2005', 'Visual Studio 2005', solution_version='9.00', project_version='8.00', flat_sln=False, uses_vcxproj=False, path=path, sdk_based=sdk_based), '2005e': VisualStudioVersion('2005e', 'Visual Studio 2005', solution_version='9.00', project_version='8.00', flat_sln=True, uses_vcxproj=False, path=path, sdk_based=sdk_based), } return versions[str(name)] def _ConvertToCygpath(path): """Convert to cygwin path if we are using cygwin.""" if sys.platform == 'cygwin': p = subprocess.Popen(['cygpath', path], stdout=subprocess.PIPE) path = p.communicate()[0].strip() return path def _DetectVisualStudioVersions(versions_to_check, force_express): """Collect the list of installed visual studio versions. Returns: A list of visual studio versions installed in descending order of usage preference. Base this on the registry and a quick check if devenv.exe exists. Only versions 8-10 are considered. Possibilities are: 2005(e) - Visual Studio 2005 (8) 2008(e) - Visual Studio 2008 (9) 2010(e) - Visual Studio 2010 (10) 2012(e) - Visual Studio 2012 (11) 2013(e) - Visual Studio 2013 (11) Where (e) is e for express editions of MSVS and blank otherwise. """ version_to_year = { '8.0': '2005', '9.0': '2008', '10.0': '2010', '11.0': '2012', '12.0': '2013', } versions = [] for version in versions_to_check: # Old method of searching for which VS version is installed # We don't use the 2010-encouraged-way because we also want to get the # path to the binaries, which it doesn't offer. keys = [r'HKLM\Software\Microsoft\VisualStudio\%s' % version, r'HKLM\Software\Wow6432Node\Microsoft\VisualStudio\%s' % version, r'HKLM\Software\Microsoft\VCExpress\%s' % version, r'HKLM\Software\Wow6432Node\Microsoft\VCExpress\%s' % version] for index in range(len(keys)): path = _RegistryGetValue(keys[index], 'InstallDir') if not path: continue path = _ConvertToCygpath(path) # Check for full. full_path = os.path.join(path, 'devenv.exe') express_path = os.path.join(path, 'vcexpress.exe') if not force_express and os.path.exists(full_path): # Add this one. versions.append(_CreateVersion(version_to_year[version], os.path.join(path, '..', '..'))) # Check for express. elif os.path.exists(express_path): # Add this one. versions.append(_CreateVersion(version_to_year[version] + 'e', os.path.join(path, '..', '..'))) # The old method above does not work when only SDK is installed. keys = [r'HKLM\Software\Microsoft\VisualStudio\SxS\VC7', r'HKLM\Software\Wow6432Node\Microsoft\VisualStudio\SxS\VC7'] for index in range(len(keys)): path = _RegistryGetValue(keys[index], version) if not path: continue path = _ConvertToCygpath(path) versions.append(_CreateVersion(version_to_year[version] + 'e', os.path.join(path, '..'), sdk_based=True)) return versions def SelectVisualStudioVersion(version='auto'): """Select which version of Visual Studio projects to generate. Arguments: version: Hook to allow caller to force a particular version (vs auto). Returns: An object representing a visual studio project format version. """ # In auto mode, check environment variable for override. if version == 'auto': version = os.environ.get('GYP_MSVS_VERSION', 'auto') version_map = { 'auto': ('10.0', '9.0', '8.0', '11.0'), '2005': ('8.0',), '2005e': ('8.0',), '2008': ('9.0',), '2008e': ('9.0',), '2010': ('10.0',), '2010e': ('10.0',), '2012': ('11.0',), '2012e': ('11.0',), '2013': ('12.0',), '2013e': ('12.0',), } override_path = os.environ.get('GYP_MSVS_OVERRIDE_PATH') if override_path: msvs_version = os.environ.get('GYP_MSVS_VERSION') if not msvs_version or 'e' not in msvs_version: raise ValueError('GYP_MSVS_OVERRIDE_PATH requires GYP_MSVS_VERSION to be ' 'set to an "e" version (e.g. 2010e)') return _CreateVersion(msvs_version, override_path, sdk_based=True) version = str(version) versions = _DetectVisualStudioVersions(version_map[version], 'e' in version) if not versions: if version == 'auto': # Default to 2005 if we couldn't find anything return _CreateVersion('2005', None) else: return _CreateVersion(version, None) return versions[0] def GenerateEnvironmentFiles(toplevel_build_dir, generator_flags, open_out): """It's not sufficient to have the absolute path to the compiler, linker, etc. on Windows, as those tools rely on .dlls being in the PATH. We also need to support both x86 and x64 compilers within the same build (to support msvs_target_platform hackery). Different architectures require a different compiler binary, and different supporting environment variables (INCLUDE, LIB, LIBPATH). So, we extract the environment here, wrap all invocations of compiler tools (cl, link, lib, rc, midl, etc.) via win_tool.py which sets up the environment, and then we do not prefix the compiler with an absolute path, instead preferring something like "cl.exe" in the rule which will then run whichever the environment setup has put in the path. When the following procedure to generate environment files does not meet your requirement (e.g. for custom toolchains), you can pass "-G ninja_use_custom_environment_files" to the gyp to suppress file generation and use custom environment files prepared by yourself.""" archs = ('x86', 'x64') if generator_flags.get('ninja_use_custom_environment_files', 0): cl_paths = {} for arch in archs: cl_paths[arch] = 'cl.exe' return cl_paths vs = GetVSVersion(generator_flags) cl_paths = {} for arch in archs: # Extract environment variables for subprocesses. args = vs.SetupScript(arch) args.extend(('&&', 'set')) popen = subprocess.Popen( args, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) variables, _ = popen.communicate() env = _ExtractImportantEnvironment(variables) env_block = _FormatAsEnvironmentBlock(env) f = open_out(os.path.join(toplevel_build_dir, 'environment.' + arch), 'wb') f.write(env_block) f.close() # Find cl.exe location for this architecture. args = vs.SetupScript(arch) args.extend(('&&', 'for', '%i', 'in', '(cl.exe)', 'do', '@echo', 'LOC:%~$PATH:i')) popen = subprocess.Popen(args, shell=True, stdout=subprocess.PIPE) output, _ = popen.communicate() cl_paths[arch] = _ExtractCLPath(output) return cl_paths def OpenOutput(path, mode='w'): """Open |path| for writing, creating directories if necessary.""" try: os.makedirs(os.path.dirname(path)) except OSError: pass return open(path, mode) vs_version = None def GetVSVersion(generator_flags): global vs_version if not vs_version: vs_version = SelectVisualStudioVersion( generator_flags.get('msvs_version', 'auto')) return vs_version def _ExtractImportantEnvironment(output_of_set): """Extracts environment variables required for the toolchain to run from a textual dump output by the cmd.exe 'set' command.""" envvars_to_save = ( 'goma_.*', # TODO(scottmg): This is ugly, but needed for goma. 'include', 'lib', 'libpath', 'path', 'pathext', 'systemroot', 'temp', 'tmp', ) env = {} for line in output_of_set.splitlines(): for envvar in envvars_to_save: if re.match(envvar + '=', line.lower()): var, setting = line.split('=', 1) if envvar == 'path': # Our own rules (for running gyp-win-tool) and other actions in # Chromium rely on python being in the path. Add the path to this # python here so that if it's not in the path when ninja is run # later, python will still be found. setting = os.path.dirname(sys.executable) + os.pathsep + setting env[var.upper()] = setting break for required in ('SYSTEMROOT', 'TEMP', 'TMP'): if required not in env: raise Exception('Environment variable "%s" ' 'required to be set to valid path' % required) return env def _FormatAsEnvironmentBlock(envvar_dict): """Format as an 'environment block' directly suitable for CreateProcess. Briefly this is a list of key=value\0, terminated by an additional \0. See CreateProcess documentation for more details.""" block = '' nul = '\0' for key, value in envvar_dict.iteritems(): block += key + '=' + value + nul block += nul return block def GenerateEnvironmentFiles(toplevel_build_dir, generator_flags): """It's not sufficient to have the absolute path to the compiler, linker, etc. on Windows, as those tools rely on .dlls being in the PATH. We also need to support both x86 and x64 compilers within the same build (to support msvs_target_platform hackery). Different architectures require a different compiler binary, and different supporting environment variables (INCLUDE, LIB, LIBPATH). So, we extract the environment here, wrap all invocations of compiler tools (cl, link, lib, rc, midl, etc.) via win_tool.py which sets up the environment, and then we do not prefix the compiler with an absolute path, instead preferring something like "cl.exe" in the rule which will then run whichever the environment setup has put in the path. When the following procedure to generate environment files does not meet your requirement (e.g. for custom toolchains), you can pass "-G ninja_use_custom_environment_files" to the gyp to suppress file generation and use custom environment files prepared by yourself.""" archs = ('x86', 'x64') if generator_flags.get('ninja_use_custom_environment_files', 0): cl_paths = {} for arch in archs: cl_paths[arch] = 'cl.exe' return cl_paths vs = GetVSVersion(generator_flags) cl_paths = {} for arch in archs: # Extract environment variables for subprocesses. args = vs.SetupScript(arch) args.extend(('&&', 'set')) popen = subprocess.Popen( args, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) variables, _ = popen.communicate() env = _ExtractImportantEnvironment(variables) env_block = _FormatAsEnvironmentBlock(env) f = OpenOutput(os.path.join(toplevel_build_dir, 'environment.' + arch), 'wb') f.write(env_block) f.close() # Find cl.exe location for this architecture. args = vs.SetupScript(arch) args.extend(('&&', 'for', '%i', 'in', '(cl.exe)', 'do', '@echo', 'LOC:%~$PATH:i')) popen = subprocess.Popen(args, shell=True, stdout=subprocess.PIPE) output, _ = popen.communicate() cl_paths[arch] = _ExtractCLPath(output) return cl_paths def _ExtractCLPath(output_of_where): """Gets the path to cl.exe based on the output of calling the environment setup batch file, followed by the equivalent of `where`.""" # Take the first line, as that's the first found in the PATH. for line in output_of_where.strip().splitlines(): if line.startswith('LOC:'): return line[len('LOC:'):].strip() #print SelectVisualStudioVersion().DefaultToolset() #GenerateEnvironmentFiles("D:\\src\\src1\\src\\out\\gn\\eraseme", {}) #print '"', GetVSVersion({}).Path(), '"' print '"', GetVSVersion({}).sdk_based, '"' #------------------------------------------------------------------------------- version_info = { '2010': { 'includes': [ 'VC\\atlmfc\\include', ], }, }