forked from LeenkxTeam/LNXSDK
		
	
		
			
	
	
		
			328 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			328 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
|  | """
 | ||
|  | Various utilities for interacting with Visual Studio on Windows. | ||
|  | """
 | ||
|  | import json | ||
|  | import os | ||
|  | import re | ||
|  | import subprocess | ||
|  | from typing import Any, Optional, Callable | ||
|  | 
 | ||
|  | import bpy | ||
|  | 
 | ||
|  | import lnx.log as log | ||
|  | import lnx.make | ||
|  | import lnx.make_state as state | ||
|  | import lnx.utils | ||
|  | 
 | ||
|  | if lnx.is_reload(__name__): | ||
|  |     log = lnx.reload_module(log) | ||
|  |     lnx.make = lnx.reload_module(lnx.make) | ||
|  |     state = lnx.reload_module(state) | ||
|  |     lnx.utils = lnx.reload_module(lnx.utils) | ||
|  | else: | ||
|  |     lnx.enable_reload(__name__) | ||
|  | 
 | ||
|  | 
 | ||
|  | # VS versions supported by khamake. Keep in mind that this list is also | ||
|  | # used for the wrd.lnx_project_win_list_vs EnumProperty! | ||
|  | supported_versions = [ | ||
|  |     ('10', '2010', 'Visual Studio 2010 (version 10)'), | ||
|  |     ('11', '2012', 'Visual Studio 2012 (version 11)'), | ||
|  |     ('12', '2013', 'Visual Studio 2013 (version 12)'), | ||
|  |     ('14', '2015', 'Visual Studio 2015 (version 14)'), | ||
|  |     ('15', '2017', 'Visual Studio 2017 (version 15)'), | ||
|  |     ('16', '2019', 'Visual Studio 2019 (version 16)'), | ||
|  |     ('17', '2022', 'Visual Studio 2022 (version 17)') | ||
|  | ] | ||
|  | 
 | ||
|  | # version_major to --visualstudio parameter | ||
|  | version_to_khamake_id = { | ||
|  |     '10': 'vs2010', | ||
|  |     '11': 'vs2012', | ||
|  |     '12': 'vs2013', | ||
|  |     '14': 'vs2015', | ||
|  |     '15': 'vs2017', | ||
|  |     '16': 'vs2019', | ||
|  |     '17': 'vs2022', | ||
|  | } | ||
|  | 
 | ||
|  | # VS versions found with fetch_installed_vs() | ||
|  | _installed_versions = [] | ||
|  | 
 | ||
|  | _REGEX_SLN_MIN_VERSION = re.compile(r'MinimumVisualStudioVersion\s*=\s*([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)') | ||
|  | 
 | ||
|  | 
 | ||
|  | def is_version_installed(version_major: str) -> bool: | ||
|  |     return any(v['version_major'] == version_major for v in _installed_versions) | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_installed_version(version_major: str, re_fetch=False) -> Optional[dict[str, str]]: | ||
|  |     for installed_version in _installed_versions: | ||
|  |         if installed_version['version_major'] == version_major: | ||
|  |             return installed_version | ||
|  | 
 | ||
|  |     # No installation was found. If re_fetch is True, fetch and try again | ||
|  |     # (the user may not have fetched installations before for example) | ||
|  |     if re_fetch: | ||
|  |         if not fetch_installed_vs(): | ||
|  |             return None | ||
|  |         return get_installed_version(version_major, False) | ||
|  | 
 | ||
|  |     return None | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_supported_version(version_major: str) -> Optional[dict[str, str]]: | ||
|  |     for version in supported_versions: | ||
|  |         if version[0] == version_major: | ||
|  |             return { | ||
|  |                 'version_major': version[0], | ||
|  |                 'year': version[1], | ||
|  |                 'name': version[2] | ||
|  |             } | ||
|  |     return None | ||
|  | 
 | ||
|  | 
 | ||
|  | def fetch_installed_vs(silent=False) -> bool: | ||
|  |     global _installed_versions | ||
|  | 
 | ||
