From 6b8df1bb96532d20a8be6bb399c509a255c998df Mon Sep 17 00:00:00 2001 From: John Sirois Date: Fri, 7 Feb 2025 15:51:46 -0800 Subject: [PATCH] Add a system to produce Windows script `.exe`s. Use the stubs generated by the uv project for this. The lineage extends back to distutils through the Posy project. The API is setup and manually tested to produce working windows scripts and the packaging system is updated to embed these stubs in the Pex PEX, wheel and scies. Actually using the API to create scripts when creating venvs on Windows is still left to do. Work towards #2658. --- .gitignore | 1 + build-backend/pex_build/setuptools/build.py | 74 ++++++----- pex/os.py | 40 +++++- pex/scie/__init__.py | 9 +- pex/scie/model.py | 116 +++--------------- pex/scie/science.py | 12 +- pex/sysconfig.py | 104 +++++++++++++++- pex/windows/__init__.py | 111 +++++++++++++++++ testing/scie.py | 6 +- .../integration/scie/test_discussion_2516.py | 4 +- tests/integration/scie/test_pex_scie.py | 67 +++++----- 11 files changed, 362 insertions(+), 182 deletions(-) create mode 100644 pex/windows/__init__.py diff --git a/.gitignore b/.gitignore index bf26a7624..ee089f1bc 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ __pycache__/ /compatible-tox-hack/compatible_tox_hack.egg-info/ /dist/ /docs/_static_dynamic/ +/pex/windows/stubs/ diff --git a/build-backend/pex_build/setuptools/build.py b/build-backend/pex_build/setuptools/build.py index ddf60f8bf..8c80e05ec 100644 --- a/build-backend/pex_build/setuptools/build.py +++ b/build-backend/pex_build/setuptools/build.py @@ -1,7 +1,7 @@ # Copyright 2024 Pex project contributors. # Licensed under the Apache License, Version 2.0 (see LICENSE). -from __future__ import absolute_import +from __future__ import absolute_import, print_function import hashlib import os.path @@ -15,8 +15,8 @@ # We re-export all setuptools' PEP-517 build backend hooks here for the build frontend to call. from setuptools.build_meta import * # NOQA -from pex import hashing, requirements -from pex.common import open_zip, temporary_dir +from pex import hashing, requirements, windows +from pex.common import open_zip, safe_copy, safe_mkdir, temporary_dir from pex.orderedset import OrderedSet from pex.pep_376 import Hash, InstalledFile, Record from pex.version import __version__ @@ -48,12 +48,33 @@ def build_wheel( wheel = setuptools.build_meta.build_wheel( wheel_directory, config_settings=config_settings, metadata_directory=metadata_directory ) # type: str - if pex_build.INCLUDE_DOCS: - wheel_path = os.path.join(wheel_directory, wheel) - with temporary_dir() as chroot: - with open_zip(wheel_path) as zip_fp: - zip_fp.extractall(chroot) + wheel_path = os.path.join(wheel_directory, wheel) + with temporary_dir() as chroot: + with open_zip(wheel_path) as zip_fp: + zip_fp.extractall(chroot) + + dist_info_dir = "pex-{version}.dist-info".format(version=__version__) + record_path = os.path.join(chroot, dist_info_dir, "RECORD") + with open(record_path) as fp: + installed_files = list(Record.read(fp)) + + for stub in windows.fetch_all_stubs(): + stub_relpath = os.path.relpath( + stub.path, os.path.dirname(os.path.dirname(os.path.dirname(windows.__file__))) + ) + stub_dst = os.path.join(chroot, stub_relpath) + safe_mkdir(os.path.dirname(stub_dst)) + safe_copy(stub.path, stub_dst) + installed_files.append( + InstalledFile( + path=stub_relpath, + hash=Hash.create(hashlib.sha256(stub.data)), + size=len(stub.data), + ) + ) + print("Embedded Windows script stub", stub.path, file=sys.stderr) + if pex_build.INCLUDE_DOCS: out_dir = os.path.join(chroot, "pex", "docs") subprocess.check_call( args=[ @@ -63,10 +84,6 @@ def build_wheel( out_dir, ] ) - dist_info_dir = "pex-{version}.dist-info".format(version=__version__) - record_path = os.path.join(chroot, dist_info_dir, "RECORD") - with open(record_path) as fp: - installed_files = list(Record.read(fp)) for root, _, files in os.walk(out_dir): for f in files: src = os.path.join(root, f) @@ -76,21 +93,22 @@ def build_wheel( installed_files.append( InstalledFile(path=dst, hash=Hash.create(hasher), size=os.path.getsize(src)) ) - Record.write(record_path, installed_files) - with open_zip(wheel_path, "w", compression=ZIP_DEFLATED) as zip_fp: - - def add_top_level_dir(name): - # type: (str) -> None - top = os.path.join(chroot, name) - zip_fp.write(top, name + "/") - for root, dirs, files in os.walk(top): - dirs[:] = sorted(dirs) - for path in sorted(files) + dirs: - src = os.path.join(root, path) - dst = os.path.relpath(src, chroot) - zip_fp.write(src, dst) - - add_top_level_dir("pex") - add_top_level_dir(dist_info_dir) + + Record.write(record_path, installed_files) + with open_zip(wheel_path, "w", compression=ZIP_DEFLATED) as zip_fp: + + def add_top_level_dir(name): + # type: (str) -> None + top = os.path.join(chroot, name) + zip_fp.write(top, name + "/") + for root, dirs, files in os.walk(top): + dirs[:] = sorted(dirs) + for path in sorted(files) + dirs: + src = os.path.join(root, path) + dst = os.path.relpath(src, chroot) + zip_fp.write(src, dst) + + add_top_level_dir("pex") + add_top_level_dir(dist_info_dir) return wheel diff --git a/pex/os.py b/pex/os.py index 1796d0a5b..4c96c93b9 100644 --- a/pex/os.py +++ b/pex/os.py @@ -6,15 +6,49 @@ import os import sys +from pex.enum import Enum from pex.typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Any, List, NoReturn, Text, Tuple, Union + +class _CurrentOs(object): + def __get__(self, obj, objtype=None): + # type: (...) -> Os.Value + if not hasattr(self, "_current"): + # N.B.: Python 2.7 uses "linux2". + if sys.platform.startswith("linux"): + self._current = Os.LINUX + elif sys.platform == "darwin": + self._current = Os.MACOS + elif sys.platform == "win32": + self._current = Os.WINDOWS + if not hasattr(self, "_current"): + raise ValueError( + "The current operating system is not supported!: {system}".format( + system=sys.platform + ) + ) + return self._current + + +class Os(Enum["Os.Value"]): + class Value(Enum.Value): + pass + + LINUX = Value("linux") + MACOS = Value("macos") + WINDOWS = Value("windows") + CURRENT = _CurrentOs() + + +Os.seal() + # N.B.: Python 2.7 uses "linux2". -LINUX = sys.platform.startswith("linux") -MAC = sys.platform == "darwin" -WINDOWS = sys.platform == "win32" +LINUX = Os.CURRENT is Os.LINUX +MAC = Os.CURRENT is Os.MACOS +WINDOWS = Os.CURRENT is Os.WINDOWS HOME_ENV_VAR = "USERPROFILE" if WINDOWS else "HOME" diff --git a/pex/scie/__init__.py b/pex/scie/__init__.py index b128c5020..594fd85fe 100644 --- a/pex/scie/__init__.py +++ b/pex/scie/__init__.py @@ -23,11 +23,11 @@ ScieConfiguration, ScieInfo, ScieOptions, - SciePlatform, ScieStyle, Url, ) from pex.scie.science import SCIENCE_RELEASES_URL, SCIENCE_REQUIREMENT +from pex.sysconfig import SysPlatform from pex.typing import TYPE_CHECKING, cast from pex.variables import ENV, Variables @@ -40,7 +40,6 @@ "ScieConfiguration", "ScieInfo", "ScieOptions", - "SciePlatform", "ScieStyle", "build", "extract_options", @@ -162,11 +161,11 @@ def register_options(parser): dest="scie_platforms", default=[], action="append", - type=SciePlatform.parse, + type=SysPlatform.parse, choices=[ platform - for platform in SciePlatform.values() - if platform not in (SciePlatform.WINDOWS_AARCH64, SciePlatform.WINDOWS_X86_64) + for platform in SysPlatform.values() + if platform not in (SysPlatform.WINDOWS_AARCH64, SysPlatform.WINDOWS_X86_64) ], help=( "The platform to produce the native PEX scie executable for. Can be specified multiple " diff --git a/pex/scie/model.py b/pex/scie/model.py index 505dc0056..dabbc91ee 100644 --- a/pex/scie/model.py +++ b/pex/scie/model.py @@ -4,8 +4,6 @@ from __future__ import absolute_import import itertools -import os -import platform from collections import OrderedDict, defaultdict from pex.common import pluralize @@ -15,6 +13,7 @@ from pex.pep_503 import ProjectName from pex.pex import PEX from pex.platforms import PlatformSpec +from pex.sysconfig import SysPlatform from pex.targets import Targets from pex.third_party.packaging import tags # noqa from pex.typing import TYPE_CHECKING, cast @@ -230,89 +229,6 @@ class BusyBoxEntryPoints(object): ad_hoc_entry_points = attr.ib() # type: Tuple[NamedEntryPoint, ...] -class _CurrentPlatform(object): - def __get__(self, obj, objtype=None): - # type: (...) -> SciePlatform.Value - if not hasattr(self, "_current"): - system = platform.system().lower() - machine = platform.machine().lower() - if "linux" == system: - if machine in ("aarch64", "arm64"): - self._current = SciePlatform.LINUX_AARCH64 - elif machine in ("armv7l", "armv8l"): - self._current = SciePlatform.LINUX_ARMV7L - elif machine == "ppc64le": - self._current = SciePlatform.LINUX_PPC64LE - elif machine == "s390x": - self._current = SciePlatform.LINUX_S390X - elif machine in ("amd64", "x86_64"): - self._current = SciePlatform.LINUX_X86_64 - elif "darwin" == system: - if machine in ("aarch64", "arm64"): - self._current = SciePlatform.MACOS_AARCH64 - elif machine in ("amd64", "x86_64"): - self._current = SciePlatform.MACOS_X86_64 - elif "windows" == system: - if machine in ("aarch64", "arm64"): - self._current = SciePlatform.WINDOWS_AARCH64 - elif machine in ("amd64", "x86_64"): - self._current = SciePlatform.WINDOWS_X86_64 - if not hasattr(self, "_current"): - raise ValueError( - "The current operating system / machine pair is not supported!: " - "{system} / {machine}".format(system=system, machine=machine) - ) - return self._current - - -class SciePlatform(Enum["SciePlatform.Value"]): - class Value(Enum.Value): - @property - def extension(self): - # type: () -> str - return ( - ".exe" - if self in (SciePlatform.WINDOWS_AARCH64, SciePlatform.WINDOWS_X86_64) - else "" - ) - - def binary_name(self, binary_name): - # type: (str) -> str - return "{binary_name}{extension}".format( - binary_name=binary_name, extension=self.extension - ) - - def qualified_binary_name(self, binary_name): - # type: (str) -> str - return "{binary_name}-{platform}{extension}".format( - binary_name=binary_name, platform=self, extension=self.extension - ) - - def qualified_file_name(self, file_name): - # type: (str) -> str - stem, ext = os.path.splitext(file_name) - return "{stem}-{platform}{ext}".format(stem=stem, platform=self, ext=ext) - - LINUX_AARCH64 = Value("linux-aarch64") - LINUX_ARMV7L = Value("linux-armv7l") - LINUX_PPC64LE = Value("linux-powerpc64") - LINUX_S390X = Value("linux-s390x") - LINUX_X86_64 = Value("linux-x86_64") - MACOS_AARCH64 = Value("macos-aarch64") - MACOS_X86_64 = Value("macos-x86_64") - WINDOWS_AARCH64 = Value("windows-aarch64") - WINDOWS_X86_64 = Value("windows-x86_64") - CURRENT = _CurrentPlatform() - - @classmethod - def parse(cls, value): - # type: (str) -> SciePlatform.Value - return cls.CURRENT if "current" == value else cls.for_value(value) - - -SciePlatform.seal() - - class Provider(Enum["Provider.Value"]): class Value(Enum.Value): pass @@ -327,7 +243,7 @@ class Value(Enum.Value): @attr.s(frozen=True) class InterpreterDistribution(object): provider = attr.ib() # type: Provider.Value - platform = attr.ib() # type: SciePlatform.Value + platform = attr.ib() # type: SysPlatform.Value version = attr.ib() # type: Union[Tuple[int, int], Tuple[int, int, int]] release = attr.ib(default=None) # type: Optional[str] @@ -357,7 +273,7 @@ class ScieInfo(object): @property def platform(self): - # type: () -> SciePlatform.Value + # type: () -> SysPlatform.Value return self.interpreter.platform @property @@ -381,7 +297,7 @@ class ScieOptions(object): scie_only = attr.ib(default=False) # type: bool busybox_entrypoints = attr.ib(default=None) # type: Optional[BusyBoxEntryPoints] busybox_pex_entrypoint_env_passthrough = attr.ib(default=False) # type: bool - platforms = attr.ib(default=()) # type: Tuple[SciePlatform.Value, ...] + platforms = attr.ib(default=()) # type: Tuple[SysPlatform.Value, ...] pbs_release = attr.ib(default=None) # type: Optional[str] pypy_release = attr.ib(default=None) # type: Optional[str] python_version = attr.ib( @@ -433,7 +349,7 @@ def _from_platform_specs( python_version = options.python_version python_versions_by_platform = defaultdict( set - ) # type: DefaultDict[SciePlatform.Value, Set[Union[Tuple[int, int], Tuple[int, int, int]]]] + ) # type: DefaultDict[SysPlatform.Value, Set[Union[Tuple[int, int], Tuple[int, int, int]]]] for platform_spec in platform_specs: if python_version: plat_python_version = python_version @@ -459,22 +375,22 @@ def _from_platform_specs( if "linux" in platform_str: if is_aarch64: - scie_platform = SciePlatform.LINUX_AARCH64 + scie_platform = SysPlatform.LINUX_AARCH64 elif is_armv7l: - scie_platform = SciePlatform.LINUX_ARMV7L + scie_platform = SysPlatform.LINUX_ARMV7L elif is_ppc64le: - scie_platform = SciePlatform.LINUX_PPC64LE + scie_platform = SysPlatform.LINUX_PPC64LE elif is_s390x: - scie_platform = SciePlatform.LINUX_S390X + scie_platform = SysPlatform.LINUX_S390X else: - scie_platform = SciePlatform.LINUX_X86_64 + scie_platform = SysPlatform.LINUX_X86_64 elif "mac" in platform_str: scie_platform = ( - SciePlatform.MACOS_AARCH64 if is_aarch64 else SciePlatform.MACOS_X86_64 + SysPlatform.MACOS_AARCH64 if is_aarch64 else SysPlatform.MACOS_X86_64 ) elif "win" in platform_str: scie_platform = ( - SciePlatform.WINDOWS_AARCH64 if is_aarch64 else SciePlatform.WINDOWS_X86_64 + SysPlatform.WINDOWS_AARCH64 if is_aarch64 else SysPlatform.WINDOWS_X86_64 ) else: continue @@ -488,17 +404,17 @@ def _from_platform_specs( # PyPy distributions for Linux aarch64 start with 3.7 (and PyPy always releases for # 2.7). if ( - SciePlatform.LINUX_AARCH64 is scie_platform + SysPlatform.LINUX_AARCH64 is scie_platform and plat_python_version[0] == 3 and plat_python_version < (3, 7) ): continue # PyPy distributions are not available for Linux armv7l - if SciePlatform.LINUX_ARMV7L is scie_platform: + if SysPlatform.LINUX_ARMV7L is scie_platform: continue # PyPy distributions for Linux ppc64le are only available for 2.7 and 3.{5,6}. if ( - SciePlatform.LINUX_PPC64LE is scie_platform + SysPlatform.LINUX_PPC64LE is scie_platform and plat_python_version[0] == 3 and plat_python_version >= (3, 7) ): @@ -506,7 +422,7 @@ def _from_platform_specs( # PyPy distributions for Mac arm64 start with 3.8 (and PyPy always releases for # 2.7). if ( - SciePlatform.MACOS_AARCH64 is scie_platform + SysPlatform.MACOS_AARCH64 is scie_platform and plat_python_version[0] == 3 and plat_python_version < (3, 8) ): diff --git a/pex/scie/science.py b/pex/scie/science.py index 5541950cf..14f73916b 100644 --- a/pex/scie/science.py +++ b/pex/scie/science.py @@ -32,10 +32,10 @@ Provider, ScieConfiguration, ScieInfo, - SciePlatform, ScieStyle, Url, ) +from pex.sysconfig import SysPlatform from pex.third_party.packaging.specifiers import SpecifierSet from pex.third_party.packaging.version import InvalidVersion from pex.tracer import TRACER @@ -75,7 +75,7 @@ def _science_binary_url(suffix=""): return "{science_releases_url}/download/v{version}/{binary}{suffix}".format( science_releases_url=SCIENCE_RELEASES_URL, version=MIN_SCIENCE_VERSION.raw, - binary=SciePlatform.CURRENT.qualified_binary_name("science-fat"), + binary=SysPlatform.CURRENT.qualified_binary_name("science-fat"), suffix=suffix, ) @@ -290,10 +290,10 @@ def _science_dir( def _science_binary_names(): # type: () -> Iterator[str] - yield SciePlatform.CURRENT.binary_name("science-fat") - yield SciePlatform.CURRENT.qualified_binary_name("science-fat") - yield SciePlatform.CURRENT.binary_name("science") - yield SciePlatform.CURRENT.qualified_binary_name("science") + yield SysPlatform.CURRENT.binary_name("science-fat") + yield SysPlatform.CURRENT.qualified_binary_name("science-fat") + yield SysPlatform.CURRENT.binary_name("science") + yield SysPlatform.CURRENT.qualified_binary_name("science") def _is_compatible_science_binary( diff --git a/pex/sysconfig.py b/pex/sysconfig.py index 0cb72dfdd..152ffce01 100644 --- a/pex/sysconfig.py +++ b/pex/sysconfig.py @@ -3,10 +3,17 @@ from __future__ import absolute_import +import os import os.path +import platform from sysconfig import get_config_var -from pex.os import WINDOWS +from pex.enum import Enum +from pex.os import WINDOWS, Os +from pex.typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Text, TypeVar EXE_EXTENSION = get_config_var("EXE") or "" EXE_EXTENSIONS = ( @@ -16,8 +23,12 @@ ) +if TYPE_CHECKING: + _Text = TypeVar("_Text", str, Text) + + def script_name(name): - # type: (str) -> str + # type: (_Text) -> _Text if not EXE_EXTENSION: return name stem, ext = os.path.splitext(name) @@ -28,3 +39,92 @@ def script_name(name): # with either sysconfig.get_config_vars() or Formatter().parse() to pick apart the script dir # suffix from any base dir template. SCRIPT_DIR = "Scripts" if WINDOWS else "bin" + + +class _CurrentPlatform(object): + def __get__(self, obj, objtype=None): + # type: (...) -> SysPlatform.Value + if not hasattr(self, "_current"): + machine = platform.machine().lower() + if Os.CURRENT is Os.LINUX: + if machine in ("aarch64", "arm64"): + self._current = SysPlatform.LINUX_AARCH64 + elif machine in ("armv7l", "armv8l"): + self._current = SysPlatform.LINUX_ARMV7L + elif machine == "ppc64le": + self._current = SysPlatform.LINUX_PPC64LE + elif machine == "s390x": + self._current = SysPlatform.LINUX_S390X + elif machine in ("amd64", "x86_64"): + self._current = SysPlatform.LINUX_X86_64 + if Os.CURRENT is Os.MACOS: + if machine in ("aarch64", "arm64"): + self._current = SysPlatform.MACOS_AARCH64 + elif machine in ("amd64", "x86_64"): + self._current = SysPlatform.MACOS_X86_64 + if Os.CURRENT is Os.WINDOWS: + if machine in ("aarch64", "arm64"): + self._current = SysPlatform.WINDOWS_AARCH64 + elif machine in ("amd64", "x86_64"): + self._current = SysPlatform.WINDOWS_X86_64 + if not hasattr(self, "_current"): + raise ValueError( + "The current operating system / machine pair is not supported!: " + "{system} / {machine}".format(system=Os.CURRENT, machine=machine) + ) + return self._current + + +class _PlatformValue(Enum.Value): + def __init__( + self, + os_type, # type: Os.Value + arch, + ): + super(_PlatformValue, self).__init__("{os}-{arch}".format(os=os_type, arch=arch)) + self.os = os_type + self.arch = arch + + @property + def extension(self): + # type: () -> str + return ".exe" if self.os is Os.WINDOWS else "" + + def binary_name(self, binary_name): + # type: (_Text) -> _Text + return "{binary_name}{extension}".format(binary_name=binary_name, extension=self.extension) + + def qualified_binary_name(self, binary_name): + # type: (_Text) -> _Text + return "{binary_name}-{platform}{extension}".format( + binary_name=binary_name, platform=self, extension=self.extension + ) + + def qualified_file_name(self, file_name): + # type: (_Text) -> _Text + stem, ext = os.path.splitext(file_name) + return "{stem}-{platform}{ext}".format(stem=stem, platform=self, ext=ext) + + +class SysPlatform(Enum["SysPlatform.Value"]): + class Value(_PlatformValue): + pass + + LINUX_AARCH64 = Value(Os.LINUX, "aarch64") + LINUX_ARMV7L = Value(Os.LINUX, "armv7l") + LINUX_PPC64LE = Value(Os.LINUX, "powerpc64") + LINUX_S390X = Value(Os.LINUX, "s390x") + LINUX_X86_64 = Value(Os.LINUX, "x86_64") + MACOS_AARCH64 = Value(Os.MACOS, "aarch64") + MACOS_X86_64 = Value(Os.MACOS, "x86_64") + WINDOWS_AARCH64 = Value(Os.WINDOWS, "aarch64") + WINDOWS_X86_64 = Value(Os.WINDOWS, "x86_64") + CURRENT = _CurrentPlatform() + + @classmethod + def parse(cls, value): + # type: (str) -> SysPlatform.Value + return cls.CURRENT if "current" == value else cls.for_value(value) + + +SysPlatform.seal() diff --git a/pex/windows/__init__.py b/pex/windows/__init__.py new file mode 100644 index 000000000..c679168fa --- /dev/null +++ b/pex/windows/__init__.py @@ -0,0 +1,111 @@ +# Copyright 2025 Pex project contributors. +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from __future__ import absolute_import + +import contextlib +import os.path +import struct +import uuid +import zipfile +from typing import Iterator, Optional + +from pex.common import safe_open +from pex.fetcher import URLFetcher +from pex.fs import safe_rename +from pex.os import Os +from pex.sysconfig import SysPlatform +from pex.typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Text + + import attr # vendor:skip +else: + from pex.third_party import attr + + +def _stub_name( + platform=SysPlatform.CURRENT, # type: SysPlatform.Value + gui=False, # type: bool +): + # type: (...) -> str + + if platform.os is not Os.WINDOWS: + raise ValueError( + "Script stubs are only available for Windows platforms; given: {platform}".format( + platform=platform + ) + ) + + return platform.binary_name( + "uv-trampoline-{arch}-{type}".format(arch=platform.arch, type="gui" if gui else "console") + ) + + +_TRAMPOLINE_VERSION = "0.5.29" + + +def _fetch_stub(stub_name): + # type: (str) -> bytes + with URLFetcher().get_body_stream( + "https://raw.githubusercontent.com/astral-sh/uv/refs/tags/{version}/crates/uv-trampoline/" + "trampolines/{stub_name}".format(version=_TRAMPOLINE_VERSION, stub_name=stub_name) + ) as in_fp: + return in_fp.read() + + +@attr.s(frozen=True) +class Stub(object): + path = attr.ib() # type: str + data = attr.ib() # type: bytes + + +def _load_stub( + platform=SysPlatform.CURRENT, # type: SysPlatform.Value + gui=False, # type: bool +): + # type: (...) -> Stub + + stub_name = _stub_name(platform=platform, gui=gui) + stub_dst = os.path.join(os.path.dirname(__file__), "stubs", stub_name) + if os.path.exists(stub_dst): + with open(stub_dst, "rb") as fp: + return Stub(path=stub_dst, data=fp.read()) + + stub = _fetch_stub(stub_name) + with safe_open( + "{stub_dst}.{unique}".format(stub_dst=stub_dst, unique=uuid.uuid4().hex), "wb" + ) as out_fp: + out_fp.write(stub) + safe_rename(out_fp.name, stub_dst) + return Stub(path=stub_dst, data=stub) + + +def fetch_all_stubs(): + # type: () -> Iterator[Stub] + for platform in (SysPlatform.WINDOWS_AARCH64, SysPlatform.WINDOWS_X86_64): + for gui in (True, False): + yield _load_stub(platform=platform, gui=gui) + + +def create_script( + path, # type: Text + contents, # type: Text + platform=SysPlatform.CURRENT, # type: SysPlatform.Value + gui=False, # type: bool + python_path=None, # type: Optional[Text] +): + # type: (...) -> None + + with open("{path}.{unique}".format(path=path, unique=uuid.uuid4().hex), "wb") as fp: + fp.write(_load_stub(platform=platform, gui=gui).data) + with contextlib.closing(zipfile.ZipFile(fp, "a")) as zip_fp: + zip_fp.writestr("__main__.py", contents.encode("utf-8"), zipfile.ZIP_STORED) + python_path_bytes = platform.binary_name( + python_path or ("pythonw" if gui else "python") + ).encode("utf-8") + fp.write(python_path_bytes) + fp.write(struct.pack("= (3, 7) - elif SciePlatform.MACOS_AARCH64 is SciePlatform.CURRENT: + elif SysPlatform.MACOS_AARCH64 is SysPlatform.CURRENT: return PY_VER >= (3, 8) else: return PY_VER >= (3, 6) diff --git a/tests/integration/scie/test_discussion_2516.py b/tests/integration/scie/test_discussion_2516.py index 691153154..23b783d81 100644 --- a/tests/integration/scie/test_discussion_2516.py +++ b/tests/integration/scie/test_discussion_2516.py @@ -9,7 +9,7 @@ from textwrap import dedent from pex.common import safe_open -from pex.scie import SciePlatform +from pex.sysconfig import SysPlatform from pex.typing import TYPE_CHECKING from testing import make_env, run_pex_command @@ -97,7 +97,7 @@ def main() -> None: os.path.join(str(tmpdir), "ardia*") ), "We expected no PEX or scie leaked in the CWD." - native_scie = os.path.join(out_dir, SciePlatform.CURRENT.value, "ardia") + native_scie = os.path.join(out_dir, SysPlatform.CURRENT.value, "ardia") output = subprocess.check_output( args=[native_scie, "Tux", "says", "Moo?"], env=make_env(PATH=None) ).decode("utf-8") diff --git a/tests/integration/scie/test_pex_scie.py b/tests/integration/scie/test_pex_scie.py index 826e9c59e..c608111e6 100644 --- a/tests/integration/scie/test_pex_scie.py +++ b/tests/integration/scie/test_pex_scie.py @@ -22,7 +22,8 @@ from pex.layout import Layout from pex.orderedset import OrderedSet from pex.os import is_exe -from pex.scie import SciePlatform, ScieStyle +from pex.scie import ScieStyle +from pex.sysconfig import SysPlatform from pex.targets import LocalInterpreter from pex.typing import TYPE_CHECKING from pex.version import __version__ @@ -179,19 +180,19 @@ def create_scies( ).assert_success() python_version_by_platform = { - SciePlatform.LINUX_AARCH64: "3.9", - SciePlatform.LINUX_ARMV7L: "3.11", - SciePlatform.LINUX_PPC64LE: "3.12", - SciePlatform.LINUX_S390X: "3.13", - SciePlatform.LINUX_X86_64: "3.10", - SciePlatform.MACOS_AARCH64: "3.11", - SciePlatform.MACOS_X86_64: "3.12", + SysPlatform.LINUX_AARCH64: "3.9", + SysPlatform.LINUX_ARMV7L: "3.11", + SysPlatform.LINUX_PPC64LE: "3.12", + SysPlatform.LINUX_S390X: "3.13", + SysPlatform.LINUX_X86_64: "3.10", + SysPlatform.MACOS_AARCH64: "3.11", + SysPlatform.MACOS_X86_64: "3.12", } - assert SciePlatform.CURRENT in python_version_by_platform + assert SysPlatform.CURRENT in python_version_by_platform def assert_platforms( output_dir, # type: str - expected_platforms, # type: Iterable[SciePlatform.Value] + expected_platforms, # type: Iterable[SysPlatform.Value] ): # type: (...) -> None @@ -209,7 +210,7 @@ def assert_platforms( assert is_exe(scie), "Expected --scie build to produce a {binary} binary.".format( binary=binary ) - if platform is SciePlatform.CURRENT: + if platform is SysPlatform.CURRENT: assert b"| PEX-scie wabbit! |" in subprocess.check_output( args=[scie, "PEX-scie wabbit!"], env=make_env(PATH=None) ) @@ -238,13 +239,13 @@ def assert_platforms( assert_platforms( output_dir=all_platforms_output_dir, expected_platforms=( - SciePlatform.LINUX_AARCH64, - SciePlatform.LINUX_ARMV7L, - SciePlatform.LINUX_PPC64LE, - SciePlatform.LINUX_S390X, - SciePlatform.LINUX_X86_64, - SciePlatform.MACOS_AARCH64, - SciePlatform.MACOS_X86_64, + SysPlatform.LINUX_AARCH64, + SysPlatform.LINUX_ARMV7L, + SysPlatform.LINUX_PPC64LE, + SysPlatform.LINUX_S390X, + SysPlatform.LINUX_X86_64, + SysPlatform.MACOS_AARCH64, + SysPlatform.MACOS_X86_64, ), ) @@ -257,17 +258,17 @@ def assert_platforms( "--scie-platform", "current", "--scie-platform", - str(SciePlatform.LINUX_AARCH64), + str(SysPlatform.LINUX_AARCH64), "--scie-platform", - str(SciePlatform.LINUX_X86_64), + str(SysPlatform.LINUX_X86_64), ], ) assert_platforms( output_dir=restricted_platforms_output_dir, expected_platforms=( - SciePlatform.CURRENT, - SciePlatform.LINUX_AARCH64, - SciePlatform.LINUX_X86_64, + SysPlatform.CURRENT, + SysPlatform.LINUX_AARCH64, + SysPlatform.LINUX_X86_64, ), ) @@ -313,7 +314,7 @@ def test_specified_science_binary(tmpdir): local_science_binary = os.path.join(str(tmpdir), "science") with open(local_science_binary, "wb") as write_fp, URLFetcher().get_body_stream( "https://github.com/a-scie/lift/releases/download/v0.10.1/{binary}".format( - binary=SciePlatform.CURRENT.qualified_binary_name("science") + binary=SysPlatform.CURRENT.qualified_binary_name("science") ) ) as read_fp: shutil.copyfileobj(read_fp, write_fp) @@ -427,19 +428,19 @@ def make_20240415_3_10_14_url(platform): ) expected_platform = None # type: Optional[str] - if SciePlatform.CURRENT is SciePlatform.LINUX_AARCH64: + if SysPlatform.CURRENT is SysPlatform.LINUX_AARCH64: expected_platform = "aarch64-unknown-linux-gnu" - elif SciePlatform.CURRENT is SciePlatform.LINUX_ARMV7L: + elif SysPlatform.CURRENT is SysPlatform.LINUX_ARMV7L: expected_platform = "armv7-unknown-linux-gnueabihf" - elif SciePlatform.CURRENT is SciePlatform.LINUX_PPC64LE: + elif SysPlatform.CURRENT is SysPlatform.LINUX_PPC64LE: expected_platform = "ppc64le-unknown-linux-gnu" - elif SciePlatform.CURRENT is SciePlatform.LINUX_S390X: + elif SysPlatform.CURRENT is SysPlatform.LINUX_S390X: expected_platform = "s390x-unknown-linux-gnu" - elif SciePlatform.CURRENT is SciePlatform.LINUX_X86_64: + elif SysPlatform.CURRENT is SysPlatform.LINUX_X86_64: expected_platform = "x86_64-unknown-linux-gnu" - elif SciePlatform.CURRENT is SciePlatform.MACOS_AARCH64: + elif SysPlatform.CURRENT is SysPlatform.MACOS_AARCH64: expected_platform = "aarch64-apple-darwin" - elif SciePlatform.CURRENT is SciePlatform.MACOS_X86_64: + elif SysPlatform.CURRENT is SysPlatform.MACOS_X86_64: expected_platform = "x86_64-apple-darwin" assert expected_platform is not None @@ -1118,7 +1119,7 @@ def test_scie_name_style_platform_file_suffix(tmpdir): run_pex_command( args=["--scie", "lazy", "--scie-name-style", "platform-file-suffix", "-o", output_file] ).assert_success() - assert sorted(["app", SciePlatform.CURRENT.qualified_binary_name("app")]) == sorted( + assert sorted(["app", SysPlatform.CURRENT.qualified_binary_name("app")]) == sorted( os.listdir(dist_dir) ) @@ -1128,7 +1129,7 @@ def test_scie_name_style_platform_parent_dir(tmpdir): # type: (Any) -> None foreign_platform = next( - plat for plat in SciePlatform.values() if SciePlatform.CURRENT is not plat + plat for plat in SysPlatform.values() if SysPlatform.CURRENT is not plat ) dist_dir = os.path.join(str(tmpdir), "dist") output_file = os.path.join(dist_dir, "app")