Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pip/_vendor/distlib/scripts.py: 22%
169 statements
« prev ^ index » next coverage.py v7.4.3, created at 2024-02-26 06:33 +0000
« prev ^ index » next coverage.py v7.4.3, created at 2024-02-26 06:33 +0000
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2013-2023 Vinay Sajip.
4# Licensed to the Python Software Foundation under a contributor agreement.
5# See LICENSE.txt and CONTRIBUTORS.txt.
6#
7from io import BytesIO
8import logging
9import os
10import re
11import struct
12import sys
13import time
14from zipfile import ZipInfo
16from .compat import sysconfig, detect_encoding, ZipFile
17from .resources import finder
18from .util import (FileOperator, get_export_entry, convert_path,
19 get_executable, get_platform, in_venv)
21logger = logging.getLogger(__name__)
23_DEFAULT_MANIFEST = '''
24<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
25<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
26 <assemblyIdentity version="1.0.0.0"
27 processorArchitecture="X86"
28 name="%s"
29 type="win32"/>
31 <!-- Identify the application security requirements. -->
32 <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
33 <security>
34 <requestedPrivileges>
35 <requestedExecutionLevel level="asInvoker" uiAccess="false"/>
36 </requestedPrivileges>
37 </security>
38 </trustInfo>
39</assembly>'''.strip()
41# check if Python is called on the first line with this expression
42FIRST_LINE_RE = re.compile(b'^#!.*pythonw?[0-9.]*([ \t].*)?$')
43SCRIPT_TEMPLATE = r'''# -*- coding: utf-8 -*-
44import re
45import sys
46from %(module)s import %(import_name)s
47if __name__ == '__main__':
48 sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
49 sys.exit(%(func)s())
50'''
53def enquote_executable(executable):
54 if ' ' in executable:
55 # make sure we quote only the executable in case of env
56 # for example /usr/bin/env "/dir with spaces/bin/jython"
57 # instead of "/usr/bin/env /dir with spaces/bin/jython"
58 # otherwise whole
59 if executable.startswith('/usr/bin/env '):
60 env, _executable = executable.split(' ', 1)
61 if ' ' in _executable and not _executable.startswith('"'):
62 executable = '%s "%s"' % (env, _executable)
63 else:
64 if not executable.startswith('"'):
65 executable = '"%s"' % executable
66 return executable
69# Keep the old name around (for now), as there is at least one project using it!
70_enquote_executable = enquote_executable
73class ScriptMaker(object):
74 """
75 A class to copy or create scripts from source scripts or callable
76 specifications.
77 """
78 script_template = SCRIPT_TEMPLATE
80 executable = None # for shebangs
82 def __init__(self,
83 source_dir,
84 target_dir,
85 add_launchers=True,
86 dry_run=False,
87 fileop=None):
88 self.source_dir = source_dir
89 self.target_dir = target_dir
90 self.add_launchers = add_launchers
91 self.force = False
92 self.clobber = False
93 # It only makes sense to set mode bits on POSIX.
94 self.set_mode = (os.name == 'posix') or (os.name == 'java'
95 and os._name == 'posix')
96 self.variants = set(('', 'X.Y'))
97 self._fileop = fileop or FileOperator(dry_run)
99 self._is_nt = os.name == 'nt' or (os.name == 'java'
100 and os._name == 'nt')
101 self.version_info = sys.version_info
103 def _get_alternate_executable(self, executable, options):
104 if options.get('gui', False) and self._is_nt: # pragma: no cover
105 dn, fn = os.path.split(executable)
106 fn = fn.replace('python', 'pythonw')
107 executable = os.path.join(dn, fn)
108 return executable
110 if sys.platform.startswith('java'): # pragma: no cover
112 def _is_shell(self, executable):
113 """
114 Determine if the specified executable is a script
115 (contains a #! line)
116 """
117 try:
118 with open(executable) as fp:
119 return fp.read(2) == '#!'
120 except (OSError, IOError):
121 logger.warning('Failed to open %s', executable)
122 return False
124 def _fix_jython_executable(self, executable):
125 if self._is_shell(executable):
126 # Workaround for Jython is not needed on Linux systems.
127 import java
129 if java.lang.System.getProperty('os.name') == 'Linux':
130 return executable
131 elif executable.lower().endswith('jython.exe'):
132 # Use wrapper exe for Jython on Windows
133 return executable
134 return '/usr/bin/env %s' % executable
136 def _build_shebang(self, executable, post_interp):
137 """
138 Build a shebang line. In the simple case (on Windows, or a shebang line
139 which is not too long or contains spaces) use a simple formulation for
140 the shebang. Otherwise, use /bin/sh as the executable, with a contrived
141 shebang which allows the script to run either under Python or sh, using
142 suitable quoting. Thanks to Harald Nordgren for his input.
144 See also: http://www.in-ulm.de/~mascheck/various/shebang/#length
145 https://hg.mozilla.org/mozilla-central/file/tip/mach
146 """
147 if os.name != 'posix':
148 simple_shebang = True
149 else:
150 # Add 3 for '#!' prefix and newline suffix.
151 shebang_length = len(executable) + len(post_interp) + 3
152 if sys.platform == 'darwin':
153 max_shebang_length = 512
154 else:
155 max_shebang_length = 127
156 simple_shebang = ((b' ' not in executable)
157 and (shebang_length <= max_shebang_length))
159 if simple_shebang:
160 result = b'#!' + executable + post_interp + b'\n'
161 else:
162 result = b'#!/bin/sh\n'
163 result += b"'''exec' " + executable + post_interp + b' "$0" "$@"\n'
164 result += b"' '''"
165 return result
167 def _get_shebang(self, encoding, post_interp=b'', options=None):
168 enquote = True
169 if self.executable:
170 executable = self.executable
171 enquote = False # assume this will be taken care of
172 elif not sysconfig.is_python_build():
173 executable = get_executable()
174 elif in_venv(): # pragma: no cover
175 executable = os.path.join(
176 sysconfig.get_path('scripts'),
177 'python%s' % sysconfig.get_config_var('EXE'))
178 else: # pragma: no cover
179 if os.name == 'nt':
180 # for Python builds from source on Windows, no Python executables with
181 # a version suffix are created, so we use python.exe
182 executable = os.path.join(
183 sysconfig.get_config_var('BINDIR'),
184 'python%s' % (sysconfig.get_config_var('EXE')))
185 else:
186 executable = os.path.join(
187 sysconfig.get_config_var('BINDIR'),
188 'python%s%s' % (sysconfig.get_config_var('VERSION'),
189 sysconfig.get_config_var('EXE')))
190 if options:
191 executable = self._get_alternate_executable(executable, options)
193 if sys.platform.startswith('java'): # pragma: no cover
194 executable = self._fix_jython_executable(executable)
196 # Normalise case for Windows - COMMENTED OUT
197 # executable = os.path.normcase(executable)
198 # N.B. The normalising operation above has been commented out: See
199 # issue #124. Although paths in Windows are generally case-insensitive,
200 # they aren't always. For example, a path containing a ẞ (which is a
201 # LATIN CAPITAL LETTER SHARP S - U+1E9E) is normcased to ß (which is a
202 # LATIN SMALL LETTER SHARP S' - U+00DF). The two are not considered by
203 # Windows as equivalent in path names.
205 # If the user didn't specify an executable, it may be necessary to
206 # cater for executable paths with spaces (not uncommon on Windows)
207 if enquote:
208 executable = enquote_executable(executable)
209 # Issue #51: don't use fsencode, since we later try to
210 # check that the shebang is decodable using utf-8.
211 executable = executable.encode('utf-8')
212 # in case of IronPython, play safe and enable frames support
213 if (sys.platform == 'cli' and '-X:Frames' not in post_interp
214 and '-X:FullFrames' not in post_interp): # pragma: no cover
215 post_interp += b' -X:Frames'
216 shebang = self._build_shebang(executable, post_interp)
217 # Python parser starts to read a script using UTF-8 until
218 # it gets a #coding:xxx cookie. The shebang has to be the
219 # first line of a file, the #coding:xxx cookie cannot be
220 # written before. So the shebang has to be decodable from
221 # UTF-8.
222 try:
223 shebang.decode('utf-8')
224 except UnicodeDecodeError: # pragma: no cover
225 raise ValueError('The shebang (%r) is not decodable from utf-8' %
226 shebang)
227 # If the script is encoded to a custom encoding (use a
228 # #coding:xxx cookie), the shebang has to be decodable from
229 # the script encoding too.
230 if encoding != 'utf-8':
231 try:
232 shebang.decode(encoding)
233 except UnicodeDecodeError: # pragma: no cover
234 raise ValueError('The shebang (%r) is not decodable '
235 'from the script encoding (%r)' %
236 (shebang, encoding))
237 return shebang
239 def _get_script_text(self, entry):
240 return self.script_template % dict(
241 module=entry.prefix,
242 import_name=entry.suffix.split('.')[0],
243 func=entry.suffix)
245 manifest = _DEFAULT_MANIFEST
247 def get_manifest(self, exename):
248 base = os.path.basename(exename)
249 return self.manifest % base
251 def _write_script(self, names, shebang, script_bytes, filenames, ext):
252 use_launcher = self.add_launchers and self._is_nt
253 linesep = os.linesep.encode('utf-8')
254 if not shebang.endswith(linesep):
255 shebang += linesep
256 if not use_launcher:
257 script_bytes = shebang + script_bytes
258 else: # pragma: no cover
259 if ext == 'py':
260 launcher = self._get_launcher('t')
261 else:
262 launcher = self._get_launcher('w')
263 stream = BytesIO()
264 with ZipFile(stream, 'w') as zf:
265 source_date_epoch = os.environ.get('SOURCE_DATE_EPOCH')
266 if source_date_epoch:
267 date_time = time.gmtime(int(source_date_epoch))[:6]
268 zinfo = ZipInfo(filename='__main__.py',
269 date_time=date_time)
270 zf.writestr(zinfo, script_bytes)
271 else:
272 zf.writestr('__main__.py', script_bytes)
273 zip_data = stream.getvalue()
274 script_bytes = launcher + shebang + zip_data
275 for name in names:
276 outname = os.path.join(self.target_dir, name)
277 if use_launcher: # pragma: no cover
278 n, e = os.path.splitext(outname)
279 if e.startswith('.py'):
280 outname = n
281 outname = '%s.exe' % outname
282 try:
283 self._fileop.write_binary_file(outname, script_bytes)
284 except Exception:
285 # Failed writing an executable - it might be in use.
286 logger.warning('Failed to write executable - trying to '
287 'use .deleteme logic')
288 dfname = '%s.deleteme' % outname
289 if os.path.exists(dfname):
290 os.remove(dfname) # Not allowed to fail here
291 os.rename(outname, dfname) # nor here
292 self._fileop.write_binary_file(outname, script_bytes)
293 logger.debug('Able to replace executable using '
294 '.deleteme logic')
295 try:
296 os.remove(dfname)
297 except Exception:
298 pass # still in use - ignore error
299 else:
300 if self._is_nt and not outname.endswith(
301 '.' + ext): # pragma: no cover
302 outname = '%s.%s' % (outname, ext)
303 if os.path.exists(outname) and not self.clobber:
304 logger.warning('Skipping existing file %s', outname)
305 continue
306 self._fileop.write_binary_file(outname, script_bytes)
307 if self.set_mode:
308 self._fileop.set_executable_mode([outname])
309 filenames.append(outname)
311 variant_separator = '-'
313 def get_script_filenames(self, name):
314 result = set()
315 if '' in self.variants:
316 result.add(name)
317 if 'X' in self.variants:
318 result.add('%s%s' % (name, self.version_info[0]))
319 if 'X.Y' in self.variants:
320 result.add('%s%s%s.%s' %
321 (name, self.variant_separator, self.version_info[0],
322 self.version_info[1]))
323 return result
325 def _make_script(self, entry, filenames, options=None):
326 post_interp = b''
327 if options:
328 args = options.get('interpreter_args', [])
329 if args:
330 args = ' %s' % ' '.join(args)
331 post_interp = args.encode('utf-8')
332 shebang = self._get_shebang('utf-8', post_interp, options=options)
333 script = self._get_script_text(entry).encode('utf-8')
334 scriptnames = self.get_script_filenames(entry.name)
335 if options and options.get('gui', False):
336 ext = 'pyw'
337 else:
338 ext = 'py'
339 self._write_script(scriptnames, shebang, script, filenames, ext)
341 def _copy_script(self, script, filenames):
342 adjust = False
343 script = os.path.join(self.source_dir, convert_path(script))
344 outname = os.path.join(self.target_dir, os.path.basename(script))
345 if not self.force and not self._fileop.newer(script, outname):
346 logger.debug('not copying %s (up-to-date)', script)
347 return
349 # Always open the file, but ignore failures in dry-run mode --
350 # that way, we'll get accurate feedback if we can read the
351 # script.
352 try:
353 f = open(script, 'rb')
354 except IOError: # pragma: no cover
355 if not self.dry_run:
356 raise
357 f = None
358 else:
359 first_line = f.readline()
360 if not first_line: # pragma: no cover
361 logger.warning('%s is an empty file (skipping)', script)
362 return
364 match = FIRST_LINE_RE.match(first_line.replace(b'\r\n', b'\n'))
365 if match:
366 adjust = True
367 post_interp = match.group(1) or b''
369 if not adjust:
370 if f:
371 f.close()
372 self._fileop.copy_file(script, outname)
373 if self.set_mode:
374 self._fileop.set_executable_mode([outname])
375 filenames.append(outname)
376 else:
377 logger.info('copying and adjusting %s -> %s', script,
378 self.target_dir)
379 if not self._fileop.dry_run:
380 encoding, lines = detect_encoding(f.readline)
381 f.seek(0)
382 shebang = self._get_shebang(encoding, post_interp)
383 if b'pythonw' in first_line: # pragma: no cover
384 ext = 'pyw'
385 else:
386 ext = 'py'
387 n = os.path.basename(outname)
388 self._write_script([n], shebang, f.read(), filenames, ext)
389 if f:
390 f.close()
392 @property
393 def dry_run(self):
394 return self._fileop.dry_run
396 @dry_run.setter
397 def dry_run(self, value):
398 self._fileop.dry_run = value
400 if os.name == 'nt' or (os.name == 'java'
401 and os._name == 'nt'): # pragma: no cover
402 # Executable launcher support.
403 # Launchers are from https://bitbucket.org/vinay.sajip/simple_launcher/
405 def _get_launcher(self, kind):
406 if struct.calcsize('P') == 8: # 64-bit
407 bits = '64'
408 else:
409 bits = '32'
410 platform_suffix = '-arm' if get_platform() == 'win-arm64' else ''
411 name = '%s%s%s.exe' % (kind, bits, platform_suffix)
412 # Issue 31: don't hardcode an absolute package name, but
413 # determine it relative to the current package
414 distlib_package = __name__.rsplit('.', 1)[0]
415 resource = finder(distlib_package).find(name)
416 if not resource:
417 msg = ('Unable to find resource %s in package %s' %
418 (name, distlib_package))
419 raise ValueError(msg)
420 return resource.bytes
422 # Public API follows
424 def make(self, specification, options=None):
425 """
426 Make a script.
428 :param specification: The specification, which is either a valid export
429 entry specification (to make a script from a
430 callable) or a filename (to make a script by
431 copying from a source location).
432 :param options: A dictionary of options controlling script generation.
433 :return: A list of all absolute pathnames written to.
434 """
435 filenames = []
436 entry = get_export_entry(specification)
437 if entry is None:
438 self._copy_script(specification, filenames)
439 else:
440 self._make_script(entry, filenames, options=options)
441 return filenames
443 def make_multiple(self, specifications, options=None):
444 """
445 Take a list of specifications and make scripts from them,
446 :param specifications: A list of specifications.
447 :return: A list of all absolute pathnames written to,
448 """
449 filenames = []
450 for specification in specifications:
451 filenames.extend(self.make(specification, options))
452 return filenames