|  |     data_instances = _vswhere_get_instances(silent) | ||
|  |     if data_instances is None: | ||
|  |         return False | ||
|  | 
 | ||
|  |     items = [] | ||
|  | 
 | ||
|  |     for inst in data_instances: | ||
|  |         name = _vswhere_get_display_name(inst) | ||
|  |         versions = _vswhere_get_version(inst) | ||
|  |         path = _vswhere_get_path(inst) | ||
|  | 
 | ||
|  |         if name is None or versions is None or path is None: | ||
|  |             if not silent: | ||
|  |                 log.warn( | ||
|  |                     f'Found a Visual Studio installation with incomplete information, skipping\n' | ||
|  |                     f'    ({name=}, {versions=}, {path=})' | ||
|  |                 ) | ||
|  |             continue | ||
|  | 
 | ||
|  |         items.append({ | ||
|  |             'version_major': versions[0], | ||
|  |             'version_full': versions[1], | ||
|  |             'version_full_ints': versions[2], | ||
|  |             'name': name, | ||
|  |             'path': path | ||
|  |         }) | ||
|  | 
 | ||
|  |     # Store in descending order | ||
|  |     items.sort(key=lambda x: x['version_major'], reverse=True) | ||
|  | 
 | ||
|  |     _installed_versions = items | ||
|  |     return True | ||
|  | 
 | ||
|  | 
 | ||
|  | def open_project_in_vs(version_major: str, version_min_full: Optional[str] = None) -> bool: | ||
|  |     installation = get_installed_version(version_major, re_fetch=True) | ||
|  |     if installation is None: | ||
|  |         if version_min_full is not None: | ||
|  |             # Try whether other installed versions are supported, versions | ||
|  |             # are already sorted in descending order | ||
|  |             for installed_version in _installed_versions: | ||
|  |                 if (installed_version['version_full_ints'] >= version_full_to_ints(version_min_full) | ||
|  |                         and int(installed_version['version_major']) < int(version_major)): | ||
|  |                     installation = installed_version | ||
|  |                     break | ||
|  | 
 | ||
|  |         # Still nothing found, warn for version_major | ||
|  |         if installation is None: | ||
|  |             vs_info = get_supported_version(version_major) | ||
|  |             log.warn(f'Could not open project in Visual Studio, {vs_info["name"]} was not found.') | ||
|  |             return False | ||
|  | 
 | ||
|  |     sln_path = get_sln_path() | ||
|  |     devenv_path = os.path.join(installation['path'], 'Common7', 'IDE', 'devenv.exe') | ||
|  |     cmd = ['start', devenv_path, sln_path] | ||
|  | 
 | ||
|  |     try: | ||
|  |         subprocess.check_call(cmd, shell=True) | ||
|  |     except subprocess.CalledProcessError as e: | ||
|  |         log.warn_called_process_error(e) | ||
|  |         return False | ||
|  | 
 | ||
|  |     return True | ||
|  | 
 | ||
|  | 
 | ||
|  | def enable_vsvars_env(version_major: str, done: Callable[[], None]) -> bool: | ||
|  |     installation = get_installed_version(version_major, re_fetch=True) | ||
|  |     if installation is None: | ||
|  |         vs_info = get_supported_version(version_major) | ||
|  |         log.error(f'Could not compile project in Visual Studio, {vs_info["name"]} was not found.') | ||
|  |         return False | ||
|  | 
 | ||
|  |     wrd = bpy.data.worlds['Lnx'] | ||
|  |     arch_bits = '64' if wrd.lnx_project_win_build_arch == 'x64' else '32' | ||
|  |     vcvars_path = os.path.join(installation['path'], 'VC', 'Auxiliary', 'Build', 'vcvars' + arch_bits + '.bat') | ||
|  | 
 | ||
|  |     if not os.path.isfile(vcvars_path): | ||
|  |         log.error( | ||
|  |             'Could not compile project in Visual Studio\n' | ||
|  |             f'    File "{vcvars_path}" not found. Please verify that {installation["name"]} was installed correctly.' | ||
|  |         ) | ||
|  |         return False | ||
|  | 
 | ||
