comparison env/lib/python3.9/site-packages/distlib/scripts.py @ 0:4f3585e2f14b draft default tip

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