From 7de14de245472af5ae42792cf66efa969f8e6c32 Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Sun, 14 Jun 2026 22:00:24 -0400 Subject: [PATCH] Allow to install from latest VS version --- gvsbuild/build.py | 3 +- gvsbuild/utils/builder.py | 74 +++++++++++++------- tests/utils/test_builder.py | 130 +++++++++++++++++++++++++++++++++++- 3 files changed, 181 insertions(+), 26 deletions(-) diff --git a/gvsbuild/build.py b/gvsbuild/build.py index 3822268d3..b17ec9186 100644 --- a/gvsbuild/build.py +++ b/gvsbuild/build.py @@ -106,6 +106,7 @@ class Configuration(str, Enum): class VsVer(str, Enum): + latest = "latest" vs2013 = "12" vs2015 = "14" vs2017 = "15" @@ -147,7 +148,7 @@ def build( patches_root_dir: Annotated[Path | None, Parameter(group=DIRECTORY_GROUP)] = None, tools_root_dir: Annotated[Path | None, Parameter(group=DIRECTORY_GROUP)] = None, git_expand_dir: Annotated[Path | None, Parameter(group=DIRECTORY_GROUP)] = None, - vs_ver: Annotated[VsVer, Parameter(group=VS_SDK_GROUP)] = VsVer.vs2022, + vs_ver: Annotated[VsVer, Parameter(group=VS_SDK_GROUP)] = VsVer.latest, vs_install_path: Annotated[Path | None, Parameter(group=VS_SDK_GROUP)] = None, win_sdk_ver: Annotated[WinSdkVersion | None, Parameter(group=VS_SDK_GROUP)] = None, net_target_framework: Annotated[str | None, Parameter(group=NET_GROUP)] = None, diff --git a/gvsbuild/utils/builder.py b/gvsbuild/utils/builder.py index 6d523da99..0dd4dba0b 100644 --- a/gvsbuild/utils/builder.py +++ b/gvsbuild/utils/builder.py @@ -41,6 +41,16 @@ class CheckVsInstallError(Exception): pass +VS_ZIP_PARTS: dict[str, str] = { + "12": "vs2013", + "14": "vs2015", + "15": "vs2017", + "16": "vs2019", + "17": "vs2022", + "18": "vs2026", +} + + class Builder: def __init__(self, opts): self.opts = opts @@ -96,20 +106,17 @@ def __init__(self, opts): self.x86 = opts.platform == "Win32" self.x64 = not self.x86 - # Create the year version for Visual Studio - vs_zip_parts = { - "12": "vs2013", - "14": "vs2015", - "15": "vs2017", - "16": "vs2019", - "17": "vs2022", - "18": "vs2026", - } + # Set vs_ver_year for specific version requests so __check_vs can use it + # for path matching. For "latest", this is resolved inside __check_vs. + if opts.vs_ver != "latest": + self.vs_ver_year = VS_ZIP_PARTS.get(opts.vs_ver, f"ms-cl-{opts.vs_ver}") + else: + self.vs_ver_year = None - self.vs_ver_year = vs_zip_parts.get(opts.vs_ver) - if not self.vs_ver_year: - self.vs_ver_year = f"ms-cl-{opts.vs_ver}" + self.__check_tools(opts) + self.__check_vs(opts) + # zip_dir depends on the resolved vs_ver_year, so compute it after __check_vs vs_part = self.vs_ver_year if opts.win_sdk_ver: vs_part += f"-{opts.win_sdk_ver}" @@ -128,9 +135,6 @@ def __init__(self, opts): self.file_built = set() os.makedirs(self.zip_dir, exist_ok=True) - self.__check_tools(opts) - self.__check_vs(opts) - def _create_msbuild_opts(self, python) -> list[str]: rt = ["/nologo", f"/p:Platform={self.opts.platform}"] if python: @@ -316,7 +320,7 @@ def restore_env(self, saved): else: del self.vs_env[key] - def __dump_vs_loc(self): + def __dump_vs_loc(self) -> list[tuple[str, str]]: vswhere = r"{}\Microsoft Visual Studio\Installer\vswhere.exe".format( os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)") ) @@ -338,17 +342,18 @@ def __dump_vs_loc(self): log.log(f"Unable to call vswhere.exe to find Visual Studio with error {e}") return [] - def __extract_paths(self, res): + def __extract_paths(self, res) -> list[tuple[str, str]]: log.message("") log.message("Visual Studio installation(s) found:") - paths = [] + installs = [] for i in res: disp = i.get("displayName", "?") path = i.get("installationPath", r"?:\?") + major = i.get("installationVersion", "0.0.0.0").split(".")[0] log.message(f" {disp} @ {path}") - paths.append(path) + installs.append((path, major)) log.message("") - return paths + return installs def __check_vs_install(self, opts, vs_path, assert_vs_version): if assert_vs_version: @@ -407,16 +412,31 @@ def __check_vs(self, opts): self.add_global_env("PATH", os.path.join(self.gtk_dir, "bin")) if opts.vs_install_path: - vs_paths = [opts.vs_install_path] + m = re.search( + r"Microsoft Visual Studio[\\\/](\d+)[\\\/]", + str(opts.vs_install_path), + ) + path_major: str | None = m.group(1) if m else None + vs_installs: list[tuple[str, str | None]] = [ + (opts.vs_install_path, path_major) + ] assert_vs_version = False else: - vs_paths = self.__dump_vs_loc() - assert_vs_version = True + vs_installs = self.__dump_vs_loc() + if opts.vs_ver == "latest": + assert_vs_version = False + vs_installs = sorted( + vs_installs, key=lambda x: int(x[1] or 0), reverse=True + ) + else: + assert_vs_version = True output = None - for path in vs_paths: + selected_major: str | None = None + for path, major in vs_installs: try: output = self.__check_vs_install(opts, path, assert_vs_version) + selected_major = major except CheckVsInstallError as check_vs_error: log.message(f'- Skipping Visual Studio at "{path}": {check_vs_error}') @@ -430,6 +450,12 @@ def __check_vs(self, opts): "to specify Visual Studio version or install location" ) + if opts.vs_ver == "latest" and selected_major: + opts.vs_ver = selected_major + self.vs_ver_year = VS_ZIP_PARTS.get( + selected_major, f"ms-cl-{selected_major}" + ) + self.vs_env = {} dbg = log.debug_on() for line in output.splitlines(): diff --git a/tests/utils/test_builder.py b/tests/utils/test_builder.py index f8a9b040f..fbdb5092e 100644 --- a/tests/utils/test_builder.py +++ b/tests/utils/test_builder.py @@ -16,8 +16,9 @@ import pytest +from gvsbuild.build import VsVer from gvsbuild.utils.base_project import Options -from gvsbuild.utils.builder import Builder, CheckVsInstallError +from gvsbuild.utils.builder import VS_ZIP_PARTS, Builder, CheckVsInstallError @pytest.fixture @@ -127,3 +128,130 @@ def test_vs_check_success(tmp_path): vcvars_path.parent.mkdir(parents=True) vcvars_path.write_text("SET FOO=BAR") builder._Builder__check_vs_install(opts, str(tmp_path), False) + + +def test_vs_zip_parts_entries(): + assert VS_ZIP_PARTS["17"] == "vs2022" + assert VS_ZIP_PARTS["18"] == "vs2026" + assert all(k.isdigit() for k in VS_ZIP_PARTS) + + +def test_vsver_has_latest(): + assert VsVer.latest == "latest" + assert VsVer.latest.value == "latest" + + +def test_extract_paths_returns_path_and_major_tuples(): + builder = Builder.__new__(Builder) + res = [ + { + "displayName": "Visual Studio Enterprise 2026", + "installationPath": r"C:\VS\18\Enterprise", + "installationVersion": "18.0.35410.57", + }, + { + "displayName": "Visual Studio Community 2022", + "installationPath": r"C:\VS\17\Community", + "installationVersion": "17.13.0.0", + }, + ] + result = builder._Builder__extract_paths(res) + assert result == [ + (r"C:\VS\18\Enterprise", "18"), + (r"C:\VS\17\Community", "17"), + ] + + +def test_extract_paths_handles_missing_installation_version(): + builder = Builder.__new__(Builder) + res = [{"displayName": "VS", "installationPath": r"C:\VS"}] + result = builder._Builder__extract_paths(res) + assert result == [(r"C:\VS", "0")] + + +_FAKE_VS_ENV = "PATH=C:\\Windows\nFOO=BAR\n" + + +@pytest.fixture +def latest_builder(mocker): + """Partially constructed Builder ready for __check_vs 'latest' tests.""" + opts = Options() + opts.vs_ver = "latest" + opts.vs_install_path = None + opts.platform = "x64" + opts.win_sdk_ver = "10.0.22000.0" # skip SDK auto-detection + + builder = Builder.__new__(Builder) + builder.opts = opts + builder.gtk_dir = r"C:\gtk" + + mocker.patch("gvsbuild.utils.builder.script_title") + mocker.patch("gvsbuild.utils.builder.log") + mocker.patch.object(builder, "add_global_env") + + return builder, opts + + +def test_check_vs_latest_selects_newest_install(latest_builder, mocker): + builder, opts = latest_builder + + # vswhere returns VS 2022 first, then VS 2026 — latest should sort and pick 2026 + mocker.patch.object( + builder, + "_Builder__dump_vs_loc", + return_value=[ + (r"C:\VS\17\Enterprise", "17"), + (r"C:\VS\18\Enterprise", "18"), + ], + ) + mocker.patch.object( + builder, "_Builder__check_vs_install", return_value=_FAKE_VS_ENV + ) + + builder._Builder__check_vs(opts) + + assert opts.vs_ver == "18" + assert builder.vs_ver_year == "vs2026" + + +def test_check_vs_latest_falls_back_to_older_version(latest_builder, mocker): + builder, opts = latest_builder + + mocker.patch.object( + builder, + "_Builder__dump_vs_loc", + return_value=[ + (r"C:\VS\17\Enterprise", "17"), + (r"C:\VS\18\Enterprise", "18"), + ], + ) + + def _check_install_side_effect(_opts, path, _assert): + if "18" in path: + raise CheckVsInstallError("vcvars not found") + return _FAKE_VS_ENV + + mocker.patch.object( + builder, + "_Builder__check_vs_install", + side_effect=_check_install_side_effect, + ) + + builder._Builder__check_vs(opts) + + assert opts.vs_ver == "17" + assert builder.vs_ver_year == "vs2022" + + +def test_check_vs_install_path_major_extracted_from_path(latest_builder, mocker): + builder, opts = latest_builder + opts.vs_install_path = r"C:\Program Files\Microsoft Visual Studio\18\Enterprise" + + mocker.patch.object( + builder, "_Builder__check_vs_install", return_value=_FAKE_VS_ENV + ) + + builder._Builder__check_vs(opts) + + assert opts.vs_ver == "18" + assert builder.vs_ver_year == "vs2026"