|  |     state.proc_publish_build = lnx.make.run_proc(vcvars_path, done) | ||
|  |     return True | ||
|  | 
 | ||
|  | 
 | ||
|  | def compile_in_vs(version_major: str, done: Callable[[], None]) -> bool: | ||
|  |     installation = get_installed_version(version_major, re_fetch=True) | ||
|  |     if installation is None: | ||
|  |         vs_info = get_supported_version(version_major) | ||
|  |         log.error(f'Could not compile project in Visual Studio, {vs_info["name"]} was not found.') | ||
|  |         return False | ||
|  | 
 | ||
|  |     wrd = bpy.data.worlds['Lnx'] | ||
|  | 
 | ||
|  |     msbuild_path = os.path.join(installation['path'], 'MSBuild', 'Current', 'Bin', 'MSBuild.exe') | ||
|  |     if not os.path.isfile(msbuild_path): | ||
|  |         log.error( | ||
|  |             'Could not compile project in Visual Studio\n' | ||
|  |             f'    File "{msbuild_path}" not found. Please verify that {installation["name"]} was installed correctly.' | ||
|  |         ) | ||
|  |         return False | ||
|  | 
 | ||
|  |     projectfile_path = get_vcxproj_path() | ||
|  | 
 | ||
|  |     cmd = [msbuild_path, projectfile_path] | ||
|  | 
 | ||
|  |     # Arguments | ||
|  |     platform = 'x64' if wrd.lnx_project_win_build_arch == 'x64' else 'win32' | ||
|  |     log_param = wrd.lnx_project_win_build_log | ||
|  |     if log_param == 'WarningsAndErrorsOnly': | ||
|  |         log_param = 'WarningsOnly;ErrorsOnly' | ||
|  | 
 | ||
|  |     cmd.extend([ | ||
|  |         '-m:' + str(wrd.lnx_project_win_build_cpu), | ||
|  |         '-clp:' + log_param, | ||
|  |         '/p:Configuration=' + wrd.lnx_project_win_build_mode, | ||
|  |         '/p:Platform=' + platform | ||
|  |     ]) | ||
|  | 
 | ||
|  |     print('\nCompiling the project ' + projectfile_path) | ||
|  |     state.proc_publish_build = lnx.make.run_proc(cmd, done) | ||
|  |     state.redraw_ui = True | ||
|  |     return True | ||
|  | 
 | ||
|  | 
 | ||
|  | def _vswhere_get_display_name(instance_data: dict[str, Any]) -> Optional[str]: | ||
|  |     name_raw = instance_data.get('displayName', None) | ||
|  |     if name_raw is None: | ||
|  |         return None | ||
|  |     return lnx.utils.safestr(name_raw).replace('_', ' ').strip() | ||
|  | 
 | ||
|  | 
 | ||
|  | def _vswhere_get_version(instance_data: dict[str, Any]) -> Optional[tuple[str, str, tuple[int, ...]]]: | ||
|  |     version_raw = instance_data.get('installationVersion', None) | ||
|  |     if version_raw is None: | ||
|  |         return None | ||
|  | 
 | ||
|  |     version_full = version_raw.strip() | ||
|  |     version_full_ints = version_full_to_ints(version_full) | ||
|  |     version_major = version_full.split('.')[0] | ||
|  |     return version_major, version_full, version_full_ints | ||
|  | 
 | ||
|  | 
 | ||
|  | def _vswhere_get_path(instance_data: dict[str, Any]) -> Optional[str]: | ||
|  |     return instance_data.get('installationPath', None) | ||
|  | 
 | ||
|  | 
 | ||
|  | def _vswhere_get_instances(silent=False) -> Optional[list[dict[str, Any]]]: | ||
|  |     # vswhere.exe only exists at that location since VS2017 v15.2, for | ||
|  |     # earlier versions we'd need to package vswhere with Leenkx | ||
|  |     exe_path = os.path.join(os.environ["ProgramFiles(x86)"], 'Microsoft Visual Studio', 'Installer', 'vswhere.exe') | ||
|  |     command = [exe_path, '-format', 'json', '-utf8'] | ||
|  | 
 | ||
