PageRenderTime 46ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/pip/_vendor/distlib/scripts.py

https://github.com/ptthiem/pip
Python | 315 lines | 270 code | 12 blank | 33 comment | 39 complexity | bd8b36582763fbc7151ccc862fc0548a MD5 | raw file
  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright (C) 2013 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. from .compat import sysconfig, fsencode, detect_encoding, ZipFile
  14. from .resources import finder
  15. from .util import FileOperator, get_export_entry, convert_path, get_executable
  16. logger = logging.getLogger(__name__)
  17. _DEFAULT_MANIFEST = '''
  18. <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  19. <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  20. <assemblyIdentity version="1.0.0.0"
  21. processorArchitecture="X86"
  22. name="%s"
  23. type="win32"/>
  24. <!-- Identify the application security requirements. -->
  25. <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
  26. <security>
  27. <requestedPrivileges>
  28. <requestedExecutionLevel level="asInvoker" uiAccess="false"/>
  29. </requestedPrivileges>
  30. </security>
  31. </trustInfo>
  32. </assembly>'''.strip()
  33. # check if Python is called on the first line with this expression
  34. FIRST_LINE_RE = re.compile(b'^#!.*pythonw?[0-9.]*([ \t].*)?$')
  35. SCRIPT_TEMPLATE = '''# -*- coding: utf-8 -*-
  36. if __name__ == '__main__':
  37. import sys, re
  38. def _resolve(module, func):
  39. __import__(module)
  40. mod = sys.modules[module]
  41. parts = func.split('.')
  42. result = getattr(mod, parts.pop(0))
  43. for p in parts:
  44. result = getattr(result, p)
  45. return result
  46. try:
  47. sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
  48. func = _resolve('%(module)s', '%(func)s')
  49. rc = func() # None interpreted as 0
  50. except Exception as e: # only supporting Python >= 2.6
  51. sys.stderr.write('%%s\\n' %% e)
  52. rc = 1
  53. sys.exit(rc)
  54. '''
  55. class ScriptMaker(object):
  56. """
  57. A class to copy or create scripts from source scripts or callable
  58. specifications.
  59. """
  60. script_template = SCRIPT_TEMPLATE
  61. executable = None # for shebangs
  62. def __init__(self, source_dir, target_dir, add_launchers=True,
  63. dry_run=False, fileop=None):
  64. self.source_dir = source_dir
  65. self.target_dir = target_dir
  66. self.add_launchers = add_launchers
  67. self.force = False
  68. self.clobber = False
  69. self.set_mode = False
  70. self.variants = set(('', 'X.Y'))
  71. self._fileop = fileop or FileOperator(dry_run)
  72. def _get_alternate_executable(self, executable, options):
  73. if options.get('gui', False) and os.name == 'nt':
  74. dn, fn = os.path.split(executable)
  75. fn = fn.replace('python', 'pythonw')
  76. executable = os.path.join(dn, fn)
  77. return executable
  78. def _get_shebang(self, encoding, post_interp=b'', options=None):
  79. if self.executable:
  80. executable = self.executable
  81. elif not sysconfig.is_python_build():
  82. executable = get_executable()
  83. elif hasattr(sys, 'base_prefix') and sys.prefix != sys.base_prefix:
  84. executable = os.path.join(sysconfig.get_path('scripts'),
  85. 'python%s' % sysconfig.get_config_var('EXE'))
  86. else:
  87. executable = os.path.join(
  88. sysconfig.get_config_var('BINDIR'),
  89. 'python%s%s' % (sysconfig.get_config_var('VERSION'),
  90. sysconfig.get_config_var('EXE')))
  91. if options:
  92. executable = self._get_alternate_executable(executable, options)
  93. executable = fsencode(executable)
  94. shebang = b'#!' + executable + post_interp + b'\n'
  95. # Python parser starts to read a script using UTF-8 until
  96. # it gets a #coding:xxx cookie. The shebang has to be the
  97. # first line of a file, the #coding:xxx cookie cannot be
  98. # written before. So the shebang has to be decodable from
  99. # UTF-8.
  100. try:
  101. shebang.decode('utf-8')
  102. except UnicodeDecodeError:
  103. raise ValueError(
  104. 'The shebang (%r) is not decodable from utf-8' % shebang)
  105. # If the script is encoded to a custom encoding (use a
  106. # #coding:xxx cookie), the shebang has to be decodable from
  107. # the script encoding too.
  108. if encoding != 'utf-8':
  109. try:
  110. shebang.decode(encoding)
  111. except UnicodeDecodeError:
  112. raise ValueError(
  113. 'The shebang (%r) is not decodable '
  114. 'from the script encoding (%r)' % (shebang, encoding))
  115. return shebang
  116. def _get_script_text(self, entry):
  117. return self.script_template % dict(module=entry.prefix,
  118. func=entry.suffix)
  119. manifest = _DEFAULT_MANIFEST
  120. def get_manifest(self, exename):
  121. base = os.path.basename(exename)
  122. return self.manifest % base
  123. def _write_script(self, names, shebang, script_bytes, filenames, ext):
  124. use_launcher = self.add_launchers and os.name == 'nt'
  125. linesep = os.linesep.encode('utf-8')
  126. if not use_launcher:
  127. script_bytes = shebang + linesep + script_bytes
  128. else:
  129. if ext == 'py':
  130. launcher = self._get_launcher('t')
  131. else:
  132. launcher = self._get_launcher('w')
  133. stream = BytesIO()
  134. with ZipFile(stream, 'w') as zf:
  135. zf.writestr('__main__.py', script_bytes)
  136. zip_data = stream.getvalue()
  137. script_bytes = launcher + shebang + linesep + zip_data
  138. for name in names:
  139. outname = os.path.join(self.target_dir, name)
  140. if use_launcher:
  141. n, e = os.path.splitext(outname)
  142. if e.startswith('.py'):
  143. outname = n
  144. outname = '%s.exe' % outname
  145. try:
  146. self._fileop.write_binary_file(outname, script_bytes)
  147. except Exception:
  148. # Failed writing an executable - it might be in use.
  149. logger.warning('Failed to write executable - trying to '
  150. 'use .deleteme logic')
  151. dfname = '%s.deleteme' % outname
  152. if os.path.exists(dfname):
  153. os.remove(dfname) # Not allowed to fail here
  154. os.rename(outname, dfname) # nor here
  155. self._fileop.write_binary_file(outname, script_bytes)
  156. logger.debug('Able to replace executable using '
  157. '.deleteme logic')
  158. try:
  159. os.remove(dfname)
  160. except Exception:
  161. pass # still in use - ignore error
  162. else:
  163. if os.name == 'nt' and not outname.endswith('.' + ext):
  164. outname = '%s.%s' % (outname, ext)
  165. if os.path.exists(outname) and not self.clobber:
  166. logger.warning('Skipping existing file %s', outname)
  167. continue
  168. self._fileop.write_binary_file(outname, script_bytes)
  169. if self.set_mode:
  170. self._fileop.set_executable_mode([outname])
  171. filenames.append(outname)
  172. def _make_script(self, entry, filenames, options=None):
  173. shebang = self._get_shebang('utf-8', options=options)
  174. script = self._get_script_text(entry).encode('utf-8')
  175. name = entry.name
  176. scriptnames = set()
  177. if '' in self.variants:
  178. scriptnames.add(name)
  179. if 'X' in self.variants:
  180. scriptnames.add('%s%s' % (name, sys.version[0]))
  181. if 'X.Y' in self.variants:
  182. scriptnames.add('%s-%s' % (name, sys.version[:3]))
  183. if options and options.get('gui', False):
  184. ext = 'pyw'
  185. else:
  186. ext = 'py'
  187. self._write_script(scriptnames, shebang, script, filenames, ext)
  188. def _copy_script(self, script, filenames):
  189. adjust = False
  190. script = os.path.join(self.source_dir, convert_path(script))
  191. outname = os.path.join(self.target_dir, os.path.basename(script))
  192. if not self.force and not self._fileop.newer(script, outname):
  193. logger.debug('not copying %s (up-to-date)', script)
  194. return
  195. # Always open the file, but ignore failures in dry-run mode --
  196. # that way, we'll get accurate feedback if we can read the
  197. # script.
  198. try:
  199. f = open(script, 'rb')
  200. except IOError:
  201. if not self.dry_run:
  202. raise
  203. f = None
  204. else:
  205. encoding, lines = detect_encoding(f.readline)
  206. f.seek(0)
  207. first_line = f.readline()
  208. if not first_line:
  209. logger.warning('%s: %s is an empty file (skipping)',
  210. self.get_command_name(), script)
  211. return
  212. match = FIRST_LINE_RE.match(first_line.replace(b'\r\n', b'\n'))
  213. if match:
  214. adjust = True
  215. post_interp = match.group(1) or b''
  216. if not adjust:
  217. if f:
  218. f.close()
  219. self._fileop.copy_file(script, outname)
  220. if self.set_mode:
  221. self._fileop.set_executable_mode([outname])
  222. filenames.append(outname)
  223. else:
  224. logger.info('copying and adjusting %s -> %s', script,
  225. self.target_dir)
  226. if not self._fileop.dry_run:
  227. shebang = self._get_shebang(encoding, post_interp)
  228. if b'pythonw' in first_line:
  229. ext = 'pyw'
  230. else:
  231. ext = 'py'
  232. n = os.path.basename(outname)
  233. self._write_script([n], shebang, f.read(), filenames, ext)
  234. if f:
  235. f.close()
  236. @property
  237. def dry_run(self):
  238. return self._fileop.dry_run
  239. @dry_run.setter
  240. def dry_run(self, value):
  241. self._fileop.dry_run = value
  242. if os.name == 'nt':
  243. # Executable launcher support.
  244. # Launchers are from https://bitbucket.org/vinay.sajip/simple_launcher/
  245. def _get_launcher(self, kind):
  246. if struct.calcsize('P') == 8: # 64-bit
  247. bits = '64'
  248. else:
  249. bits = '32'
  250. name = '%s%s.exe' % (kind, bits)
  251. # Issue 31: don't hardcode an absolute package name, but
  252. # determine it relative to the current package
  253. distlib_package = __name__.rsplit('.', 1)[0]
  254. result = finder(distlib_package).find(name).bytes
  255. return result
  256. # Public API follows
  257. def make(self, specification, options=None):
  258. """
  259. Make a script.
  260. :param specification: The specification, which is either a valid export
  261. entry specification (to make a script from a
  262. callable) or a filename (to make a script by
  263. copying from a source location).
  264. :param options: A dictionary of options controlling script generation.
  265. :return: A list of all absolute pathnames written to.
  266. """
  267. filenames = []
  268. entry = get_export_entry(specification)
  269. if entry is None:
  270. self._copy_script(specification, filenames)
  271. else:
  272. self._make_script(entry, filenames, options=options)
  273. return filenames
  274. def make_multiple(self, specifications, options=None):
  275. """
  276. Take a list of specifications and make scripts from them,
  277. :param specifications: A list of specifications.
  278. :return: A list of all absolute pathnames written to,
  279. """
  280. filenames = []
  281. for specification in specifications:
  282. filenames.extend(self.make(specification, options))
  283. return filenames