diff --git a/README.md b/README.md
index 89d2bde..14a7a38 100644
--- a/README.md
+++ b/README.md
@@ -66,3 +66,62 @@ You can also ensure that the virtual environment is recreated by using the `--cl
```bash
poetry bundle venv /path/to/environment --clear
```
+
+#### --platform option (Experimental)
+This option allows you to specify a target platform for binary wheel selection, allowing you to install wheels for
+architectures/platforms other than the host system.
+
+The primary use case is in CI/CD operations to produce a deployable asset, such as a ZIP file for AWS Lambda and other
+such cloud providers. It is common for the runtimes of these target environments to be different enough from the CI/CD's
+runner host such that the binary wheels selected using the host's criteria are not compatible with the target system's.
+
+#### Supported platform values
+The `--platform` option requires a value that conforms to the [Python Packaging Platform Tag format](
+https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/#platform-tag). Only the following
+"families" are supported at this time:
+- `manylinux`
+- `musllinux`
+- `macosx`
+
+#### Examples of valid platform tags
+This is not a comprehensive list, but illustrates typical examples.
+
+`manylinux_2_28_x86_64`, `manylinux1_x86_64`, `manylinux2010_x86_64`, `manylinux2014_x86_64`
+
+`musllinux_1_2_x86_64`
+
+`macosx_10_9_x86_64`, `macosx_10_9_intel`, `macosx_11_1_universal2`, `macosx_11_0_arm64`
+
+#### Example use case for AWS Lambda
+As an example of one motivating use case for this option, consider the AWS Lambda "serverless" execution environment.
+Depending upon which Python version you configure for your runtime, you may get different versions of the Linux system
+runtime. When dealing with pre-compiled binary wheels, these runtime differences can matter. If a shared library from
+a wheel is packaged in your deployment artifact that is incompatible with the runtime provided environment, then your
+Python "function" will error at execution time in the serverless environment.
+The issue arises when the build system that is producing the deployment artifact has a materially different platform
+from the selected serverless Lambda runtime.
+
+For example, the Python 3.11 Lambda runtime is Amazon Linux 2, which includes the Glibc 2.26 library. If a Python
+application is packaged and deployed in this environment that contains wheels built for a more recent version of Glibc,
+then a runtime error will result. This is likely to occur even if the build system is the same CPU architecture
+(e.g. x86_64) and core platform (e.g. Linux) and there is a package dependency that provides multiple precompiled
+wheels for various Glibc (or other system library) versions. The "best" wheel in the context of the build system can
+differ from that of the target execution environment.
+
+
+#### Limitations
+**This is not an actual cross-compiler**. Nor is it a containerized compilation/build environment. It simply allows
+controlling which **prebuilt** binaries are selected. It is not a replacement for cross-compilation or containerized
+builds for use cases requiring that.
+
+If there is not a binary wheel distribution compatible with the specified platform, then the package's source
+distribution is selected. If there are compile/build steps for "extensions" that need to run for the source
+distribution, then these operations will execute in the context of the host CI/build system.
+**This means that the `--platform` option
+has no impact on any extension compile/build operations that must occur during package installation.**
+This feature is only for
+**selecting** prebuilt wheels, and **not for compiling** them from source.
+
+Arguably, in a vast number of use cases, prebuilt wheel binaries are available for your packages and simply selecting
+them based on a platform other than the host CI/build system is much faster and simpler than heavier build-from-source
+alternatives.
diff --git a/src/poetry_plugin_bundle/bundlers/venv_bundler.py b/src/poetry_plugin_bundle/bundlers/venv_bundler.py
index 8de42ca..952fa4d 100644
--- a/src/poetry_plugin_bundle/bundlers/venv_bundler.py
+++ b/src/poetry_plugin_bundle/bundlers/venv_bundler.py
@@ -12,6 +12,7 @@
from cleo.io.outputs.section_output import SectionOutput
from poetry.poetry import Poetry
from poetry.repositories.lockfile_repository import LockfileRepository
+ from poetry.utils.env import Env
class VenvBundler(Bundler):
@@ -23,6 +24,7 @@ def __init__(self) -> None:
self._remove: bool = False
self._activated_groups: set[str] | None = None
self._compile: bool = False
+ self._platform: str | None = None
def set_path(self, path: Path) -> VenvBundler:
self._path = path
@@ -49,6 +51,11 @@ def set_compile(self, compile: bool = False) -> VenvBundler:
return self
+ def set_platform(self, platform: str | None) -> VenvBundler:
+ self._platform = platform
+
+ return self
+
def bundle(self, poetry: Poetry, io: IO) -> bool:
from pathlib import Path
from tempfile import TemporaryDirectory
@@ -60,7 +67,6 @@ def bundle(self, poetry: Poetry, io: IO) -> bool:
from poetry.installation.installer import Installer
from poetry.installation.operations.install import Install
from poetry.packages.locker import Locker
- from poetry.utils.env import Env
from poetry.utils.env import EnvManager
from poetry.utils.env.python import Python
from poetry.utils.env.python.exceptions import InvalidCurrentPythonVersionError
@@ -134,6 +140,9 @@ def create_venv_at_path(
)
env = manager.create_venv_at_path(self._path, python=python, force=True)
+ if self._platform:
+ self._constrain_env_platform(env, self._platform)
+
self._write(io, f"{message}: Installing dependencies")
class CustomLocker(Locker):
@@ -239,3 +248,12 @@ def _write(self, io: IO | SectionOutput, message: str) -> None:
return
io.overwrite(message)
+
+ def _constrain_env_platform(self, env: Env, platform: str) -> None:
+ """
+ Set the argument environment's supported tags
+ based on the configured platform override.
+ """
+ from poetry_plugin_bundle.utils.platforms import create_supported_tags
+
+ env._supported_tags = create_supported_tags(platform, env)
diff --git a/src/poetry_plugin_bundle/console/commands/bundle/venv.py b/src/poetry_plugin_bundle/console/commands/bundle/venv.py
index 15fb9a4..70eaf99 100644
--- a/src/poetry_plugin_bundle/console/commands/bundle/venv.py
+++ b/src/poetry_plugin_bundle/console/commands/bundle/venv.py
@@ -45,6 +45,17 @@ class BundleVenvCommand(BundleCommand):
" because the old installer always compiles.)",
flag=True,
),
+ option(
+ "platform",
+ None,
+ (
+ "Only use wheels compatible with the specified platform."
+ " Otherwise the default behavior uses the platform"
+ " of the running system. (Experimental)"
+ ),
+ flag=False,
+ value_required=True,
+ ),
]
bundler_name = "venv"
@@ -54,4 +65,5 @@ def configure_bundler(self, bundler: VenvBundler) -> None: # type: ignore[overr
bundler.set_executable(self.option("python"))
bundler.set_remove(self.option("clear"))
bundler.set_compile(self.option("compile"))
+ bundler.set_platform(self.option("platform"))
bundler.set_activated_groups(self.activated_groups)
diff --git a/src/poetry_plugin_bundle/utils/__init__.py b/src/poetry_plugin_bundle/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/poetry_plugin_bundle/utils/platforms.py b/src/poetry_plugin_bundle/utils/platforms.py
new file mode 100644
index 0000000..0ee69ae
--- /dev/null
+++ b/src/poetry_plugin_bundle/utils/platforms.py
@@ -0,0 +1,158 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import TYPE_CHECKING
+
+
+if TYPE_CHECKING:
+ from packaging.tags import Tag
+ from poetry.utils.env import Env
+
+
+@dataclass
+class PlatformTagParseResult:
+ platform: str
+ version_major: int
+ version_minor: int
+ arch: str
+
+ @staticmethod
+ def parse(tag: str) -> PlatformTagParseResult:
+ import re
+
+ match = re.match("([a-z]+)_([0-9]+)_([0-9]+)_(.*)", tag)
+ if not match:
+ raise ValueError(f"Invalid platform tag: {tag}")
+ platform, version_major_str, version_minor_str, arch = match.groups()
+ return PlatformTagParseResult(
+ platform=platform,
+ version_major=int(version_major_str),
+ version_minor=int(version_minor_str),
+ arch=arch,
+ )
+
+ def to_tag(self) -> str:
+ return "_".join(
+ [self.platform, str(self.version_major), str(self.version_minor), self.arch]
+ )
+
+
+def create_supported_tags(platform: str, env: Env) -> list[Tag]:
+ """
+ Given a platform specifier string, generate a list of compatible tags
+ for the argument environment's interpreter.
+
+ Refer to:
+ https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/#platform-tag
+ https://pip.pypa.io/en/stable/cli/pip_install/#cmdoption-platform
+ """
+ from packaging.tags import INTERPRETER_SHORT_NAMES
+ from packaging.tags import compatible_tags
+ from packaging.tags import cpython_tags
+ from packaging.tags import generic_tags
+
+ if platform.startswith("manylinux"):
+ supported_platforms = create_supported_manylinux_platforms(platform)
+ elif platform.startswith("musllinux"):
+ supported_platforms = create_supported_musllinux_platforms(platform)
+ elif platform.startswith("macosx"):
+ supported_platforms = create_supported_macosx_platforms(platform)
+ else:
+ raise NotImplementedError(f"Platform {platform} not supported")
+
+ python_implementation = env.python_implementation.lower()
+ python_version = env.version_info[:2]
+ interpreter_name = INTERPRETER_SHORT_NAMES.get(
+ python_implementation, python_implementation
+ )
+ interpreter = None
+
+ if interpreter_name == "cp":
+ tags = list(
+ cpython_tags(python_version=python_version, platforms=supported_platforms)
+ )
+ interpreter = f"{interpreter_name}{python_version[0]}{python_version[1]}"
+ else:
+ tags = list(
+ generic_tags(
+ interpreter=interpreter, abis=[], platforms=supported_platforms
+ )
+ )
+
+ tags.extend(
+ compatible_tags(
+ interpreter=interpreter,
+ python_version=python_version,
+ platforms=supported_platforms,
+ )
+ )
+
+ return tags
+
+
+def create_supported_manylinux_platforms(platform: str) -> list[str]:
+ """
+ https://peps.python.org/pep-0600/
+ manylinux_${GLIBCMAJOR}_${GLIBCMINOR}_${ARCH}
+
+ For now, only GLIBCMAJOR "2" is supported. It is unclear if there will be a need to support a future major
+ version like "3" and if specified, how generate the compatible 2.x version tags.
+ """
+ # Implementation based on https://peps.python.org/pep-0600/#package-installers
+
+ tag = normalize_legacy_manylinux_alias(platform)
+
+ parsed = PlatformTagParseResult.parse(tag)
+ return [
+ f"{parsed.platform}_{parsed.version_major}_{tag_minor}_{parsed.arch}"
+ for tag_minor in range(parsed.version_minor, -1, -1)
+ ]
+
+
+LEGACY_MANYLINUX_ALIASES = {
+ "manylinux1": "manylinux_2_5",
+ "manylinux2010": "manylinux_2_12",
+ "manylinux2014": "manylinux_2_17",
+}
+
+
+def normalize_legacy_manylinux_alias(tag: str) -> str:
+ tag_os_index_end = tag.index("_")
+ tag_os = tag[:tag_os_index_end]
+ tag_arch_suffix = tag[tag_os_index_end:]
+ os_replacement = LEGACY_MANYLINUX_ALIASES.get(tag_os)
+ if not os_replacement:
+ return tag
+
+ return os_replacement + tag_arch_suffix
+
+
+def create_supported_macosx_platforms(platform: str) -> list[str]:
+ import re
+
+ from packaging.tags import mac_platforms
+
+ match = re.match("macosx_([0-9]+)_([0-9]+)_(.*)", platform)
+ if not match:
+ raise ValueError(f"Invalid macosx tag: {platform}")
+ tag_major_str, tag_minor_str, tag_arch = match.groups()
+ tag_major_max = int(tag_major_str)
+ tag_minor_max = int(tag_minor_str)
+
+ return list(mac_platforms(version=(tag_major_max, tag_minor_max), arch=tag_arch))
+
+
+def create_supported_musllinux_platforms(platform: str) -> list[str]:
+ import re
+
+ match = re.match("musllinux_([0-9]+)_([0-9]+)_(.*)", platform)
+ if not match:
+ raise ValueError(f"Invalid musllinux tag: {platform}")
+ tag_major_str, tag_minor_str, tag_arch = match.groups()
+ tag_major_max = int(tag_major_str)
+ tag_minor_max = int(tag_minor_str)
+
+ return [
+ f"musllinux_{tag_major_max}_{minor}_{tag_arch}"
+ for minor in range(tag_minor_max, -1, -1)
+ ]
diff --git a/tests/bundlers/test_venv_bundler.py b/tests/bundlers/test_venv_bundler.py
index 3ddaf8f..d8570c1 100644
--- a/tests/bundlers/test_venv_bundler.py
+++ b/tests/bundlers/test_venv_bundler.py
@@ -11,6 +11,7 @@
from cleo.formatters.style import Style
from cleo.io.buffered_io import BufferedIO
from poetry.core.packages.package import Package
+from poetry.core.packages.utils.link import Link
from poetry.factory import Factory
from poetry.installation.operations.install import Install
from poetry.puzzle.exceptions import SolverProblemError
@@ -422,3 +423,79 @@ def test_bundler_non_package_mode(
• Bundled simple-project-non-package-mode (1.2.3) into {path}
"""
assert expected == io.fetch_output()
+
+
+def test_bundler_platform_override(
+ io: BufferedIO, tmpdir: str, mocker: MockerFixture, config: Config
+) -> None:
+ poetry = Factory().create_poetry(
+ Path(__file__).parent.parent / "fixtures" / "project_with_binary_wheel"
+ )
+ poetry.set_config(config)
+
+ def get_links_fake(package: Package) -> list[Link]:
+ return [Link(f"https://example.com/{file['file']}") for file in package.files]
+
+ mocker.patch(
+ "poetry.installation.chooser.Chooser._get_links", side_effect=get_links_fake
+ )
+ mocker.patch("poetry.installation.executor.Executor._execute_uninstall")
+ mocker.patch("poetry.installation.executor.Executor._execute_update")
+ mock_download_link = mocker.patch(
+ "poetry.installation.executor.Executor._download_link"
+ )
+ mocker.patch("poetry.installation.wheel_installer.WheelInstaller.install")
+
+ def get_installed_links() -> dict[str, str]:
+ return {
+ call[0][0].package.name: call[0][1].filename
+ for call in mock_download_link.call_args_list
+ }
+
+ bundler = VenvBundler()
+ bundler.set_path(Path(tmpdir))
+ bundler.set_remove(True)
+
+ bundler.set_platform("manylinux_2_28_x86_64")
+ bundler.bundle(poetry, io)
+ installed_link_by_package = get_installed_links()
+ assert "manylinux_2_28_x86_64" in installed_link_by_package["cryptography"]
+ assert "manylinux_2_17_x86_64" in installed_link_by_package["cffi"]
+ assert "py3-none-any.whl" in installed_link_by_package["pycparser"]
+
+ bundler.set_platform("manylinux2014_x86_64")
+ bundler.bundle(poetry, io)
+ installed_link_by_package = get_installed_links()
+ assert "manylinux2014_x86_64" in installed_link_by_package["cryptography"]
+ assert "manylinux_2_17_x86_64" in installed_link_by_package["cffi"]
+ assert "py3-none-any.whl" in installed_link_by_package["pycparser"]
+
+ bundler.set_platform("macosx_10_9_x86_64")
+ bundler.bundle(poetry, io)
+ installed_link_by_package = get_installed_links()
+ assert "macosx_10_9_universal2" in installed_link_by_package["cryptography"]
+ expected_cffi_platform = (
+ "macosx_10_9_x86_64" if sys.version_info < (3, 13) else "cffi-1.17.1.tar.gz"
+ )
+ assert expected_cffi_platform in installed_link_by_package["cffi"]
+ assert "py3-none-any.whl" in installed_link_by_package["pycparser"]
+
+ bundler.set_platform("macosx_11_0_arm64")
+ bundler.bundle(poetry, io)
+ installed_link_by_package = get_installed_links()
+ assert "macosx_10_9_universal2" in installed_link_by_package["cryptography"]
+ expected_cffi_platform = (
+ "macosx_11_0_arm64" if sys.version_info >= (3, 9) else "cffi-1.17.1.tar.gz"
+ )
+ assert expected_cffi_platform in installed_link_by_package["cffi"]
+ assert "py3-none-any.whl" in installed_link_by_package["pycparser"]
+
+ bundler.set_platform("musllinux_1_2_aarch64")
+ bundler.bundle(poetry, io)
+ installed_link_by_package = get_installed_links()
+ assert "musllinux_1_2_aarch64" in installed_link_by_package["cryptography"]
+ expected_cffi_platform = (
+ "musllinux_1_1_aarch64" if sys.version_info >= (3, 9) else "cffi-1.17.1.tar.gz"
+ )
+ assert expected_cffi_platform in installed_link_by_package["cffi"]
+ assert "py3-none-any.whl" in installed_link_by_package["pycparser"]
diff --git a/tests/fixtures/project_with_binary_wheel/README.md b/tests/fixtures/project_with_binary_wheel/README.md
new file mode 100644
index 0000000..e69de29
diff --git a/tests/fixtures/project_with_binary_wheel/poetry.lock b/tests/fixtures/project_with_binary_wheel/poetry.lock
new file mode 100644
index 0000000..dc3e369
--- /dev/null
+++ b/tests/fixtures/project_with_binary_wheel/poetry.lock
@@ -0,0 +1,145 @@
+# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
+
+[[package]]
+name = "cffi"
+version = "1.17.1"
+description = "Foreign Function Interface for Python calling C code."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
+ {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
+ {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"},
+ {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"},
+ {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"},
+ {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"},
+ {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"},
+ {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"},
+ {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"},
+ {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"},
+ {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"},
+ {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"},
+ {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"},
+ {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"},
+ {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"},
+ {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"},
+ {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"},
+ {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"},
+ {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"},
+ {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"},
+ {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"},
+ {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"},
+ {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"},
+ {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"},
+ {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"},
+ {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"},
+ {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"},
+ {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"},
+ {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"},
+ {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"},
+ {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"},
+ {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"},
+ {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"},
+ {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"},
+ {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"},
+ {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"},
+ {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"},
+ {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"},
+ {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"},
+ {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"},
+ {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"},
+ {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"},
+ {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"},
+ {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"},
+ {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"},
+ {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"},
+ {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"},
+ {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"},
+ {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"},
+ {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"},
+ {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"},
+ {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"},
+ {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"},
+ {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"},
+ {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"},
+ {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"},
+ {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"},
+ {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"},
+ {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"},
+ {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"},
+ {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"},
+ {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"},
+ {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"},
+ {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"},
+ {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"},
+ {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
+ {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
+]
+
+[package.dependencies]
+pycparser = "*"
+
+[[package]]
+name = "cryptography"
+version = "43.0.1"
+description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"},
+ {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"},
+ {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"},
+ {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"},
+ {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"},
+ {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"},
+ {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"},
+ {file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"},
+ {file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"},
+ {file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"},
+ {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"},
+ {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"},
+ {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"},
+ {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"},
+ {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"},
+ {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"},
+ {file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"},
+ {file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"},
+ {file = "cryptography-43.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034"},
+ {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d"},
+ {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289"},
+ {file = "cryptography-43.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84"},
+ {file = "cryptography-43.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365"},
+ {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96"},
+ {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172"},
+ {file = "cryptography-43.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2"},
+ {file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"},
+]
+
+[package.dependencies]
+cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
+
+[package.extras]
+docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
+docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"]
+nox = ["nox"]
+pep8test = ["check-sdist", "click", "mypy", "ruff"]
+sdist = ["build"]
+ssh = ["bcrypt (>=3.1.5)"]
+test = ["certifi", "cryptography-vectors (==43.0.1)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
+test-randomorder = ["pytest-randomly"]
+
+[[package]]
+name = "pycparser"
+version = "2.22"
+description = "C parser in Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
+ {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
+]
+
+[metadata]
+lock-version = "2.0"
+python-versions = "^3.8"
+content-hash = "19d494cbff48b9b5b1224dd0e296ad5bbe66bba8708606aa0339fc60033f5b2d"
diff --git a/tests/fixtures/project_with_binary_wheel/project_with_binary_wheel/__init__.py b/tests/fixtures/project_with_binary_wheel/project_with_binary_wheel/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/fixtures/project_with_binary_wheel/pyproject.toml b/tests/fixtures/project_with_binary_wheel/pyproject.toml
new file mode 100644
index 0000000..4776aaa
--- /dev/null
+++ b/tests/fixtures/project_with_binary_wheel/pyproject.toml
@@ -0,0 +1,15 @@
+[tool.poetry]
+name = "project-with-binary-wheel"
+version = "0.1.0"
+description = ""
+authors = ["BrandonLWhite "]
+readme = "README.md"
+
+[tool.poetry.dependencies]
+python = "^3.8"
+cryptography = "^43.0.1"
+
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/utils/test_platforms.py b/tests/utils/test_platforms.py
new file mode 100644
index 0000000..2cb42b3
--- /dev/null
+++ b/tests/utils/test_platforms.py
@@ -0,0 +1,164 @@
+from __future__ import annotations
+
+import pytest
+
+from poetry.utils.env import MockEnv
+
+import poetry_plugin_bundle.utils.platforms as platforms
+
+
+def _get_supported_tags_set(
+ platform: str, python_version_info: tuple[int, int, int]
+) -> set[str]:
+ env = MockEnv(version_info=python_version_info)
+ result = platforms.create_supported_tags(platform, env)
+ return {str(tag) for tag in result}
+
+
+def _test_create_supported_tags(
+ platform: str,
+ python_version_info: tuple[int, int, int],
+ expected_tags: set[str],
+ unexpected_tags: set[str],
+) -> None:
+ result_set = _get_supported_tags_set(platform, python_version_info)
+
+ assert result_set.issuperset(expected_tags)
+ assert not result_set.intersection(unexpected_tags)
+
+
+def test_create_supported_tags_manylinux() -> None:
+ _test_create_supported_tags(
+ platform="manylinux_2_12_x86_64",
+ python_version_info=(3, 12, 1),
+ expected_tags={
+ "cp312-cp312-manylinux_2_12_x86_64",
+ "cp312-none-manylinux_2_12_x86_64",
+ "cp312-abi3-manylinux_2_12_x86_64",
+ "cp312-cp312-manylinux_2_1_x86_64",
+ "cp311-abi3-manylinux_2_12_x86_64",
+ "py312-none-manylinux_2_12_x86_64",
+ "py312-none-any",
+ "cp312-none-any",
+ },
+ unexpected_tags={
+ "cp313-cp313-manylinux_2_12_x86_64",
+ "cp312-cp312-manylinux_2_13_x86_64",
+ },
+ )
+
+
+def test_create_supported_tags_legacy_manylinux_aliases() -> None:
+ _test_create_supported_tags(
+ platform="manylinux1_x86_64",
+ python_version_info=(3, 10, 2),
+ expected_tags={
+ "cp310-cp310-manylinux_2_5_x86_64",
+ "cp310-cp310-manylinux_2_1_x86_64",
+ },
+ unexpected_tags={
+ "cp310-cp310-manylinux_2_6_x86_64",
+ },
+ )
+
+ _test_create_supported_tags(
+ platform="manylinux2010_x86_64",
+ python_version_info=(3, 10, 2),
+ expected_tags={
+ "cp310-cp310-manylinux_2_12_x86_64",
+ "cp310-cp310-manylinux_2_1_x86_64",
+ },
+ unexpected_tags={
+ "cp310-cp310-manylinux_2_13_x86_64",
+ },
+ )
+
+ _test_create_supported_tags(
+ platform="manylinux2014_x86_64",
+ python_version_info=(3, 11, 9),
+ expected_tags={
+ "cp311-cp311-manylinux_2_17_x86_64",
+ "cp311-cp311-manylinux_2_1_x86_64",
+ },
+ unexpected_tags={
+ "cp311-cp311-manylinux_2_24_x86_64",
+ },
+ )
+
+
+def test_create_supported_tags_macosx() -> None:
+ _test_create_supported_tags(
+ platform="macosx_11_0_arm64",
+ python_version_info=(3, 11, 8),
+ expected_tags={
+ "cp311-abi3-macosx_11_0_arm64",
+ "cp311-abi3-macosx_10_12_universal2",
+ "cp311-none-macosx_11_0_universal2",
+ "py311-none-any",
+ },
+ unexpected_tags={
+ "cp311-none-macosx_11_1_universal2",
+ },
+ )
+
+ _test_create_supported_tags(
+ platform="macosx_10_9_x86_64",
+ python_version_info=(3, 11, 8),
+ expected_tags={
+ "cp311-abi3-macosx_10_9_universal2",
+ "cp311-abi3-macosx_10_9_x86_64",
+ "cp311-abi3-macosx_10_9_intel",
+ "cp311-none-macosx_10_7_universal2",
+ "py311-none-any",
+ },
+ unexpected_tags={
+ "cp311-none-macosx_11_1_universal2",
+ "cp311-abi3-macosx_11_0_arm64",
+ },
+ )
+
+
+def test_create_supported_tags_musllinux() -> None:
+ _test_create_supported_tags(
+ platform="musllinux_1_1_x86_64",
+ python_version_info=(3, 13, 1),
+ expected_tags={
+ "cp313-cp313-musllinux_1_1_x86_64",
+ "cp313-abi3-musllinux_1_1_x86_64",
+ "py312-none-any",
+ "cp313-none-any",
+ },
+ unexpected_tags={
+ "cp313-cp313-musllinux_1_2_x86_64",
+ },
+ )
+
+
+def test_create_supported_tags_unsupported_platform() -> None:
+ env = MockEnv(version_info=(3, 12, 1))
+
+ unsupported_platforms = [
+ "win32",
+ "linux",
+ "foobar",
+ ]
+ for platform in unsupported_platforms:
+ with pytest.raises(NotImplementedError):
+ platforms.create_supported_tags(platform, env)
+
+
+def test_create_supported_tags_malformed_platforms() -> None:
+ env = MockEnv(version_info=(3, 12, 1))
+
+ malformed_platforms = [
+ "macosx_11_blah_arm64",
+ "macosx",
+ "manylinux_blammo_12_x86_64",
+ "manylinux_x86_64",
+ "manylinux",
+ "musllinux_?_1_x86_64",
+ "musllinux",
+ ]
+ for platform in malformed_platforms:
+ with pytest.raises(ValueError):
+ platforms.create_supported_tags(platform, env)