|
1 | 1 | """Zipfile entry point which supports auto-extracting itself based on zip-safety.""" |
2 | 2 |
|
3 | | -from collections import defaultdict |
4 | | -from importlib import import_module, machinery |
5 | | -from importlib.abc import MetaPathFinder |
6 | | -from importlib.metadata import Distribution |
7 | | -from importlib.util import spec_from_loader |
8 | | -from site import getsitepackages |
9 | | -import itertools |
10 | 3 | import os |
11 | | -import re |
12 | 4 | import runpy |
13 | 5 | import sys |
14 | | -import tempfile |
15 | | -import zipfile |
16 | 6 |
|
17 | 7 | # Put this pex on the path before anything else. |
18 | 8 | PEX = os.path.abspath(sys.argv[0]) |
|
26 | 16 | ZIP_SAFE = __ZIP_SAFE__ |
27 | 17 | PEX_STAMP = '__PEX_STAMP__' |
28 | 18 |
|
29 | | -# Workaround for https://bugs.python.org/issue15795 |
30 | | -class ZipFileWithPermissions(zipfile.ZipFile): |
31 | | - """ Custom ZipFile class handling file permissions. """ |
32 | | - |
33 | | - def _extract_member(self, member, targetpath, pwd): |
34 | | - if not isinstance(member, zipfile.ZipInfo): |
35 | | - member = self.getinfo(member) |
36 | | - |
37 | | - targetpath = super(ZipFileWithPermissions, self)._extract_member( |
38 | | - member, targetpath, pwd |
39 | | - ) |
40 | | - |
41 | | - attr = member.external_attr >> 16 |
42 | | - if attr != 0: |
43 | | - os.chmod(targetpath, attr) |
44 | | - return targetpath |
45 | | - |
46 | | -class SoImport(MetaPathFinder): |
47 | | - """So import. Much binary. Such dynamic. Wow.""" |
48 | | - |
49 | | - def __init__(self): |
50 | | - self.suffixes = machinery.EXTENSION_SUFFIXES # list, as importlib will not be using the file description |
51 | | - self.suffixes_by_length = sorted(self.suffixes, key=lambda x: -len(x)) |
52 | | - # Identify all the possible modules we could handle. |
53 | | - self.modules = {} |
54 | | - if zipfile.is_zipfile(sys.argv[0]): |
55 | | - zf = ZipFileWithPermissions(sys.argv[0]) |
56 | | - for name in zf.namelist(): |
57 | | - path, _ = self.splitext(name) |
58 | | - if path: |
59 | | - if path.startswith('.bootstrap/'): |
60 | | - path = path[len('.bootstrap/'):] |
61 | | - importpath = path.replace('/', '.') |
62 | | - self.modules.setdefault(importpath, name) |
63 | | - if path.startswith(MODULE_DIR): |
64 | | - self.modules.setdefault(importpath[len(MODULE_DIR)+1:], name) |
65 | | - if self.modules: |
66 | | - self.zf = zf |
67 | | - |
68 | | - def find_spec(self, name, path, target=None): |
69 | | - """Implements abc.MetaPathFinder.""" |
70 | | - if name in self.modules: |
71 | | - return spec_from_loader(name, self) |
72 | | - |
73 | | - def create_module(self, spec): |
74 | | - """Create a module object that we're going to load.""" |
75 | | - filename = self.modules[spec.name] |
76 | | - prefix, ext = self.splitext(filename) |
77 | | - with tempfile.NamedTemporaryFile(suffix=ext, prefix=os.path.basename(prefix)) as f: |
78 | | - f.write(self.zf.read(filename)) |
79 | | - f.flush() |
80 | | - spec.origin = f.name |
81 | | - loader = machinery.ExtensionFileLoader(spec.name, f.name) |
82 | | - spec.loader = loader |
83 | | - mod = loader.create_module(spec) |
84 | | - # Make it look like module came from the original location for nicer tracebacks. |
85 | | - mod.__file__ = filename |
86 | | - return mod |
87 | | - |
88 | | - def exec_module(self, mod): |
89 | | - """Because we set spec.loader above, the ExtensionFileLoader's exec_module is called.""" |
90 | | - raise NotImplementedError("SoImport.exec_module isn't used") |
91 | | - |
92 | | - def splitext(self, path): |
93 | | - """Similar to os.path.splitext, but splits our longest known suffix preferentially.""" |
94 | | - for suffix in self.suffixes_by_length: |
95 | | - if path.endswith(suffix): |
96 | | - return path[:-len(suffix)], suffix |
97 | | - return None, None |
98 | | - |
99 | | - |
100 | | -class PexDistribution(Distribution): |
101 | | - """Represents a distribution package that exists within a pex file (which is, ultimately, a zip |
102 | | - file). Distribution packages are identified by the presence of a suitable dist-info or egg-info |
103 | | - directory member inside the pex file, which need not necessarily exist at the top level if a |
104 | | - directory prefix is specified in the constructor. |
105 | | - """ |
106 | | - def __init__(self, name, pex_file, zip_file, files, prefix): |
107 | | - self._name = name |
108 | | - self._zf = zip_file |
109 | | - self._pex_file = pex_file |
110 | | - self._prefix = prefix |
111 | | - # Mapping of <path within distribution> -> <full path in zipfile> |
112 | | - self._files = files |
113 | | - |
114 | | - def read_text(self, filename): |
115 | | - full_name = self._files.get(filename) |
116 | | - if full_name: |
117 | | - return self._zf.read(full_name).decode(encoding="utf-8") |
118 | | - |
119 | | - def locate_file(self, path): |
120 | | - return zipfile.Path( |
121 | | - self._pex_file, |
122 | | - at=os.path.join(self._prefix, path) if self._prefix else path, |
123 | | - ) |
124 | | - |
125 | | - read_text.__doc__ = Distribution.read_text.__doc__ |
126 | | - |
127 | | - |
128 | | -class ModuleDirImport(MetaPathFinder): |
129 | | - """Handles imports to a directory equivalently to them being at the top level. |
130 | | -
|
131 | | - This means that if one writes `import third_party.python.six`, it's imported like `import six`, |
132 | | - but becomes accessible under both names. This handles both the fully-qualified import names |
133 | | - and packages importing as their expected top-level names internally. |
134 | | - """ |
135 | | - def __init__(self, module_dir=MODULE_DIR): |
136 | | - self.prefix = module_dir.replace("/", ".") + "." |
137 | | - self._distributions = self._find_all_distributions(module_dir) |
138 | | - |
139 | | - def _find_all_distributions(self, module_dir): |
140 | | - pex_file = sys.argv[0] |
141 | | - if zipfile.is_zipfile(pex_file): |
142 | | - zf = ZipFileWithPermissions(pex_file) |
143 | | - r = re.compile(r"{module_dir}{sep}([^/]+)-[^/-]+?\.(?:dist|egg)-info/(.*)".format( |
144 | | - module_dir=module_dir, |
145 | | - sep = os.sep, |
146 | | - )) |
147 | | - filenames = defaultdict(dict) |
148 | | - for name in zf.namelist(): |
149 | | - match = r.match(name) |
150 | | - if match: |
151 | | - filenames[match.group(1)][match.group(2)] = name |
152 | | - return {mod: [PexDistribution(mod, pex_file, zf, files, prefix=module_dir)] |
153 | | - for mod, files in filenames.items()} |
154 | | - return {} |
155 | | - |
156 | | - def find_spec(self, name, path, target=None): |
157 | | - """Implements abc.MetaPathFinder.""" |
158 | | - if name.startswith(self.prefix): |
159 | | - return spec_from_loader(name, self) |
160 | | - |
161 | | - def create_module(self, spec): |
162 | | - """Actually load a module that we said we'd handle in find_module.""" |
163 | | - module = import_module(spec.name.removeprefix(self.prefix)) |
164 | | - sys.modules[spec.name] = module |
165 | | - return module |
166 | | - |
167 | | - def exec_module(self, mod): |
168 | | - """Nothing to do, create_module already did the work.""" |
169 | | - |
170 | | - def find_distributions(self, context): |
171 | | - """Return an iterable of all Distribution instances capable of |
172 | | - loading the metadata for packages for the indicated ``context``. |
173 | | - """ |
174 | | - if context.name: |
175 | | - # The installed directories have underscores in the place of what might be a hyphen |
176 | | - # in the package name (e.g. the package opentelemetry-sdk installs opentelemetry_sdk). |
177 | | - return self._distributions.get(context.name.replace("-", "_"), []) |
178 | | - else: |
179 | | - return itertools.chain(*self._distributions.values()) |
180 | | - |
181 | | - def get_code(self, fullname): |
182 | | - module = import_module(fullname.removeprefix(self.prefix)) |
183 | | - return module.__loader__.get_code(fullname) |
184 | | - |
185 | | - |
186 | | -def add_module_dir_to_sys_path(dirname): |
| 19 | + |
| 20 | +def add_module_dir_to_sys_path(dirname, zip_safe=True): |
187 | 21 | """Adds the given dirname to sys.path if it's nonempty.""" |
| 22 | + # Add .bootstrap dir to path, after the initial pex entry |
| 23 | + sys.path = sys.path[:1] + [os.path.join(sys.path[0], '.bootstrap')] + sys.path[1:] |
| 24 | + # Now we have .bootstrap on the path, we can import our own hooks. |
| 25 | + import plz |
188 | 26 | if dirname: |
189 | 27 | sys.path = sys.path[:1] + [os.path.join(sys.path[0], dirname)] + sys.path[1:] |
190 | | - sys.meta_path.insert(0, ModuleDirImport(dirname)) |
| 28 | + sys.meta_path.insert(0, plz.ModuleDirImport(dirname)) |
| 29 | + if zip_safe: |
| 30 | + sys.meta_path.append(plz.SoImport(MODULE_DIR)) |
191 | 31 |
|
192 | 32 |
|
193 | 33 | def pex_basepath(temp=False): |
@@ -297,8 +137,6 @@ def main(): |
297 | 137 |
|
298 | 138 | N.B. This gets redefined by pex_test_main to run tests instead. |
299 | 139 | """ |
300 | | - # Add .bootstrap dir to path, after the initial pex entry |
301 | | - sys.path = sys.path[:1] + [os.path.join(sys.path[0], '.bootstrap')] + sys.path[1:] |
302 | 140 | # Starts a debugging session, if defined, before running the entry point. |
303 | 141 | if os.getenv("PLZ_DEBUG") is not None: |
304 | 142 | start_debugger() |
|
0 commit comments