|  |     try: | ||
|  |         result = subprocess.check_output(command) | ||
|  |     except subprocess.CalledProcessError as e: | ||
|  |         # Do not silence this warning, if this exception is caught there | ||
|  |         # likely is an issue in the command above | ||
|  |         log.warn_called_process_error(e) | ||
|  |         return None | ||
|  |     except FileNotFoundError as e: | ||
|  |         if not silent: | ||
|  |             log.warn(f'Could not open file "{exe_path}", make sure the file exists (errno {e.errno}).') | ||
|  |         return None | ||
|  | 
 | ||
|  |     result = json.loads(result.decode('utf-8')) | ||
|  |     return result | ||
|  | 
 | ||
|  | 
 | ||
|  | def version_full_to_ints(version_full: str) -> tuple[int, ...]: | ||
|  |     return tuple(int(i) for i in version_full.split('.')) | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_project_path() -> str: | ||
|  |     return os.path.join(lnx.utils.get_fp_build(), 'windows-hl-build') | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_project_name(): | ||
|  |     wrd = bpy.data.worlds['Lnx'] | ||
|  |     return lnx.utils.safesrc(wrd.lnx_project_name + '-' + wrd.lnx_project_version) | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_sln_path() -> str: | ||
|  |     project_path = get_project_path() | ||
|  |     project_name = get_project_name() | ||
|  |     return os.path.join(project_path, project_name + '.sln') | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_vcxproj_path() -> str: | ||
|  |     project_name = get_project_name() | ||
|  |     project_path = get_project_path() | ||
|  |     return os.path.join(project_path, project_name + '.vcxproj') | ||
|  | 
 | ||
|  | 
 | ||
|  | def fetch_project_version() -> tuple[Optional[str], Optional[str], Optional[str]]: | ||
|  |     version_major = None | ||
|  |     version_min_full = None | ||
|  | 
 | ||
|  |     try: | ||
|  |         # References: | ||
|  |         # https://learn.microsoft.com/en-us/visualstudio/extensibility/internals/solution-dot-sln-file?view=vs-2022#file-header | ||
|  |         # https://github.com/Kode/kmake/blob/a104a89b55218054ceed761d5bc75d6e5cd60573/kmake/src/Exporters/VisualStudioExporter.ts#L188-L225 | ||
|  |         with open(get_sln_path(), 'r') as file: | ||
|  |             for linenum, line in enumerate(file): | ||
|  |                 line = line.strip() | ||
|  | 
 | ||
|  |                 if linenum == 1: | ||
|  |                     if line == '# Visual Studio Version 17': | ||
|  |                         version_major = 17 | ||
|  |                     elif line == '# Visual Studio Version 16': | ||
|  |                         version_major = 16 | ||
|  |                     elif line == '# Visual Studio 15': | ||
|  |                         version_major = 15 | ||
|  |                     elif line == '# Visual Studio 14': | ||
|  |                         version_major = 14 | ||
|  |                     elif line == '# Visual Studio 2013': | ||
|  |                         version_major = 12 | ||
|  |                     elif line == '# Visual Studio 2012': | ||
|  |                         version_major = 11 | ||
|  |                     elif line == '# Visual Studio 2010': | ||
|  |                         version_major = 10 | ||
|  |                     else: | ||
|  |                         log.warn(f'Could not parse Visual Studio version. Invalid major version, parsed {line}') | ||
|  |                         return None, None, 'err_invalid_version_major' | ||
|  | 
 | ||
|  |                 elif linenum == 3 and version_major >= 12: | ||
|  |                     match = _REGEX_SLN_MIN_VERSION.match(line) | ||
|  |                     if match: | ||
|  |                         version_min_full = match.group(1) | ||
|  |                         break | ||
|  | 
 | ||
|  |                     log.warn(f'Could not parse Visual Studio version. Invalid full version, parsed {line}') | ||
|  |                     return None, None, 'err_invalid_version_full' | ||
|  | 
 | ||
|  |     except FileNotFoundError: | ||
|  |         return None, None, 'err_file_not_found' | ||
|  | 
 | ||
|  |     return str(version_major), version_min_full, None |