LNXSDK/leenkx/blender/lnx/utils_vs.py
2025-01-22 16:18:30 +01:00

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