Skip to content

Commit 96cf9a2

Browse files
committed
Add --scie option to produce native PEX exes.
You can now specify `--scie {eager,lazy}` when building a PEX file and one or more additional native executable PEX scies will be produced along side the PEX file. These PEX scies will contain a portable CPython interpreter from [Python Standalone Builds][PBS] in the `--scie eager` case and will instead fetch a portable CPython interpreter just in time on first boot on a given machine if needed in the `--scie lazy` case. Although Pex will pick the target platforms and target portable CPython interpreter version automatically, if more control is desired over which platforms are targeted and which Python version is used, then `--scie-platform`, `--scie-pbs-release`, and `--scie-python-version` can be specified. Closes pex-tool#636 Closes pex-tool#1007 Closes pex-tool#2096 [PBS]: https://github.com/indygreg/python-build-standalone
1 parent e13f168 commit 96cf9a2

9 files changed

+844
-11
lines changed

Diff for: pex/bin/pex.py

+43-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from argparse import Action, ArgumentDefaultsHelpFormatter, ArgumentError, ArgumentParser
1717
from textwrap import TextWrapper
1818

19-
from pex import dependency_configuration, pex_warnings
19+
from pex import dependency_configuration, pex_warnings, scie
2020
from pex.argparse import HandleBoolAction
2121
from pex.commands.command import (
2222
GlobalConfigurationError,
@@ -29,6 +29,7 @@
2929
from pex.dist_metadata import Requirement
3030
from pex.docs.command import serve_html_docs
3131
from pex.enum import Enum
32+
from pex.fetcher import URLFetcher
3233
from pex.inherit_path import InheritPath
3334
from pex.interpreter_constraints import InterpreterConstraint, InterpreterConstraints
3435
from pex.layout import Layout, ensure_installed
@@ -56,6 +57,7 @@
5657
from pex.resolve.resolver_options import create_pip_configuration
5758
from pex.resolve.resolvers import Unsatisfiable, sorted_requirements
5859
from pex.result import Error, ResultError, catch, try_
60+
from pex.scie import ScieConfiguration
5961
from pex.targets import Targets
6062
from pex.tracer import TRACER
6163
from pex.typing import TYPE_CHECKING, cast
@@ -314,6 +316,8 @@ def configure_clp_pex_options(parser):
314316
),
315317
)
316318

