-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathentry_point.py
executable file
·311 lines (251 loc) · 10.3 KB
/
entry_point.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
#!/usr/bin/env python
"""
This module is the entry point executed when you run `conda.exe` on the command line.
It will end up calling the `conda` CLI, but it intercepts the call to do some
preliminary work and handling some special cases that arise when PyInstaller is involved.
"""
import os
import sys
from multiprocessing import freeze_support
def _create_dummy_executor(*args, **kwargs):
"use this for debugging, because ProcessPoolExecutor isn't pdb/ipdb friendly"
from concurrent.futures import Executor
class DummyExecutor(Executor):
def map(self, func, *iterables):
for iterable in iterables:
for thing in iterable:
yield func(thing)
return DummyExecutor(*args, **kwargs)
def _fix_sys_path():
"""
Before any more imports, leave cwd out of sys.path for internal 'conda shell.*' commands.
see https://github.com/conda/conda/issues/6549
"""
if (
len(sys.argv) > 1
and sys.argv[1].startswith("shell.")
and sys.path
and sys.path[0] == ""
):
# The standard first entry in sys.path is an empty string,
# and os.path.abspath('') expands to os.getcwd().
del sys.path[0]
def _constructor_parse_cli():
import argparse
# This might be None!
CPU_COUNT = os.cpu_count()
# See validation results for magic number of 3
# https://dholth.github.io/conda-benchmarks/#extract.TimeExtract.time_extract?conda-package-handling=2.0.0a2&p-format='.conda'&p-format='.tar.bz2'&p-lang='py' # noqa
DEFAULT_NUM_PROCESSORS = 1 if not CPU_COUNT else min(3, CPU_COUNT)
class _NumProcessorsAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
"""Converts a string representing the max number of workers to an integer
while performing validation checks; raises argparse.ArgumentError if anything fails.
"""
ERROR_MSG = f"Value must be int between 0 (auto) and {CPU_COUNT}."
try:
num = int(values)
except ValueError as exc:
raise argparse.ArgumentError(self, ERROR_MSG) from exc
# cpu_count can return None, so skip this check if that happens
if CPU_COUNT:
# See Windows notes for magic number of 61
# https://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor
max_cpu_num = min(CPU_COUNT, 61) if os.name == "nt" else CPU_COUNT
if num > max_cpu_num:
raise argparse.ArgumentError(self, ERROR_MSG)
if num < 0:
raise argparse.ArgumentError(self, ERROR_MSG)
elif num == 0:
num = None # let the multiprocessing module decide
setattr(namespace, self.dest, num)
p = argparse.ArgumentParser(description="constructor helper subcommand")
p.add_argument(
"--prefix",
action="store",
required=True,
help="path to the conda environment to operate on",
)
# We can't add this option yet because micromamba doesn't support it
# Instead we check for the CONDA_ROOT_PREFIX env var; see below
# p.add_argument(
# "--root-prefix",
# action="store",
# help="path to root path of the conda installation; "
# "defaults to --prefix if not provided",
# )
p.add_argument(
"--num-processors",
default=DEFAULT_NUM_PROCESSORS,
metavar="N",
action=_NumProcessorsAction,
help="Number of processors to use with --extract-conda-pkgs. "
"Value must be int between 0 (auto) and the number of processors. "
f"Defaults to {DEFAULT_NUM_PROCESSORS}.",
)
g = p.add_mutually_exclusive_group(required=True)
g.add_argument(
"--extract-conda-pkgs",
action="store_true",
help="extract conda packages found in prefix/pkgs",
)
g.add_argument(
"--extract-tarball",
action="store_true",
help="extract tarball from stdin",
)
g.add_argument(
"--make-menus",
nargs="*",
metavar="PKG_NAME",
help="create menu items for the given packages; "
"if none are given, create menu items for all packages "
"in the environment specified by --prefix",
)
g.add_argument(
"--rm-menus",
action="store_true",
help="remove menu items for all packages "
"in the environment specified by --prefix",
)
args, args_unknown = p.parse_known_args()
args.prefix = os.path.abspath(args.prefix)
args.root_prefix = os.path.abspath(os.environ.get("CONDA_ROOT_PREFIX", args.prefix))
if "--num-processors" in sys.argv and not args.extract_conda_pkgs:
raise argparse.ArgumentError(
"--num-processors can only be used with --extract-conda-pkgs"
)
return args, args_unknown
def _constructor_extract_conda_pkgs(prefix, max_workers=None):
from concurrent.futures import ProcessPoolExecutor
import tqdm
from conda.base.constants import CONDA_PACKAGE_EXTENSIONS
from conda_package_handling import api
executor = ProcessPoolExecutor(max_workers=max_workers)
os.chdir(os.path.join(prefix, "pkgs"))
flist = []
for ext in CONDA_PACKAGE_EXTENSIONS:
for pkg in os.listdir(os.getcwd()):
if pkg.endswith(ext):
fn = os.path.join(os.getcwd(), pkg)
flist.append(fn)
with tqdm.tqdm(total=len(flist), leave=False) as t:
for fn, _ in zip(flist, executor.map(api.extract, flist)):
t.set_description("Extracting : %s" % os.path.basename(fn))
t.update()
def _constructor_extract_tarball():
import tarfile
t = tarfile.open(mode="r|*", fileobj=sys.stdin.buffer)
t.extractall()
t.close()
def _constructor_menuinst(prefix, pkg_names=None, root_prefix=None, remove=False):
import importlib.util
root_prefix = root_prefix or prefix
utility_script = os.path.join(root_prefix, "Lib", "_nsis.py")
spec = importlib.util.spec_from_file_location("constructor_utils", utility_script)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
if remove:
module.rm_menus(prefix=prefix, root_prefix=prefix)
elif pkg_names is not None:
module.mk_menus(
remove=False,
prefix=prefix,
pkg_names=pkg_names,
root_prefix=prefix,
)
def _constructor_subcommand():
r"""
This is the entry point for the `conda constructor` subcommand. This subcommand
only exists in conda-standalone for now. constructor uses it to:
- extract conda packages
- extract the tarball payload contained in the shell installers
- invoke menuinst to create and remove menu items on Windows
It is supported by a module included in `constructor`, `_nsis.py`, which is placed
in `$INSTDIR\Lib\_nsis.py` on Windows installations.
"""
args, _ = _constructor_parse_cli()
os.chdir(args.prefix)
if args.extract_conda_pkgs:
_constructor_extract_conda_pkgs(args.prefix, max_workers=args.num_processors)
elif args.extract_tarball:
_constructor_extract_tarball()
# when called with --make-menus and no package names, the value is an empty list
# hence the explicit check for None
elif (args.make_menus is not None) or args.rm_menus:
if sys.platform != "win32":
raise NotImplementedError(
"Menu creation and removal is only supported on Windows"
)
_constructor_menuinst(
prefix=args.prefix,
pkg_names=args.make_menus,
remove=args.rm_menus,
root_prefix=args.root_prefix,
)
def _python_subcommand():
"""
Since conda-standalone is actually packaging a full Python interpreter,
we can leverage it by exposing an entry point that mimics its CLI.
This can become useful while debugging.
We don't use argparse because it might absorb some of the arguments.
We are only trying to mimic a subset of the Python CLI, so it can be done
by hand. Options we support are:
- -V/--version: print the version
- a path: run the file or directory/__main__.py
- -c: run the command
- -m: run the module
- no arguments: start an interactive session
- stdin: run the passed input as if it was '-c'
"""
del sys.argv[1] # remove the 'python' argument
first_arg = sys.argv[1] if len(sys.argv) > 1 else None
if first_arg is None:
if sys.stdin.isatty(): # interactive
from code import InteractiveConsole
class CondaStandaloneConsole(InteractiveConsole):
pass
return CondaStandaloneConsole().interact(exitmsg="")
else: # piped stuff
for line in sys.stdin:
exec(line)
return
if first_arg in ("-V", "--version"):
print("Python " + ".".join([str(x) for x in sys.version_info[:3]]))
return
import runpy
if os.path.exists(first_arg):
runpy.run_path(first_arg, run_name="__main__")
return
if len(sys.argv) > 2:
if first_arg == "-m":
del sys.argv[1] # delete '-m'
mod_name = sys.argv[1] # save the actual module name
del sys.argv[1] # delete the module name
runpy.run_module(mod_name, alter_sys=True, run_name="__main__")
return
elif first_arg == "-c":
del sys.argv[0] # remove the executable, but keep '-c' in sys.argv
cmd = sys.argv[1] # save the actual command
del sys.argv[1] # remove the passed command
exec(cmd) # the extra arguments are still in sys.argv
return
print("Usage: conda.exe python [-V] [-c cmd | -m mod | file] [arg] ...")
if first_arg in ("-h", "--help"):
return
return 1
def _conda_main():
from conda.cli import main
_fix_sys_path()
main()
def main():
# https://docs.python.org/3/library/multiprocessing.html#multiprocessing.freeze_support
freeze_support()
if len(sys.argv) > 1:
if sys.argv[1] == "constructor":
return _constructor_subcommand()
elif sys.argv[1] == "python":
return _python_subcommand()
return _conda_main()
if __name__ == "__main__":
sys.exit(main())