Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion gvsbuild/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ class Configuration(str, Enum):


class VsVer(str, Enum):
latest = "latest"
vs2013 = "12"
vs2015 = "14"
vs2017 = "15"
Expand Down Expand Up @@ -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,
Expand Down
74 changes: 50 additions & 24 deletions gvsbuild/utils/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}"
Expand All @@ -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:
Expand Down Expand Up @@ -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)")
)
Expand All @@ -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:
Expand Down Expand Up @@ -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}')

Expand All @@ -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():
Expand Down
130 changes: 129 additions & 1 deletion tests/utils/test_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Loading