319+
scie.register_options(group)
320+
317321
group.add_argument(
318322
"--always-write-cache",
319323
dest="always_write_cache",
@@ -1233,6 +1237,27 @@ def do_main(
12331237
cmdline, # type: List[str]
12341238
env, # type: Dict[str, str]
12351239
):
1240+
scie_options = scie.extract_options(options)
1241+
if scie_options and not options.pex_name:
1242+
raise ValueError(
1243+
"You must specify `-o`/`--output-file` to use `{scie_options}`.".format(
1244+
scie_options=scie.render_options(scie_options)
1245+
)
1246+
)
1247+
scie_configuration = None # type: Optional[ScieConfiguration]
1248+
if scie_options:
1249+
scie_configuration = scie_options.create_configuration(targets=targets)
1250+
if not scie_configuration:
1251+
raise ValueError(
1252+
"You selected `{scie_options}`, but none of the selected targets have "
1253+
"compatible interpreters that can be embedded to form a scie:\n{targets}".format(
1254+
scie_options=scie.render_options(scie_options),
1255+
targets="\n".join(
1256+
target.render_description() for target in targets.unique_targets()
1257+
),
1258+
)
1259+
)
1260+
12361261
with TRACER.timed("Building pex"):
12371262
pex_builder = build_pex(
12381263
requirement_configuration=requirement_configuration,
@@ -1276,6 +1301,23 @@ def do_main(
12761301
verbose=options.seed == Seed.VERBOSE,
12771302
)
12781303
print(seed_info)
1304+
if scie_configuration:
1305+
url_fetcher = URLFetcher(
1306+
network_configuration=resolver_configuration.network_configuration,
1307+
password_entries=resolver_configuration.repos_configuration.password_entries,
1308+
)
1309+
with TRACER.timed("Building scie(s)"):
1310+
for par_info in scie.build(
1311+
configuration=scie_configuration, pex_file=pex_file, url_fetcher=url_fetcher
1312+
):
1313+
log(
1314+
"Saved PEX scie for CPython {version} on {platform} to {scie}".format(
1315+
version=par_info.target.version_str,
1316+
platform=par_info.platform,
1317+
scie=os.path.relpath(par_info.file),
1318+
),
1319+
V=options.verbosity,
1320+
)
12791321
else:
12801322
if not _compatible_with_current_platform(interpreter, targets.platforms):
12811323
log("WARNING: attempting to run PEX with incompatible platforms!", V=1)

Diff for: pex/platforms.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
def _normalize_platform(platform):
3131
# type: (str) -> str
32-
return platform.replace("-", "_").replace(".", "_")
32+
return platform.lower().replace("-", "_").replace(".", "_")
3333

3434

3535
@attr.s(frozen=True)

Diff for: pex/resolve/resolver_configuration.py

+15
Original file line numberDiff line numberDiff line change
@@ -199,9 +199,24 @@ class PexRepositoryConfiguration(object):
199199
network_configuration = attr.ib(default=NetworkConfiguration()) # type: NetworkConfiguration
200200
transitive = attr.ib(default=True) # type: bool
201201

202+
@property
203+
def repos_configuration(self):
204+
# type: () -> ReposConfiguration
205+
return ReposConfiguration()
206+
202207

203208
@attr.s(frozen=True)
204209
class LockRepositoryConfiguration(object):
205210
parse_lock = attr.ib() # type: Callable[[], Union[Lockfile, Error]]
206211
lock_file_path = attr.ib() # type: str
207212
pip_configuration = attr.ib() # type: PipConfiguration
213+
214+
@property
215+
def repos_configuration(self):
216+
# type: () -> ReposConfiguration
217+
return self.pip_configuration.repos_configuration
218+
219+
@property
220+
def network_configuration(self):
221+
# type: () -> NetworkConfiguration
222+
return self.pip_configuration.network_configuration

Diff for: pex/scie/__init__.py

+175
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# Copyright 2024 Pex project contributors.
2+
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3+
4+
from __future__ import absolute_import
5+
6+
from argparse import Namespace, _ActionsContainer
7+
8+
from pex.fetcher import URLFetcher
9+
from pex.orderedset import OrderedSet
10+
from pex.pep_440 import Version
11+
from pex.scie import science
12+
from pex.scie.model import (
13+
ScieConfiguration,
14+
ScieInfo,
15+
ScieOptions,
16+
SciePlatform,
17+
ScieStyle,
18+
ScieTarget,
19+
)
20+
from pex.typing import TYPE_CHECKING, cast
21+
from pex.variables import ENV, Variables
22+
23+
if TYPE_CHECKING:
24+
from typing import Iterator, Optional, Tuple, Union
25+
26+
27+
__all__ = (
28+
"ScieConfiguration",
29+
"ScieInfo",
30+
"SciePlatform",
31+
"ScieStyle",
32+
"ScieTarget",
33+
"build",
34+
)
35+
36+
37+
def register_options(parser):
38+
# type: (_ActionsContainer) -> None
39+
40+
parser.add_argument(
41+
"--scie",
42+
dest="scie_style",
43+
default=None,
44+
type=ScieStyle.for_value,
45+
choices=ScieStyle.values(),
46+
help=(
47+
"Create one or more native executable scies from your PEX that include a portable "
48+
"CPython interpreter along with your PEX making for a truly hermetic PEX that can run "
49+
"on machines with no Python installed at all. If your PEX has multiple targets, "
50+
"whether `--platform`s, `--complete-platform`s or local interpreters in any "
51+
"combination, then one PEX scie will be made for each platform, selecting the latest "
52+
"compatible portable CPython interpreter. Note that only CPython>=3.8 is supported. If "
53+
"you'd like to explicitly control the target platforms or the exact portable CPython "
54+
"selected, see `--scie-platform`, `--scie-pbs-release` and `--scie-python-version`. "
55+
"Specifying `--scie {lazy}` will fetch the portable CPython interpreter just in time "
56+
"on first boot of the PEX scie on a given machine if needed. The URL(s) to fetch the "
57+
"portable CPython interpreter from can be customized by exporting the "
58+
"PEX_BOOTSTRAP_URLS environment variable pointing to a json file with the format: "
59+
'`{{"ptex": {{<file name 1>: <url>, ...}}}}` where the file names should match those '
60+
"found via `SCIE=inspect <the PEX scie> | jq .ptex` with appropriate replacement URLs. "
61+
"Specifying `--scie {eager}` will embed the portable CPython interpreter in your PEX "
62+
"scie making for a larger file, but requiring no internet access to boot. If you have "
63+
"customization needs not addressed by the Pex `--scie*` options, consider using "
64+
"`science` to build your scies (which is what Pex uses behind the scenes); see: "
65+
"https://science.scie.app.".format(lazy=ScieStyle.LAZY, eager=ScieStyle.EAGER)
66+
),
67+
)
68+
parser.add_argument(
69+
"--scie-platform",
70+
dest="scie_platforms",
71+
default=[],
72+
action="append",
73+
type=SciePlatform.for_value,
74+
choices=SciePlatform.values(),
75+
help=(
76+
"The platform to produce the native PEX scie executable for. Can be specified multiple "
77+
"times."
78+
),
79+
)
80+
parser.add_argument(
81+
"--scie-pbs-release",
82+
dest="scie_pbs_release",
83+
default=None,
84+
type=str,
85+
help=(
86+
"The Python Standalone Builds release to use. Currently releases are dates of the form "
87+
"YYYYMMDD, e.g.: '20240713'. See their GitHub releases page at "
88+
"https://github.com/indygreg/python-build-standalone/releases to discover available "
89+
"releases. If left unspecified the latest release is used. N.B.: The latest lookup is "
90+
"cached for 5 days. To force a fresh lookup you can remove the cache at "
91+
"<USER CACHE DIR>/science/downloads."
92+
),
93+
)
94+
parser.add_argument(
95+
"--scie-python-version",
96+
dest="scie_python_version",
97+
default=None,
98+
type=Version,
99+
help=(
100+
"The portable CPython version to select. Can be either in `<major>.<minor>` form; "
101+
"e.g.: '3.11', or else fully specified as `<major>.<minor>.<patch>`; e.g.: '3.11.3'. "
102+
"If you don't specify this option, Pex will do its best to guess appropriate portable "
103+
"CPython versions. N.B.: Python Standalone Builds does not provide all patch versions; "
104+
"so you should check their releases at "
105+
"https://github.com/indygreg/python-build-standalone/releases if you wish to pin down "
106+
"to the patch level."
107+
),
108+
)
109+
110+
111+
def render_options(options):
112+
# type: (ScieOptions) -> str
113+
114+
args = ["--scie", str(options.style)]
115+
for platform in options.platforms:
116+
args.append("--scie-platform")
117+
args.append(str(platform))
118+
if options.pbs_release:
119+
args.append("--scie-pbs-release")
120+
args.append(options.pbs_release)
121+
if options.python_version:
122+
args.append("--scie-python-version")
123+
args.append(".".join(map(str, options.python_version)))
124+
return " ".join(args)
125+
126+
127+
def extract_options(options):
128+
# type: (Namespace) -> Optional[ScieOptions]
129+
130+
if not options.scie_style:
131+
return None
132+
133+
python_version = None # type: Optional[Union[Tuple[int, int], Tuple[int, int, int]]]
134+
if options.scie_python_version:
135+
if (
136+
not options.scie_python_version.parsed_version.release
137+
or len(options.scie_python_version.parsed_version.release) < 2
138+
):
139+
raise ValueError(
140+
"Invalid Python version: '{python_version}'.\n"
141+
"Must be in the form `<major>.<minor>` or `<major>.<minor>.<release>`".format(
142+
python_version=options.scie_python_version
143+
)
144+
)
145+
python_version = cast(
146+
"Union[Tuple[int, int], Tuple[int, int, int]]",
147+
options.scie_python_version.parsed_version.release,
148+
)
149+
if python_version < (3, 8):
150+
raise ValueError(
151+
"Invalid Python version: '{python_version}'.\n"
152+
"Scies are built using Python Standalone Builds which only supports Python >=3.8.\n"
153+
"To find supported Python versions, you can browse the releases here:\n"
154+
" https://github.com/indygreg/python-build-standalone/releases".format(
155+
python_version=options.scie_python_version
156+
)
157+
)
158+
159+
return ScieOptions(
160+
style=options.scie_style,
161+
platforms=tuple(OrderedSet(options.scie_platforms)),
162+
pbs_release=options.scie_pbs_release,
163+
python_version=python_version,
164+
)
165+
166+
167+
def build(
168+
configuration, # type: ScieConfiguration
169+
pex_file, # type: str
170+
url_fetcher=None, # type: Optional[URLFetcher]
171+
env=ENV, # type: Variables
172+
):
173+
# type: (...) -> Iterator[ScieInfo]
174+
175+
return science.build(configuration, pex_file, url_fetcher=url_fetcher, env=env)

Diff for: pex/scie/configure-binding.py

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Copyright 2024 Pex project contributors.
2+
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3+
4+
from __future__ import print_function
5+
6+
import os
7+
import sys
8+
9+
10+
def write_bindings(
11+
env_file, # type: str
12+
installed_pex_dir, # type: str
13+
):
14+
# type: (...) -> None
15+
with open(env_file, "a") as fp:
16+
print("PYTHON=" + sys.executable, file=fp)
17+
print("PEX=" + os.path.realpath(os.path.join(installed_pex_dir, "__main__.py")), file=fp)
18+
19+
20+
if __name__ == "__main__":
21+
write_bindings(
22+
env_file=os.environ["SCIE_BINDING_ENV"],
23+
installed_pex_dir=(
24+
# The zipapp case:
25+
os.environ["_PEX_SCIE_INSTALLED_PEX_DIR"]
26+
# The --venv case:
27+
or os.environ.get("VIRTUAL_ENV", os.path.dirname(os.path.dirname(sys.executable)))
28+
),
29+
)
30+
sys.exit(0)

0 commit comments

Comments
 (0)