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
 |