Skip to content
Merged
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
1 change: 1 addition & 0 deletions changes/2855.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The minimum supported Windows build number can now be specified using the `min_os_version` option.
5 changes: 5 additions & 0 deletions docs/en/reference/platforms/windows/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ The digest algorithm to request the Timestamp Authority server uses for the time

The following options can be added to the `tool.briefcase.app.<appname>.windows` section of your `pyproject.toml` file.

#### `min_os_version`

The minimum [Windows build number](https://en.wikipedia.org/wiki/List_of_Microsoft_Windows_versions) that the app will support.
This is used by MSI installers to block installation on unsupported versions.

#### `dotnet_version` { #dotnet-version }

The minimum .NET runtime version required by the application, as a version string (e.g., `"10.0.0"`). This is used by MSI installers to verify that the required runtime is installed before installing the app. If this value is not set, no .NET runtime check is performed.
Expand Down
32 changes: 32 additions & 0 deletions src/briefcase/platforms/windows/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,18 @@ def verify_host(self):
Install a 64bit version of Python and run Briefcase again.
""")

def target_windows_build(self, app: FinalizedAppConfig) -> int | None:
"""The minimum supported Windows build number for the app from
``briefcase.toml``.

:param app: The config object for the app
:return: version or None if one isn't specified
"""
try:
return self.briefcase_toml(app)["briefcase"]["target_windows_build"]
except KeyError:
return None


class WindowsCreateCommand(CreateCommand):
def support_package_filename(self, support_revision):
Expand Down Expand Up @@ -243,6 +255,26 @@ def _cleanup_app_support_package(self, support_path):
""",
)

def _install_app_requirements(
self,
app: FinalizedAppConfig,
requires: list[str],
app_packages_path: Path,
**kwargs,
):
if template_min_version := self.target_windows_build(app):
min_version = int(getattr(app, "min_os_version", template_min_version))
if min_version < int(template_min_version):
raise BriefcaseCommandError(
"Your Windows app specifies a minimum build number of "
f"{min_version}, but the app template only supports "
f"{template_min_version}"
)

return super()._install_app_requirements(
app, requires, app_packages_path, **kwargs
)

def install_license(self, app: FinalizedAppConfig):
"""Install the license for the project as a single RTF document.

Expand Down
2 changes: 2 additions & 0 deletions tests/platforms/windows/app/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ def first_app_templated(first_app_config, tmp_path):
dedent(
"""\
[paths]
app_path = "src/app"
app_packages_path = "src/app_packages"
extras_path = "custom_extras"
"""
),
Expand Down
60 changes: 59 additions & 1 deletion tests/platforms/windows/app/create/test_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
import pytest
from packaging.version import Version

from briefcase.exceptions import UnsupportedHostError
from briefcase.exceptions import BriefcaseCommandError, UnsupportedHostError
from briefcase.integrations.subprocess import Subprocess
from briefcase.platforms.windows.app import WindowsAppCreateCommand


Expand Down Expand Up @@ -259,3 +260,60 @@ def test_external(create_command, external_first_app, tmp_path):
context = create_command.output_format_template_context(external_first_app)
assert context["package_path"] == str(tmp_path / "base_path/external/src")
assert context["binary_path"] == "internal/app.exe"


@pytest.mark.parametrize(
("template_version", "app_version", "compatible"),
[
(10240, 7601, False),
(10240, 10240, True),
(10240, 17763, True),
(None, 10240, True),
(10240, None, True),
(None, None, True),
# Values provided as strings are converted to int
("10240", "7601", False),
(10240, "7601", False),
("10240", 7601, False),
],
)
def test_min_os_version(
create_command,
first_app_templated,
template_version,
app_version,
compatible,
):
"""If the app defines a min OS version that is incompatible with the app template,
an error is raised."""
first_app_templated.requires = ["first", "second==1.2.3", "third>=3.2.1"]
create_command.target_windows_build = MagicMock(return_value=template_version)
if app_version:
first_app_templated.min_os_version = app_version
create_command.tools[first_app_templated].app_context = MagicMock(
spec_set=Subprocess
)
if not compatible:
with pytest.raises(
BriefcaseCommandError,
match=(
f"Your Windows app specifies a minimum build number of {app_version}, "
f"but the app template only supports {template_version}"
),
):
create_command.install_app_requirements(first_app_templated)
create_command.tools[first_app_templated].app_context.run.assert_not_called()
else:
create_command.install_app_requirements(first_app_templated)
create_command.tools[first_app_templated].app_context.run.assert_called()


def test_target_windows_build(create_command, first_app_templated):
"Test that the target Windows build is returned"

create_command._briefcase_toml[first_app_templated] = {"briefcase": {}}
assert create_command.target_windows_build(first_app_templated) is None
create_command._briefcase_toml[first_app_templated] = {
"briefcase": {"target_windows_build": 10240}
}
assert create_command.target_windows_build(first_app_templated) == 10240
24 changes: 24 additions & 0 deletions tests/platforms/windows/visualstudio/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from textwrap import dedent

import pytest

from ....utils import create_file


@pytest.fixture
def first_app_templated(first_app_config, tmp_path):
bundle_path = tmp_path / "base_path/build/first-app/windows/visualstudio"

create_file(
bundle_path / "briefcase.toml",
dedent(
"""\
[paths]
app_path = "src/app"
app_packages_path = "src/app_packages"
extras_path = "custom_extras"
"""
),
)

return first_app_config
57 changes: 57 additions & 0 deletions tests/platforms/windows/visualstudio/test_create.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from unittest.mock import MagicMock

import pytest

from briefcase.exceptions import BriefcaseCommandError
from briefcase.integrations.subprocess import Subprocess
from briefcase.platforms.windows.visualstudio import WindowsVisualStudioCreateCommand

# Most tests and fixtures are the same for both "app" and "visualstudio". This file only
Expand All @@ -22,3 +26,56 @@ def test_package_path(create_command, first_app_config, tmp_path):
assert context["package_path"] == str(
tmp_path / "base_path/build/first-app/windows/visualstudio/x64/Release"
)


@pytest.mark.parametrize(
("template_version", "app_version", "compatible"),
[
(10240, 7601, False),
(10240, 10240, True),
(10240, 17763, True),
(None, 10240, True),
(10240, None, True),
(None, None, True),
],
)
def test_min_os_version(
create_command,
first_app_templated,
template_version,
app_version,
compatible,
):
"""If the app defines a min OS version that is incompatible with the app template,
an error is raised."""
first_app_templated.requires = ["first", "second==1.2.3", "third>=3.2.1"]
create_command.target_windows_build = MagicMock(return_value=template_version)
if app_version:
first_app_templated.min_os_version = app_version
create_command.tools[first_app_templated].app_context = MagicMock(
spec_set=Subprocess
)
if not compatible:
with pytest.raises(
BriefcaseCommandError,
match=(
f"Your Windows app specifies a minimum build number of {app_version}, "
f"but the app template only supports {template_version}"
),
):
create_command.install_app_requirements(first_app_templated)
create_command.tools[first_app_templated].app_context.run.assert_not_called()
else:
create_command.install_app_requirements(first_app_templated)
create_command.tools[first_app_templated].app_context.run.assert_called()


def test_target_windows_build(create_command, first_app_templated):
"Test that the target Windows build is returned"

create_command._briefcase_toml[first_app_templated] = {"briefcase": {}}
assert create_command.target_windows_build(first_app_templated) is None
create_command._briefcase_toml[first_app_templated] = {
"briefcase": {"target_windows_build": 10240}
}
assert create_command.target_windows_build(first_app_templated) == 10240