Skip to content

Commit

Permalink
Add a system to produce Windows script .exes.
Browse files Browse the repository at this point in the history
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 pex-tool#2658.
  • Loading branch information
jsirois committed Feb 7, 2025
1 parent 39e8778 commit 6b8df1b
Show file tree
Hide file tree
Showing 11 changed files with 362 additions and 182 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ __pycache__/
/compatible-tox-hack/compatible_tox_hack.egg-info/
/dist/
/docs/_static_dynamic/
/pex/windows/stubs/

74 changes: 46 additions & 28 deletions build-backend/pex_build/setuptools/build.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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__
Expand Down Expand Up @@ -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=[
Expand All @@ -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)
Expand All @@ -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
40 changes: 37 additions & 3 deletions pex/os.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
9 changes: 4 additions & 5 deletions pex/scie/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -40,7 +40,6 @@
"ScieConfiguration",
"ScieInfo",
"ScieOptions",
"SciePlatform",
"ScieStyle",
"build",
"extract_options",
Expand Down Expand Up @@ -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 "
Expand Down
Loading

0 comments on commit 6b8df1b

Please sign in to comment.