scripts.py 13 KB

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