Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[experimental] adds pants_oxidized_experimental target, creating a standalone binary distribution for Pants #16484

Merged
merged 52 commits into from
Aug 29, 2022
Merged
Show file tree
Hide file tree
Changes from 51 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
e80dafe
test thingy
Jul 29, 2022
30493df
Package now builds and self-hosts
Jul 29, 2022
bf95375
Remove extraneous irrelevant stuff
Jul 29, 2022
0669dae
style changes
Jul 29, 2022
4c6c064
Use `f_globals` instead of `inspect.getmodule` in `collect_rules`
Jul 29, 2022
335c898
Use `f_globals` instead of `inspect.getmodule` in `collect_rules`
Jul 29, 2022
eb4da97
Merge branch 'chrisjrn/7369-inspect-module' into chrisjrn/run-my-pants
Jul 29, 2022
85f00b8
Adds `ox.py` construct, which we can hopefully remove sometime
Jul 29, 2022
78deb85
Adds workaround for subsystem initialisation error
Jul 29, 2022
e6f162e
Replaces `pkgutil.get_data` with `resources.read_resource`
Aug 2, 2022
a9f4f7f
Update pyo3 run instructions
Aug 2, 2022
2b4c73d
Make `VERSION` resource loadable without upsetting the rest of the pa…
Aug 3, 2022
913e894
Replaces `pkgutil.get_data` with `resources.read_resource`
Aug 2, 2022
ac24a35
Make `VERSION` resource loadable without upsetting the rest of the pa…
Aug 3, 2022
353f05d
Lint build files
Aug 3, 2022
6582d44
Creates the `pants._version` package, designed to ensure that the `VE…
Aug 4, 2022
a914acc
lint
Aug 4, 2022
4fc608e
Merge branch 'chrisjrn/7369-resource-loader' into chrisjrn/run-my-pants
Aug 4, 2022
f1dc695
Merge branch 'main' into chrisjrn/run-my-pants
Aug 4, 2022
184940f
Patch `sys.argv[0]` to be a correct value under pyoxidizer
Aug 4, 2022
a226425
Use a better sentinel for `class_definition_lineno`
Aug 4, 2022
c633384
Tidy up `ox.py`
Aug 4, 2022
32207de
Ensure that `ox` is bootstrapped before anything that depends on `sys…
Aug 4, 2022
3b651ef
Remove customisations to `pyoxidizer` config
Aug 4, 2022
b7b2f06
rename `pyO3-build.md`
Aug 4, 2022
23062b2
Merge remote-tracking branch 'origin/main' into chrisjrn/run-my-pants
Aug 4, 2022
8e2ee9e
Reset the interpreter constraints for pyoxidizer
Aug 4, 2022
9602900
Remove `bin/VERSION`
Aug 4, 2022
1b71073
reorganise pyoxidizer-howto
Aug 4, 2022
aaadeaa
Disable static linking against copyleft libraries
Aug 4, 2022
03478d3
Adds no-op `boostrap_pyoxidizer` function
Aug 5, 2022
6cf9a62
Just enough machinery to get `pex` to run `pip`.
Aug 9, 2022
53f9e3f
Makes `run_pex_venv` script work
Aug 9, 2022
cc60f16
Creates `traditional_import_machinery()` context manager and uses in …
Aug 11, 2022
94582dc
tidy up comments; remove logging infrastructure
Aug 11, 2022
ebca4b8
Merge remote-tracking branch 'origin/main' into chrisjrn/run-my-pants
Aug 11, 2022
17ecac0
Revert "Revert "Replace `pkgutil.get_data` with new `read_resource` A…
Aug 11, 2022
1e77a52
Approaches the `read_resource` api with an approach that doesn't requ…
Aug 11, 2022
1f76dec
Remove Welcome To Pants log
Aug 11, 2022
36453fc
Lets Pants start up with no arguments
Aug 11, 2022
78a840f
Remove extraneous logging
Aug 11, 2022
684ae67
Swap from `importlib.resources` to `importlib_resources` backport
Aug 12, 2022
c51b2a0
See if things work without the `__init__.py` in place
Aug 12, 2022
087e108
WHEEEE
Aug 12, 2022
f557c17
Merge remote-tracking branch 'origin/main' into chrisjrn/run-my-pants
Aug 25, 2022
b6916ba
Merge branch 'main' into chrisjrn/run-my-pants
Aug 25, 2022
dc51dec
update lockfile
Aug 25, 2022
fd19524
Load backend modules with standard Python import machinery
Aug 25, 2022
2a86168
updates instructions
Aug 25, 2022
5203dbd
Fix merge snafus
Aug 26, 2022
727ed34
Renames the `run_as` methods to begin with an underscore.
Aug 26, 2022
8984bde
Address code review feedback
Aug 29, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pants.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ backend_packages.add = [
"pants.backend.build_files.fmt.black",
"pants.backend.python",
"pants.backend.experimental.python.lint.autoflake",
"pants.backend.experimental.python.packaging.pyoxidizer",
"pants.backend.explorer",
"pants.backend.python.lint.black",
"pants.backend.python.lint.docformatter",
Expand Down
2 changes: 1 addition & 1 deletion src/python/pants/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
# Licensed under the Apache License, Version 2.0 (see LICENSE).

python_sources(
dependencies=[":resources"],
overrides={
# Enable `python -m pants ...` style execution ala `json.tool` or `venv`.
"__main__.py": {"dependencies": ["src/python/pants/bin:pants_loader"]},
"version.py": {"dependencies": ["./VERSION:resources"]},
},
)

Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@
def make_exe():
dist = default_python_distribution()
policy = dist.make_python_packaging_policy()
policy.extension_module_filter = "no-copyleft"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very important if static linking gets enabled -- static linking is the trigger clause that makes the copyleft features of the GPL get switched on.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a reasonable default for this flag, given the potential landmine. @sureshjoshi: are you ok with:

  1. changing the default
  2. exposing a flag on pyoxidizer_binary to override it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, no problem with that.

One heads up is that there is basically infinite configurability with this file, which is why I added this: https://www.pantsbuild.org/docs/reference-pyoxidizer_binary#codetemplatecode

Idea being, if the default "good enough" template wasn't enough, no one was perma-blocked - they could just create a custom .bzlt file and they're off to the races. I intentionally tried to use the PyOx defaults as much as possible for this default template... Principle of least surprise for incoming users.

So, I guess the question(s) is/are: what should the default template include, and is this use case is worth adding a specific flag or is it more of a "use the template" solution.

I won't even pretend to have the answer to that question. Nor do I feel strongly about it. 🤷🏽

Aside: Link to docs about this feature: https://pyoxidizer.readthedocs.io/en/stable/pyoxidizer_config_type_python_packaging_policy.html#starlark_pyoxidizer.PythonPackagingPolicy.extension_module_filter

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @sureshjoshi -- recognising that there is indeed a facility to override the entire template, I figured it was better to make the default template a safer default for people to use.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coolest of the beans. As mentioned, no strong opinions on that here.

I would suggest as we're using a non-default setting, it may be worth a note in the help


# Note: Adding this for pydanic and libs that have the "unable to load from memory" error
# https://github.com/indygreg/PyOxidizer/issues/438
policy.resources_location_fallback = "filesystem-relative:lib"

python_config = dist.make_python_interpreter_config()

$RUN_MODULE

exe = dist.to_python_executable(
Expand Down
7 changes: 7 additions & 0 deletions src/python/pants/bin/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,11 @@ pex_binary(
strip_pex_env=False,
)


pyoxidizer_binary(
name="pants_oxidized_experimental",
dependencies=["src/python/pants:pants-packaged"],
entry_point="pants.bin.pants_loader",
)

python_tests(name="tests")
7 changes: 7 additions & 0 deletions src/python/pants/bin/pants_loader.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright 2017 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).


import importlib
import locale
import os
Expand All @@ -9,6 +10,7 @@
import warnings
from textwrap import dedent

from pants import ox
from pants.base.exiter import PANTS_FAILED_EXIT_CODE
from pants.bin.pants_env_vars import (
DAEMON_ENTRYPOINT,
Expand Down Expand Up @@ -109,6 +111,11 @@ def main(cls) -> None:


def main() -> None:
ox.bootstrap_pyoxidizer()

if ox.is_oxidized and ox.pex_main():
return
chrisjrn marked this conversation as resolved.
Show resolved Hide resolved

PantsLoader.main()


Expand Down
17 changes: 17 additions & 0 deletions src/python/pants/bin/pyoxidizer-howto.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
NOTES:

Run `./pants package --pyoxidizer-interpreter-constraints="['CPython==3.9.*']" src/python/pants/bin:pants_oxidized_experimental`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are the ICs only because you're on Apple Silicon, or it's to force the target to use 3.9? Should we set this in pants.toml?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment, the pants scripts will compile native_engine against the Python SDK that is specified by Pants' global interpreter constrains. PyOxidizer will eventually package the Pants Python code along with a Python distribution.

At the moment, our interpreter constraints don't naturally align. If this were for anything other than packaging our own product, it might be worth fixing properly, but I suspect this will end up just being part of a release script.


The binary will be `dist/src.python.pants.bin/pants_oxidized_experimental/aarch64-apple-darwin/debug/install/pants_oxidized_experimental` -- this will not work on the pants repo itself (yet?)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it doesn't embed some of the backends?

You can sometimes get around that by disabling the backends, and skipping config verification:

--backend-packages='-["internal_plugins.test_lockfile_fixtures", "pants.backend.explorer"]' --no-verify-config

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There were things that didn't load early on in the process (that we may have since fixed); these notes haven't been hugely updated since I got things working initially. I'd still rather have a scratch file living in the repo rather than putting it in official docs (for now, anyway)



# Code signing errors?


Obtain a self-signed certificate using info at at https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/Procedures/Procedures.html

I've called mine "pyox-test".

Get native engine to build first (i.e. run `./pants` and wait for rust to finish doing its thing).

Run `codesign -s pyox-test /Users/chrisjrn/src/pants/dist/src.python.pants.bin/pants_oxidized_experimental/aarch64-apple-darwin/debug/install/lib/pants/engine/internals/native_engine.so`
1 change: 1 addition & 0 deletions src/python/pants/engine/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ def collect_rules(*namespaces: Union[ModuleType, Mapping[str, Any]]) -> Iterable

If no namespaces are given, collects all the @rules in the caller's module namespace.
"""

if not namespaces:
currentframe = inspect.currentframe()
assert isinstance(currentframe, FrameType)
Expand Down
4 changes: 3 additions & 1 deletion src/python/pants/init/extension_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from pkg_resources import Requirement, WorkingSet

from pants import ox
from pants.base.exceptions import BackendConfigurationError
from pants.build_graph.build_configuration import BuildConfiguration
from pants.goal.builtins import register_builtin_goals
Expand Down Expand Up @@ -138,7 +139,8 @@ def load_backend(build_configuration: BuildConfiguration.Builder, backend_packag
"""
backend_module = backend_package + ".register"
try:
module = importlib.import_module(backend_module)
with ox.traditional_import_machinery():
module = importlib.import_module(backend_module)
except ImportError as ex:
traceback.print_exc()
raise BackendConfigurationError(f"Failed to load the {backend_module} backend: {ex!r}")
Expand Down
17 changes: 10 additions & 7 deletions src/python/pants/init/plugin_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,16 @@ def resolve(
env: CompleteEnvironment,
) -> WorkingSet:
"""Resolves any configured plugins and adds them to the working_set."""
for resolved_plugin_location in self._resolve_plugins(
options_bootstrapper, env, self._request
):
site.addsitedir(
resolved_plugin_location
) # Activate any .pth files plugin wheels may have.
self._working_set.add_entry(resolved_plugin_location)
from pants import ox
chrisjrn marked this conversation as resolved.
Show resolved Hide resolved

with ox.traditional_import_machinery():
for resolved_plugin_location in self._resolve_plugins(
options_bootstrapper, env, self._request
):
site.addsitedir(
resolved_plugin_location
) # Activate any .pth files plugin wheels may have.
self._working_set.add_entry(resolved_plugin_location)
return self._working_set

def _resolve_plugins(
Expand Down
8 changes: 7 additions & 1 deletion src/python/pants/option/subsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from abc import ABCMeta
from typing import Any, ClassVar, TypeVar

from pants import ox
from pants.engine.internals.selectors import AwaitableConstraints, Get
from pants.option.errors import OptionsError
from pants.option.option_types import collect_options_info
Expand Down Expand Up @@ -55,7 +56,12 @@ def signature(cls):
partial_construct_subsystem.__name__ = name
partial_construct_subsystem.__module__ = cls.__module__
partial_construct_subsystem.__doc__ = cls.help
_, class_definition_lineno = inspect.getsourcelines(cls)

# `inspect.getsourcelines` does not work under oxidation
if not ox.is_oxidized:
_, class_definition_lineno = inspect.getsourcelines(cls)
else:
class_definition_lineno = 0 # `inspect.getsourcelines` returns 0 when undefined.
partial_construct_subsystem.__line_number__ = class_definition_lineno

return dict(
Expand Down
160 changes: 160 additions & 0 deletions src/python/pants/ox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

import contextlib
import functools
import importlib.machinery
import logging
import os
import runpy
import site
import sys
import zipimport

_logger = logging.getLogger(__name__)


def bootstrap_pyoxidizer() -> None:
if is_oxidized:
_logger.info("Pants is running as a PyOxidizer binary.")


# Provide the `is_oxidized` symbol, to allow for workarounds in Pants code where we use things
# that don't work under PyOxidizer's custom importer. `oxidized_importer` is only accessible
# in Pants under PyOxidizer, so an import failure will occur if we're not oxidized.
try:
import oxidized_importer # type: ignore # pants: no-infer-dep # noqa: F401

is_oxidized = True
except ModuleNotFoundError:
is_oxidized = False


if is_oxidized and not sys.argv[0]:
# A not insignificant amount of Pants code relies on `sys.argv[0]`, which is modified in an
# invalid way by python's `pymain_run_module` support. For our purposes, the executable
# distribution is the correct `argv[0]`.
# See https://github.com/indygreg/PyOxidizer/issues/307
sys.argv[0] = sys.executable


def pex_main() -> bool:
"""Detect whether some external process is trying to invoke this binary as Pex.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC this is because Pants tries to resolve plugins using sys.executable right here:

@rule
async def resolve_plugins(
request: PluginsRequest, global_options: GlobalOptions
) -> ResolvedPluginDistributions:
"""This rule resolves plugins using a VenvPex, and exposes the absolute paths of their dists.
NB: This relies on the fact that PEX constructs venvs in a stable location (within the
`named_caches` directory), but consequently needs to disable the process cache: see the
ProcessCacheScope reference in the body.
"""
requirements = PexRequirements(
req_strings=sorted(global_options.plugins),
constraints_strings=(str(constraint) for constraint in request.constraints),
)
if not requirements:
return ResolvedPluginDistributions()
python: PythonExecutable | None = None
if not request.interpreter_constraints:
python = cast(
PythonExecutable,
PythonExecutable.fingerprinted(
sys.executable, ".".join(map(str, sys.version_info[:3])).encode("utf8")
),
)

The ICs are not present via this code path:

class PluginResolver:
"""Encapsulates the state of plugin loading for the given WorkingSet.
Plugin loading is inherently stateful, and so this class captures the state of the WorkingSet at
creation time, even though it will be mutated by each call to `PluginResolver.resolve`. This
makes the inputs to each `resolve(..)` call idempotent, even if the output is not.
"""
def __init__(
self,
scheduler: BootstrapScheduler,
interpreter_constraints: Optional[InterpreterConstraints] = None,
working_set: Optional[WorkingSet] = None,
) -> None:
self._scheduler = scheduler
self._working_set = working_set or global_working_set
self._request = PluginsRequest(
interpreter_constraints, tuple(dist.as_requirement() for dist in self._working_set)
)

self._plugin_resolver = PluginResolver(self._bootstrap_scheduler)

If all this is true, could you just have Pants set an env var or use some other direct signal to tell you here what's up? That would avoid divining tea leaves like this.

Copy link
Contributor

@jsirois jsirois Aug 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or just don't use sys.executable ever. Run Pex via a real Python until PyOxidizer supports being one of those. That's the IC path, so all the code is there to do this IIUC.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could set an env var for the initial invocation, but the issue at hand is the pile of invocations that are made by Pex in the plugin bootstrapping process (i.e. to run venv and pip itself), so we'd need to ensure that that specific environment variable is propagated. And we'd still need to look at argv to figure out which bootstrapping function to run.

It's worth understanding why we use sys.executable in the pre-pyoxidizer case: it's to make sure that plugins are configured with the same Python version and platform as being used to run Pants. We could set ICs and platform constraints by inspecting various things under sys, but in an environment where a Python interpreter is present, realistically, it should just end up finding the value of sys.executable, just with a giant pile more indirection.

Remember that the goal of this is to support running Pants without the need for a Python interpreter to be present. If we didn't use sys.executable and relied on the Python support elsewhere in Pants to load Pex:

  • we'd need to have a real Python present
  • it would need to be the same version as provided with PyOxidizer,
  • and it'd only be necessary for the purpose of downloading the plugins.
  • The external interpreter would never actually run the downloaded packages -- those would be loaded inside Pants

Basically, it would entirely defeat the purpose of not needing a Python interpreter

A better solution would be to not fetch plugins by pip at all, and then as long as we can unzip a wheel file into a reliable location, we wouldn't need to invoke Pex and venv at all. I'd be willing to talk about this use case for a future version of Pants; I think it might make a bit of sense.

Copy link
Contributor

@jsirois jsirois Aug 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, plugin Python needs to match our provided Python in the form of the pyoxidezed one. Gotcha.

This also could've been achieved by just having the wrapper script download a statically linked Python, which I believe the py03 family of projects provides. Then all the problems are solved - we have a real python interpreter to use, the user need not have one installed, etc. The loss is slightly more complexity in our pants wrapper script / setup, but IIUC that script still lives in this world anyhow.

A better solution would be to not fetch plugins by pip at all, and then as long as we can unzip a wheel file into a reliable location, we wouldn't need to invoke Pex and venv at all. I'd be willing to talk about this use case for a future version of Pants; I think it might make a bit of sense.

That sounds impossible. So would you just require any plugin has to provide some sort of manifest of its transitive dependency wheels, or provides all those wheels too or just outlaw 3rdparty deps for plugins? The latter seems the only workable thing in that world; otherwise Pants needs to reinvent conflict resolution between 3rdparty wheel deps.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could switch to depending on Pip though and using runpy to execute it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the idea I'm proposing doesn't use pyoxidizer at all. It ships Pants as-is. The only difference is the wrapper script doesn't look for Python on the local system, it downloads a pre-compiled one with no Pants in it at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, the PyO3-distributed interpreter has the issue I just described

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha, wow.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The custom interpreter is measurably faster, so it's good that we have it for the most part. It's just that it takes a bit more of a scorched-earth approach to __file__ than is absolutely necessary. 🤷

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW It actually looks like the pyoxy interpreter handles __file__ just fine and this is just using it stock and not customizing it with a yaml config to turn off the pyoxidized importer for the stdlib stuff:

jsirois@Gill-Windows:/mnt/c/Users/John Sirois/Downloads/pyoxy-0.2.0-x86_64-unknown-linux-gnu-cpython3.10$ cat package/module.py
import sys


print(__file__, file=sys.stderr)

try:
    print(sys.__file__, file=sys.stderr)
except AttributeError:
    print(f"{sys.meta_path}", file=sys.stderr)

jsirois@Gill-Windows:/mnt/c/Users/John Sirois/Downloads/pyoxy-0.2.0-x86_64-unknown-linux-gnu-cpython3.10$ ./pyoxy run-python -- -m package.module
/mnt/c/Users/John Sirois/Downloads/pyoxy-0.2.0-x86_64-unknown-linux-gnu-cpython3.10/package/module.py
[<oxidized_importer.OxidizedFinder object at 0x7f3e425a7be0>, <class '_frozen_importlib_external.PathFinder'>]

I'm realizing I can at least use this over in Pex to support Par I think.


When bootstrapping a plugin venv, Pants will reinvoke Pex in the binary that Pants was loaded
with. Pex will subsequently run a bunch of Python processes in order to run pip, venv, and
`python -c` invocations. This looks for certain command line switches, and attempt to invoke the
relevant modules that those switches indicate.
"""

if len(sys.argv) < 2:
return False

if sys.argv[1] == "./pex":
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "Detect whether some external process is trying to invoke this binary as Pex." bit seems like it might be a bit misleading. It's actually trying to use "this binary as Python", right? Because argv[0] is expected to be the python interpreter, and argv[1] is the argument to the interpreter.

Probably just a question of naming, but.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are all invocations that are Pants attempting to launch pex, or that are effected by pex attempting to bootstrap our plugins. The only machinery that is present is the machinery that supports pex (and then again, only for this extremely specific workflow), and I don't particularly want to give the impression that we're making a generic Python interpreter here.

Open to better names, but calling it "as Python" seems equally misleading

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I changed the first line of the comment to strongly imply that we only support Pex use cases)

_run_as_pex()
return True

if sys.argv[1] == "-sE" and sys.argv[2].endswith("/pex"):
_run_pex_venv()
return True

if len(sys.argv) == 4 and sys.argv[1:3] == ["-s", "-c"]:
_run_as_dash_c()
return True

if ("-m", "venv") in zip(sys.argv, sys.argv[1:]):
_run_as_venv()
return True

return False


@contextlib.contextmanager
def traditional_import_machinery():
# pex relies heavily on `__file__`, which the Oxidized importer does not
# believe in. This reinstates the default Python import machinery before
# loading and running `pex`, but keeps the PyOxidizer machinery at lowest
# priority, so we can still load interned `.py` sources (e.g. the stdlib)

old_sys_meta_path = sys.meta_path
old_sys_path_hooks = sys.path_hooks

if is_oxidized:
sys.meta_path = [
importlib.machinery.BuiltinImporter,
importlib.machinery.FrozenImporter,
importlib.machinery.PathFinder,
] + sys.meta_path
sys.path_hooks = [
zipimport.zipimporter,
importlib.machinery.FileFinder.path_hook(
(
importlib.machinery.ExtensionFileLoader,
[".cpython-39-darwin.so", ".abi3.so", ".so"],
),
(importlib.machinery.SourceFileLoader, [".py"]),
(importlib.machinery.SourcelessFileLoader, [".pyc"]),
),
] + sys.path_hooks

yield
chrisjrn marked this conversation as resolved.
Show resolved Hide resolved

sys.meta_path = old_sys_meta_path
sys.path_hooks = old_sys_path_hooks


def use_traditional_import_machinery(f):
@functools.wraps(f)
def wrapped(*a, **k):
with traditional_import_machinery():
return f(*a, **k)

return wrapped


@use_traditional_import_machinery
def _run_as_pex():
g = {}
f = runpy.run_path("./pex", init_globals=g)
del sys.argv[1]
f["bootstrap_pex"]("./pex")
sys.exit(0)


@use_traditional_import_machinery
def _run_as_venv():
index = sys.argv.index("-m")

import venv

# `venv` is supplied by the oxidized importer (the stdlib is embedded in the rust binary)
# but uses the `__file__` attribute to find the location of `activate` scripts. These scripts
# are not needed by pex, so we're setting the value to something bogus just to prevent
# subsequent exceptions.
venv.__file__ = "SOMETHING/THAT/IS/NOT/NONE"

sys.argv[1:] = sys.argv[index + 2 :]
runpy.run_module("venv")
sys.exit(0)


@use_traditional_import_machinery
def _run_as_dash_c():
if "PEX" in os.environ:
# Get pex into the modules cache
pex = runpy.run_path(os.environ["PEX"])
pex["bootstrap_pex"](os.environ["PEX"], execute=False)

index = sys.argv.index("-c")
exec(sys.argv[index + 1], {}, {})
sys.exit(0)


@use_traditional_import_machinery
def _run_pex_venv():
path_to_run = sys.argv[2]
site.PREFIXES = [os.path.dirname(path_to_run)]
site.addsitepackages(set())
del sys.argv[1:3]
runpy.run_path(path_to_run, run_name="__main__")
sys.exit(0)