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
|