diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 3015729f..b84ddaf7 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -340,6 +340,7 @@ class _WheelBuilder(): _manifest: Dict[str, List[_Entry]] _limited_api: bool _allow_windows_shared_libs: bool + _build_details: mesonpy._tags.BuildDetails @property def _has_internal_libs(self) -> bool: @@ -364,14 +365,14 @@ def _pure(self) -> bool: def tag(self) -> mesonpy._tags.Tag: """Wheel tags.""" if self._pure: - return mesonpy._tags.Tag('py3', 'none', 'any') + return mesonpy._tags.Tag('py3', 'none', 'any', build_details=self._build_details) if not self._has_extension_modules: # The wheel has platform dependent code (is not pure) but # does not contain any extension module (does not # distribute any file in {platlib}) thus use generic # implementation and ABI tags. - return mesonpy._tags.Tag('py3', 'none', None) - return mesonpy._tags.Tag(None, self._stable_abi, None) + return mesonpy._tags.Tag('py3', 'none', None, build_details=self._build_details) + return mesonpy._tags.Tag(None, self._stable_abi, None, build_details=self._build_details) @property def name(self) -> str: @@ -844,6 +845,27 @@ def __init__( ''') self._meson_native_file.write_text(native_file_data, encoding='utf-8') + # Starting with version 1.10, Meson can consume a `build-details.json` + # file following the specification is PEP 739 to obtain required + # information to build extension modules without having to run the + # interpreter. The path to the `build-details.json` can be specified + # passing the with the `-Dpython.build_config=` option to `meson + # setup`. Extract the value passed to this option and use the details + # in the `build-details.json` file to compute the wheel tag. + self._build_details: mesonpy._tags.BuildDetails = mesonpy._tags.introspect_build_details() + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument('-D', action='append', default=[]) + args, _ = parser.parse_known_args(self._meson_args['setup']) + for arg in reversed(args.D): + name, value = arg.split('=', 1) + if name == 'python.build_config': + try: + with open(value, 'r', encoding='utf8') as f: + self._build_details = json.load(f) + except OSError as err: + raise ConfigError(f'The file specified as "python.build_config" cannot be opened: {err}') from err + break + # reconfigure if we have a valid Meson build directory. Meson # uses the presence of the 'meson-private/coredata.dat' file # in the build directory as indication that the build @@ -1161,13 +1183,15 @@ def sdist(self, directory: Path) -> pathlib.Path: def wheel(self, directory: Path) -> pathlib.Path: """Generates a wheel in the specified directory.""" self.build() - builder = _WheelBuilder(self._metadata, self._manifest, self._limited_api, self._allow_windows_shared_libs) + builder = _WheelBuilder( + self._metadata, self._manifest, self._limited_api, self._allow_windows_shared_libs, self._build_details) return builder.build(directory) def editable(self, directory: Path) -> pathlib.Path: """Generates an editable wheel in the specified directory.""" self.build() - builder = _EditableWheelBuilder(self._metadata, self._manifest, self._limited_api, self._allow_windows_shared_libs) + builder = _EditableWheelBuilder( + self._metadata, self._manifest, self._limited_api, self._allow_windows_shared_libs, self._build_details) return builder.build(directory, self._source_dir, self._build_dir, self._build_command, self._editable_verbose) @@ -1333,7 +1357,7 @@ def build_editable( if not config_settings: config_settings = {} if 'build-dir' not in config_settings and 'builddir' not in config_settings: - config_settings['build-dir'] = 'build/' + mesonpy._tags.get_abi_tag() + config_settings['build-dir'] = 'build/' + mesonpy._tags.get_abi_tag(mesonpy._tags.introspect_build_details()) out = pathlib.Path(wheel_directory) with _project(config_settings) as project: diff --git a/mesonpy/_tags.py b/mesonpy/_tags.py index 00c86100..e0a91c0e 100644 --- a/mesonpy/_tags.py +++ b/mesonpy/_tags.py @@ -9,6 +9,27 @@ import struct import sys import sysconfig +import typing + + +if typing.TYPE_CHECKING: # pragma: no cover + from typing import TypedDict + + class _Abi(TypedDict): + extension_suffix: str + + class _ImplementationVersion(TypedDict): + major: int + minor: int + + class _Implementation(TypedDict): + name: str + version: _ImplementationVersion + + class BuildDetails(TypedDict): + abi: _Abi + implementation: _Implementation + platform: str # https://peps.python.org/pep-0425/#python-tag @@ -24,14 +45,91 @@ _32_BIT_INTERPRETER = struct.calcsize('P') == 4 -def get_interpreter_tag() -> str: - name = sys.implementation.name +def _get_macosx_platform() -> str: + ver, _, arch = platform.mac_ver() + major, minor = map(int, ver.split('.')[:2]) + + # Python built with older macOS SDK on macOS 11, reports an + # nonexistent macOS 10.16 version instead of the real version. + # + # The packaging module introduced a workaround + # https://github.com/pypa/packaging/commit/67c4a2820c549070bbfc4bfbf5e2a250075048da + # + # This results in packaging versions up to 21.3 generating + # platform tags like "macosx_10_16_x86_64" and later versions + # generating "macosx_11_0_x86_64". Using the latter would be more + # correct but prevents the resulting wheel from being installed on + # systems using packaging 21.3 or earlier (pip 22.3 or earlier). + # + # Fortunately packaging versions carrying the workaround still + # accepts "macosx_10_16_x86_64" as a compatible platform tag. We + # can therefore ignore the issue and generate the slightly + # incorrect tag. + + if _32_BIT_INTERPRETER: + # 32-bit Python running on a 64-bit kernel. + if arch == 'ppc64': + arch = 'ppc' + if arch == 'x86_64': + arch = 'i386' + + return f'macosx-{major}.{minor}-{arch}' + + +def _get_ios_platform() -> str: + ver = platform.ios_ver().release # type: ignore[attr-defined] + major, minor = map(int, ver.split('.')[:2]) + + # Although _multiarch is an internal implementation detail, it's a core part + # of how CPython is implemented on iOS; this attribute is also relied upon + # by `packaging` as part of tag determination. + multiarch = sys.implementation._multiarch.replace('-', '_') + + return f'ios-{major}.{minor}-{multiarch}' + + +def introspect_build_details() -> BuildDetails: + platform = sysconfig.get_platform() + if platform.startswith('macosx'): + platform = _get_macosx_platform() + elif platform.startswith('ios'): + platform = _get_ios_platform() + elif _32_BIT_INTERPRETER: + # 32-bit Python running on a 64-bit kernel. + if platform == 'linux-x86_64': + platform = 'linux_i686' + if platform == 'linux-aarch64': + platform = 'linux_armv7l' + + return { + 'abi': { + # PyPy reports a $SOABI that does not agree with $EXT_SUFFIX. + # Using $EXT_SUFFIX will not break when PyPy will fix this. + # See https://foss.heptapod.net/pypy/pypy/-/issues/3816 and + # https://github.com/pypa/packaging/pull/607. + 'extension_suffix': str(sysconfig.get_config_var('EXT_SUFFIX')), + }, + 'implementation': { + 'name': sys.implementation.name, + 'version': { + 'major': sys.version_info.major, + 'minor': sys.version_info.minor, + }, + }, + 'platform': platform, + } + + +def get_interpreter_tag(build_details: BuildDetails) -> str: + name = build_details['implementation']['name'] + _v = build_details['implementation']['version'] + major = _v['major'] + minor = _v['minor'] name = INTERPRETERS.get(name, name) - version = sys.version_info - return f'{name}{version[0]}{version[1]}' + return f'{name}{major}{minor}' -def get_abi_tag() -> str: +def get_abi_tag(build_details: BuildDetails) -> str: # The best solution to obtain the Python ABI is to parse the # $SOABI or $EXT_SUFFIX sysconfig variables as defined in PEP-314. @@ -39,7 +137,8 @@ def get_abi_tag() -> str: # Using $EXT_SUFFIX will not break when PyPy will fix this. # See https://foss.heptapod.net/pypy/pypy/-/issues/3816 and # https://github.com/pypa/packaging/pull/607. - empty, abi, ext = str(sysconfig.get_config_var('EXT_SUFFIX')).split('.') + ext_suffix = build_details['abi']['extension_suffix'] + empty, abi, ext = ext_suffix.split('.') # The packaging module initially based his understanding of the # $SOABI variable on the inconsistent value reported by PyPy, and @@ -60,8 +159,9 @@ def get_abi_tag() -> str: return abi.replace('.', '_').replace('-', '_') -def _get_macosx_platform_tag() -> str: - ver, _, arch = platform.mac_ver() +def _get_macosx_platform_tag(platform: str) -> str: + name, ver, arch = platform.split('-', 2) + assert name == 'macosx' # Override the architecture with the one provided in the # _PYTHON_HOST_PLATFORM environment variable. This environment @@ -81,24 +181,7 @@ def _get_macosx_platform_tag() -> str: parts = os.environ.get('MACOSX_DEPLOYMENT_TARGET', '').split('.')[:2] version = tuple(map(int, parts + ['0'] * (2 - len(parts)))) except ValueError: - version = tuple(map(int, ver.split('.')))[:2] - - # Python built with older macOS SDK on macOS 11, reports an - # nonexistent macOS 10.16 version instead of the real version. - # - # The packaging module introduced a workaround - # https://github.com/pypa/packaging/commit/67c4a2820c549070bbfc4bfbf5e2a250075048da - # - # This results in packaging versions up to 21.3 generating - # platform tags like "macosx_10_16_x86_64" and later versions - # generating "macosx_11_0_x86_64". Using the latter would be more - # correct but prevents the resulting wheel from being installed on - # systems using packaging 21.3 or earlier (pip 22.3 or earlier). - # - # Fortunately packaging versions carrying the workaround still - # accepts "macosx_10_16_x86_64" as a compatible platform tag. We - # can therefore ignore the issue and generate the slightly - # incorrect tag. + version = tuple(map(int, ver.split('.')[:2])) # The minimum macOS ABI version on arm64 is 11.0. The macOS SDK # on arm64 silently bumps any compatibility version specified via @@ -120,53 +203,39 @@ def _get_macosx_platform_tag() -> str: # the patch level. Reset the patch level to zero. minor = 0 - if _32_BIT_INTERPRETER: - # 32-bit Python running on a 64-bit kernel. - if arch == 'ppc64': - arch = 'ppc' - if arch == 'x86_64': - arch = 'i386' - return f'macosx_{major}_{minor}_{arch}' -def _get_ios_platform_tag() -> str: +def _get_ios_platform_tag(platform: str) -> str: + name, version, multiarch = platform.split('-', 2) + assert name == 'ios' + # Override the iOS version if one is provided via the # IPHONEOS_DEPLOYMENT_TARGET environment variable. try: parts = os.environ.get('IPHONEOS_DEPLOYMENT_TARGET', '').split('.')[:2] - version = tuple(map(int, parts + ['0'] * (2 - len(parts)))) + major, minor = map(int, parts + ['0'] * (2 - len(parts))) except ValueError: - version = tuple(map(int, platform.ios_ver().release.split('.')))[:2] # type: ignore[attr-defined] + major, minor = map(int, version.split('.')) - # Although _multiarch is an internal implementation detail, it's a core part - # of how CPython is implemented on iOS; this attribute is also relied upon - # by `packaging` as part of tag determination. - multiarch = sys.implementation._multiarch.replace('-', '_') - - return f'ios_{version[0]}_{version[1]}_{multiarch}' + return f'ios_{major}_{minor}_{multiarch.replace("-", "_")}' -def get_platform_tag() -> str: - platform = sysconfig.get_platform() +def get_platform_tag(build_details: BuildDetails) -> str: + platform = build_details['platform'] if platform.startswith('macosx'): - return _get_macosx_platform_tag() + return _get_macosx_platform_tag(platform) if platform.startswith('ios'): - return _get_ios_platform_tag() - if _32_BIT_INTERPRETER: - # 32-bit Python running on a 64-bit kernel. - if platform == 'linux-x86_64': - return 'linux_i686' - if platform == 'linux-aarch64': - return 'linux_armv7l' + return _get_ios_platform_tag(platform) return platform.replace('-', '_').replace('.', '_').lower() class Tag: - def __init__(self, interpreter: str | None = None, abi: str | None = None, platform: str | None = None): - self.interpreter = interpreter or get_interpreter_tag() - self.abi = abi or get_abi_tag() - self.platform = platform or get_platform_tag() + def __init__(self, interpreter: str | None = None, abi: str | None = None, platform: str | None = None, + *, build_details: BuildDetails): + self.interpreter = interpreter or get_interpreter_tag(build_details) + self.abi = abi or get_abi_tag(build_details) + self.platform = platform or get_platform_tag(build_details) def __str__(self) -> str: return f'{self.interpreter}-{self.abi}-{self.platform}' diff --git a/tests/test_project.py b/tests/test_project.py index 9700f853..1dc3146c 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -9,6 +9,7 @@ import sys import sysconfig import textwrap +import types from unittest.mock import Mock @@ -26,6 +27,7 @@ from mesonpy._util import chdir from .conftest import MESON_VERSION, in_git_repo_context, metadata, package_dir +from .test_tags import SYSTEM_BUILD_DETAILS def test_unsupported_python_version(package_unsupported_python_version): @@ -358,7 +360,7 @@ def test_archflags_envvar_parsing(package_purelib_and_platlib, monkeypatch, arch monkeypatch.setenv('ARCHFLAGS', archflags) arch = archflags.split()[-1] with mesonpy._project(): - assert mesonpy._tags.Tag().platform.endswith(arch) + assert mesonpy._tags.Tag(build_details=SYSTEM_BUILD_DETAILS).platform.endswith(arch) finally: # revert environment variable setting done by the in-process build os.environ.pop('_PYTHON_HOST_PLATFORM', None) @@ -438,6 +440,10 @@ def test_ios_project(package_simple, monkeypatch, multiarch, tmp_path): subsystem = 'ios-simulator' if abi == 'iphonesimulator' else 'ios' # Mock being on iOS + monkeypatch.setattr( + sys, 'implementation', + types.SimpleNamespace(**(sys.implementation.__dict__ | {'_multiarch': multiarch})) + ) monkeypatch.setattr(sys, 'platform', 'ios') monkeypatch.setattr(platform, 'machine', Mock(return_value=arch)) monkeypatch.setattr(sysconfig, 'get_platform', Mock(return_value=f'ios-13.0-{multiarch}')) diff --git a/tests/test_tags.py b/tests/test_tags.py index 0fb3fcfd..9f717c5d 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -2,7 +2,10 @@ # # SPDX-License-Identifier: MIT +from __future__ import annotations + import importlib.machinery +import json import os import pathlib import platform @@ -55,11 +58,12 @@ def get_abi3_suffix(): SUFFIX = sysconfig.get_config_var('EXT_SUFFIX') ABI3SUFFIX = get_abi3_suffix() STABLE_ABI_KIND = 'abi3t' if FREE_THREADED_BUILD and sys.version_info >= (3, 15) else 'abi3' +SYSTEM_BUILD_DETAILS = mesonpy._tags.introspect_build_details() def test_wheel_tag(): - assert str(mesonpy._tags.Tag()) == f'{INTERPRETER}-{ABI}-{PLATFORM}' - assert str(mesonpy._tags.Tag(abi='abi3')) == f'{INTERPRETER}-abi3-{PLATFORM}' + assert str(mesonpy._tags.Tag(build_details=SYSTEM_BUILD_DETAILS)) == f'{INTERPRETER}-{ABI}-{PLATFORM}' + assert str(mesonpy._tags.Tag(abi='abi3', build_details=SYSTEM_BUILD_DETAILS)) == f'{INTERPRETER}-abi3-{PLATFORM}' @pytest.mark.skipif(sys.platform != 'darwin', reason='macOS specific test') @@ -67,16 +71,16 @@ def test_macos_platform_tag(monkeypatch): for minor in range(9, 16): monkeypatch.setenv('MACOSX_DEPLOYMENT_TARGET', f'10.{minor}') version = (10, minor) if platform.mac_ver()[2] != 'arm64' else (11, 0) - assert next(packaging.tags.mac_platforms(version)) == mesonpy._tags.get_platform_tag() + assert next(packaging.tags.mac_platforms(version)) == mesonpy._tags.get_platform_tag(SYSTEM_BUILD_DETAILS) for major in range(11, 20): for minor in range(3): monkeypatch.setenv('MACOSX_DEPLOYMENT_TARGET', f'{major}.{minor}') - assert next(packaging.tags.mac_platforms((major, minor))) == mesonpy._tags.get_platform_tag() + assert next(packaging.tags.mac_platforms((major, minor))) == mesonpy._tags.get_platform_tag(SYSTEM_BUILD_DETAILS) for major in range(11, 13): monkeypatch.setenv('MACOSX_DEPLOYMENT_TARGET', f'{major}.0') - assert next(packaging.tags.mac_platforms((major, 0))) == mesonpy._tags.get_platform_tag() + assert next(packaging.tags.mac_platforms((major, 0))) == mesonpy._tags.get_platform_tag(SYSTEM_BUILD_DETAILS) monkeypatch.setenv('MACOSX_DEPLOYMENT_TARGET', f'{major}') - assert next(packaging.tags.mac_platforms((major, 0))) == mesonpy._tags.get_platform_tag() + assert next(packaging.tags.mac_platforms((major, 0))) == mesonpy._tags.get_platform_tag(SYSTEM_BUILD_DETAILS) @pytest.mark.skipif(sys.platform != 'darwin', reason='macOS specific test') @@ -84,17 +88,17 @@ def test_macos_platform_tag_arm64(monkeypatch): monkeypatch.setenv('_PYTHON_HOST_PLATFORM', 'macosx-12.0-arm64') # Verify that the minimum platform ABI version on arm64 is 11.0. monkeypatch.setenv('MACOSX_DEPLOYMENT_TARGET', '10.12') - assert mesonpy._tags.get_platform_tag() == 'macosx_11_0_arm64' + assert mesonpy._tags.get_platform_tag(SYSTEM_BUILD_DETAILS) == 'macosx_11_0_arm64' monkeypatch.setenv('MACOSX_DEPLOYMENT_TARGET', '12.34') - assert mesonpy._tags.get_platform_tag() == 'macosx_12_0_arm64' + assert mesonpy._tags.get_platform_tag(SYSTEM_BUILD_DETAILS) == 'macosx_12_0_arm64' @pytest.mark.skipif(sys.platform != 'darwin', reason='macOS specific test') def test_python_host_platform(monkeypatch): monkeypatch.setenv('_PYTHON_HOST_PLATFORM', 'macosx-12.0-arm64') - assert mesonpy._tags.get_platform_tag().endswith('arm64') + assert mesonpy._tags.get_platform_tag(SYSTEM_BUILD_DETAILS).endswith('arm64') monkeypatch.setenv('_PYTHON_HOST_PLATFORM', 'macosx-11.1-x86_64') - assert mesonpy._tags.get_platform_tag().endswith('x86_64') + assert mesonpy._tags.get_platform_tag(SYSTEM_BUILD_DETAILS).endswith('x86_64') @pytest.mark.skipif(sys.version_info < (3, 13), reason='requires Python 3.13 or higher') @@ -105,22 +109,23 @@ def test_ios_platform_tag(monkeypatch): monkeypatch.setattr(sysconfig, 'get_platform', Mock(return_value='ios-13.0-arm64-iphoneos')) ios_ver = platform.IOSVersionInfo('iOS', '13.0', 'iPhone', False) monkeypatch.setattr(platform, 'ios_ver', Mock(return_value=ios_ver)) + build_details = mesonpy._tags.introspect_build_details() # Check the default value - assert next(packaging.tags.ios_platforms((13, 0))) == mesonpy._tags.get_platform_tag() + assert next(packaging.tags.ios_platforms((13, 0))) == mesonpy._tags.get_platform_tag(build_details) # Check the value when IPHONEOS_DEPLOYMENT_TARGET is set. for major in range(13, 20): for minor in range(3): monkeypatch.setenv('IPHONEOS_DEPLOYMENT_TARGET', f'{major}.{minor}') - assert next(packaging.tags.ios_platforms((major, minor))) == mesonpy._tags.get_platform_tag() + assert next(packaging.tags.ios_platforms((major, minor))) == mesonpy._tags.get_platform_tag(build_details) def wheel_builder_test_factory(content, pure=True, limited_api=False): manifest = defaultdict(list) for key, value in content.items(): manifest[key] = [mesonpy._Entry(pathlib.Path(x), os.path.join('build', x)) for x in value] - return mesonpy._WheelBuilder(None, manifest, limited_api, False) + return mesonpy._WheelBuilder(None, manifest, limited_api, False, SYSTEM_BUILD_DETAILS) def test_tag_empty_wheel(): @@ -176,3 +181,62 @@ def test_tag_stable_abi_multiarch(): }, pure=False, limited_api=True) abi = 'abi3.abi3t' if STABLE_ABI_KIND == 'abi3t' else 'abi3' assert str(builder.tag) == f'{INTERPRETER}-{abi}-{PLATFORM}' + + +@pytest.mark.skipif(sys.platform == 'darwin', reason='build-details on macos disagree with system over deployment target') +def test_system_build_details(): + try: + with open(os.path.join(sysconfig.get_path('stdlib'), 'build-details.json'), encoding='utf8') as f: + build_details = json.load(f) + except FileNotFoundError: + return pytest.skip('build-details.json not found') + assert str(mesonpy._tags.Tag(build_details=SYSTEM_BUILD_DETAILS)) == str(mesonpy._tags.Tag(build_details=build_details)) + + +BUILD_DETAILS = { + 'linux-x86_64': { + 'platform': 'linux-x86_64', + 'implementation': {'name': 'cpython', 'version': {'major': 3, 'minor': 15}}, + 'abi': {'extension_suffix': '.cpython-315-x86_64-linux-gnu.so'} + }, + 'macosx-x86_64': { + 'platform': 'macosx-11.0-x86_64', + 'implementation': {'name': 'cpython', 'version': {'major': 3, 'minor': 14}}, + 'abi': {'extension_suffix': '.cpython-314-darwin.so'} + }, + 'macosx-arm64': { + 'platform': 'macosx-11.0-arm64', + 'implementation': {'name': 'cpython', 'version': {'major': 3, 'minor': 14}}, + 'abi': {'extension_suffix': '.cpython-314-darwin.so'} + }, +} + + +@pytest.mark.parametrize( + ('build_details', 'expected_tag'), + [ + (BUILD_DETAILS['linux-x86_64'], 'cp315-cp315-linux_x86_64'), + (BUILD_DETAILS['macosx-x86_64'], 'cp314-cp314-macosx_11_0_x86_64'), + (BUILD_DETAILS['macosx-arm64'], 'cp314-cp314-macosx_11_0_arm64'), + ] +) +def test_build_details(monkeypatch, build_details: mesonpy._tags.BuildDetails, expected_tag: str): + # this should not affect the result + monkeypatch.setattr(mesonpy._tags, '_32_BIT_INTERPRETER', True) + assert str(mesonpy._tags.Tag(build_details=build_details)) == expected_tag + + +@pytest.mark.parametrize( + ('version', 'expected_tag_x86_64', 'expected_tag_arm64'), + [ + ('10', 'cp314-cp314-macosx_10_0_x86_64', 'cp314-cp314-macosx_11_0_arm64'), + ('10.3', 'cp314-cp314-macosx_10_3_x86_64', 'cp314-cp314-macosx_11_0_arm64'), + ('11.2', 'cp314-cp314-macosx_11_0_x86_64', 'cp314-cp314-macosx_11_0_arm64'), + ] +) +def test_macos_deployment_target_overrides_build_details( + monkeypatch, version: str, expected_tag_x86_64: str, expected_tag_arm64: str +): + monkeypatch.setenv('MACOSX_DEPLOYMENT_TARGET', version) + assert str(mesonpy._tags.Tag(build_details=BUILD_DETAILS['macosx-x86_64'])) == expected_tag_x86_64 + assert str(mesonpy._tags.Tag(build_details=BUILD_DETAILS['macosx-arm64'])) == expected_tag_arm64 diff --git a/tests/test_wheel.py b/tests/test_wheel.py index 3fb3b429..54e0eadd 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -255,7 +255,7 @@ def test_entrypoints(wheel_full_metadata): def test_top_level_modules(package_module_types): with mesonpy._project() as project: builder = mesonpy._EditableWheelBuilder( - project._metadata, project._manifest, project._limited_api, project._allow_windows_shared_libs) + project._metadata, project._manifest, project._limited_api, project._allow_windows_shared_libs, None) assert set(builder._top_level_modules) == { 'file', 'package',