You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

417 lines
16 KiB

4 years ago
  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. 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 = r'''# -*- 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. def _enquote_executable(executable):
  57. if ' ' in executable:
  58. # make sure we quote only the executable in case of env
  59. # for example /usr/bin/env "/dir with spaces/bin/jython"
  60. # instead of "/usr/bin/env /dir with spaces/bin/jython"
  61. # otherwise whole
  62. if executable.startswith('/usr/bin/env '):
  63. env, _executable = executable.split(' ', 1)
  64. if ' ' in _executable and not _executable.startswith('"'):
  65. executable = '%s "%s"' % (env, _executable)
  66. else:
  67. if not executable.startswith('"'):
  68. executable = '"%s"' % executable
  69. return executable
  70. class ScriptMaker(object):
  71. """
  72. A class to copy or create scripts from source scripts or callable
  73. specifications.
  74. """
  75. script_template = SCRIPT_TEMPLATE
  76. executable = None # for shebangs
  77. def __init__(self, source_dir, target_dir, add_launchers=True,
  78. dry_run=False, fileop=None):
  79. self.source_dir = source_dir
  80. self.target_dir = target_dir
  81. self.add_launchers = add_launchers
  82. self.force = False
  83. self.clobber = False
  84. # It only makes sense to set mode bits on POSIX.
  85. self.set_mode = (os.name == 'posix') or (os.name == 'java' and
  86. os._name == 'posix')
  87. self.variants = set(('', 'X.Y'))
  88. self._fileop = fileop or FileOperator(dry_run)
  89. self._is_nt = os.name == 'nt' or (
  90. os.name == 'java' and os._name == 'nt')
  91. def _get_alternate_executable(self, executable, options):
  92. if options.get('gui', False) and self._is_nt: # pragma: no cover
  93. dn, fn = os.path.split(executable)
  94. fn = fn.replace('python', 'pythonw')
  95. executable = os.path.join(dn, fn)
  96. return executable
  97. if sys.platform.startswith('java'): # pragma: no cover
  98. def _is_shell(self, executable):
  99. """
  100. Determine if the specified executable is a script
  101. (contains a #! line)
  102. """
  103. try:
  104. with open(executable) as fp:
  105. return fp.read(2) == '#!'
  106. except (OSError, IOError):
  107. logger.warning('Failed to open %s', executable)
  108. return False
  109. def _fix_jython_executable(self, executable):
  110. if self._is_shell(executable):
  111. # Workaround for Jython is not needed on Linux systems.
  112. import java
  113. if java.lang.System.getProperty('os.name') == 'Linux':
  114. return executable
  115. elif executable.lower().endswith('jython.exe'):
  116. # Use wrapper exe for Jython on Windows
  117. return executable
  118. return '/usr/bin/env %s' % executable
  119. def _build_shebang(self, executable, post_interp):
  120. """
  121. Build a shebang line. In the simple case (on Windows, or a shebang line
  122. which is not too long or contains spaces) use a simple formulation for
  123. the shebang. Otherwise, use /bin/sh as the executable, with a contrived
  124. shebang which allows the script to run either under Python or sh, using
  125. suitable quoting. Thanks to Harald Nordgren for his input.
  126. See also: http://www.in-ulm.de/~mascheck/various/shebang/#length
  127. https://hg.mozilla.org/mozilla-central/file/tip/mach
  128. """
  129. if os.name != 'posix':
  130. simple_shebang = True
  131. else:
  132. # Add 3 for '#!' prefix and newline suffix.
  133. shebang_length = len(executable) + len(post_interp) + 3
  134. if sys.platform == 'darwin':
  135. max_shebang_length = 512
  136. else:
  137. max_shebang_length = 127
  138. simple_shebang = ((b' ' not in executable) and
  139. (shebang_length <= max_shebang_length))
  140. if simple_shebang:
  141. result = b'#!' + executable + post_interp + b'\n'
  142. else:
  143. result = b'#!/bin/sh\n'
  144. result += b"'''exec' " + executable + post_interp + b' "$0" "$@"\n'
  145. result += b"' '''"
  146. return result
  147. def _get_shebang(self, encoding, post_interp=b'', options=None):
  148. enquote = True
  149. if self.executable:
  150. executable = self.executable
  151. enquote = False # assume this will be taken care of
  152. elif not sysconfig.is_python_build():
  153. executable = get_executable()
  154. elif in_venv(): # pragma: no cover
  155. executable = os.path.join(sysconfig.get_path('scripts'),
  156. 'python%s' % sysconfig.get_config_var('EXE'))
  157. else: # pragma: no cover
  158. executable = os.path.join(
  159. sysconfig.get_config_var('BINDIR'),
  160. 'python%s%s' % (sysconfig.get_config_var('VERSION'),
  161. sysconfig.get_config_var('EXE')))
  162. if options:
  163. executable = self._get_alternate_executable(executable, options)
  164. if sys.platform.startswith('java'): # pragma: no cover
  165. executable = self._fix_jython_executable(executable)
  166. # Normalise case for Windows
  167. executable = os.path.normcase(executable)
  168. # If the user didn't specify an executable, it may be necessary to
  169. # cater for executable paths with spaces (not uncommon on Windows)
  170. if enquote:
  171. executable = _enquote_executable(executable)
  172. # Issue #51: don't use fsencode, since we later try to
  173. # check that the shebang is decodable using utf-8.
  174. executable = executable.encode('utf-8')
  175. # in case of IronPython, play safe and enable frames support
  176. if (sys.platform == 'cli' and '-X:Frames' not in post_interp
  177. and '-X:FullFrames' not in post_interp): # pragma: no cover
  178. post_interp += b' -X:Frames'
  179. shebang = self._build_shebang(executable, post_interp)
  180. # Python parser starts to read a script using UTF-8 until
  181. # it gets a #coding:xxx cookie. The shebang has to be the
  182. # first line of a file, the #coding:xxx cookie cannot be
  183. # written before. So the shebang has to be decodable from
  184. # UTF-8.
  185. try:
  186. shebang.decode('utf-8')
  187. except UnicodeDecodeError: # pragma: no cover
  188. raise ValueError(
  189. 'The shebang (%r) is not decodable from utf-8' % shebang)
  190. # If the script is encoded to a custom encoding (use a
  191. # #coding:xxx cookie), the shebang has to be decodable from
  192. # the script encoding too.
  193. if encoding != 'utf-8':
  194. try:
  195. shebang.decode(encoding)
  196. except UnicodeDecodeError: # pragma: no cover
  197. raise ValueError(
  198. 'The shebang (%r) is not decodable '
  199. 'from the script encoding (%r)' % (shebang, encoding))
  200. return shebang
  201. def _get_script_text(self, entry):
  202. return self.script_template % dict(module=entry.prefix,
  203. func=entry.suffix)
  204. manifest = _DEFAULT_MANIFEST
  205. def get_manifest(self, exename):
  206. base = os.path.basename(exename)
  207. return self.manifest % base
  208. def _write_script(self, names, shebang, script_bytes, filenames, ext):
  209. use_launcher = self.add_launchers and self._is_nt
  210. linesep = os.linesep.encode('utf-8')
  211. if not shebang.endswith(linesep):
  212. shebang += linesep
  213. if not use_launcher:
  214. script_bytes = shebang + script_bytes
  215. else: # pragma: no cover
  216. if ext == 'py':
  217. launcher = self._get_launcher('t')
  218. else:
  219. launcher = self._get_launcher('w')
  220. stream = BytesIO()
  221. with ZipFile(stream, 'w') as zf:
  222. zf.writestr('__main__.py', script_bytes)
  223. zip_data = stream.getvalue()
  224. script_bytes = launcher + shebang + zip_data
  225. for name in names:
  226. outname = os.path.join(self.target_dir, name)
  227. if use_launcher: # pragma: no cover
  228. n, e = os.path.splitext(outname)
  229. if e.startswith('.py'):
  230. outname = n
  231. outname = '%s.exe' % outname
  232. try:
  233. self._fileop.write_binary_file(outname, script_bytes)
  234. except Exception:
  235. # Failed writing an executable - it might be in use.
  236. logger.warning('Failed to write executable - trying to '
  237. 'use .deleteme logic')
  238. dfname = '%s.deleteme' % outname
  239. if os.path.exists(dfname):
  240. os.remove(dfname) # Not allowed to fail here
  241. os.rename(outname, dfname) # nor here
  242. self._fileop.write_binary_file(outname, script_bytes)
  243. logger.debug('Able to replace executable using '
  244. '.deleteme logic')
  245. try:
  246. os.remove(dfname)
  247. except Exception:
  248. pass # still in use - ignore error
  249. else:
  250. if self._is_nt and not outname.endswith('.' + ext): # pragma: no cover
  251. outname = '%s.%s' % (outname, ext)
  252. if os.path.exists(outname) and not self.clobber:
  253. logger.warning('Skipping existing file %s', outname)
  254. continue
  255. self._fileop.write_binary_file(outname, script_bytes)
  256. if self.set_mode:
  257. self._fileop.set_executable_mode([outname])
  258. filenames.append(outname)
  259. def _make_script(self, entry, filenames, options=None):
  260. post_interp = b''
  261. if options:
  262. args = options.get('interpreter_args', [])
  263. if args:
  264. args = ' %s' % ' '.join(args)
  265. post_interp = args.encode('utf-8')
  266. shebang = self._get_shebang('utf-8', post_interp, options=options)
  267. script = self._get_script_text(entry).encode('utf-8')
  268. name = entry.name
  269. scriptnames = set()
  270. if '' in self.variants:
  271. scriptnames.add(name)
  272. if 'X' in self.variants:
  273. scriptnames.add('%s%s' % (name, sys.version[0]))
  274. if 'X.Y' in self.variants:
  275. scriptnames.add('%s-%s' % (name, sys.version[:3]))
  276. if options and options.get('gui', False):
  277. ext = 'pyw'
  278. else:
  279. ext = 'py'
  280. self._write_script(scriptnames, shebang, script, filenames, ext)
  281. def _copy_script(self, script, filenames):
  282. adjust = False
  283. script = os.path.join(self.source_dir, convert_path(script))
  284. outname = os.path.join(self.target_dir, os.path.basename(script))
  285. if not self.force and not self._fileop.newer(script, outname):
  286. logger.debug('not copying %s (up-to-date)', script)
  287. return
  288. # Always open the file, but ignore failures in dry-run mode --
  289. # that way, we'll get accurate feedback if we can read the
  290. # script.
  291. try:
  292. f = open(script, 'rb')
  293. except IOError: # pragma: no cover
  294. if not self.dry_run:
  295. raise
  296. f = None
  297. else:
  298. first_line = f.readline()
  299. if not first_line: # pragma: no cover
  300. logger.warning('%s: %s is an empty file (skipping)',
  301. self.get_command_name(), script)
  302. return
  303. match = FIRST_LINE_RE.match(first_line.replace(b'\r\n', b'\n'))
  304. if match:
  305. adjust = True
  306. post_interp = match.group(1) or b''
  307. if not adjust:
  308. if f:
  309. f.close()
  310. self._fileop.copy_file(script, outname)
  311. if self.set_mode:
  312. self._fileop.set_executable_mode([outname])
  313. filenames.append(outname)
  314. else:
  315. logger.info('copying and adjusting %s -> %s', script,
  316. self.target_dir)
  317. if not self._fileop.dry_run:
  318. encoding, lines = detect_encoding(f.readline)
  319. f.seek(0)
  320. shebang = self._get_shebang(encoding, post_interp)
  321. if b'pythonw' in first_line: # pragma: no cover
  322. ext = 'pyw'
  323. else:
  324. ext = 'py'
  325. n = os.path.basename(outname)
  326. self._write_script([n], shebang, f.read(), filenames, ext)
  327. if f:
  328. f.close()
  329. @property
  330. def dry_run(self):
  331. return self._fileop.dry_run
  332. @dry_run.setter
  333. def dry_run(self, value):
  334. self._fileop.dry_run = value
  335. if os.name == 'nt' or (os.name == 'java' and os._name == 'nt'): # pragma: no cover
  336. # Executable launcher support.
  337. # Launchers are from https://bitbucket.org/vinay.sajip/simple_launcher/
  338. def _get_launcher(self, kind):
  339. if struct.calcsize('P') == 8: # 64-bit
  340. bits = '64'
  341. else:
  342. bits = '32'
  343. name = '%s%s.exe' % (kind, bits)
  344. # Issue 31: don't hardcode an absolute package name, but
  345. # determine it relative to the current package
  346. distlib_package = __name__.rsplit('.', 1)[0]
  347. result = finder(distlib_package).find(name).bytes
  348. return result
  349. # Public API follows
  350. def make(self, specification, options=None):
  351. """
  352. Make a script.
  353. :param specification: The specification, which is either a valid export
  354. entry specification (to make a script from a
  355. callable) or a filename (to make a script by
  356. copying from a source location).
  357. :param options: A dictionary of options controlling script generation.
  358. :return: A list of all absolute pathnames written to.
  359. """
  360. filenames = []
  361. entry = get_export_entry(specification)
  362. if entry is None:
  363. self._copy_script(specification, filenames)
  364. else:
  365. self._make_script(entry, filenames, options=options)
  366. return filenames
  367. def make_multiple(self, specifications, options=None):
  368. """
  369. Take a list of specifications and make scripts from them,
  370. :param specifications: A list of specifications.
  371. :return: A list of all absolute pathnames written to,
  372. """
  373. filenames = []
  374. for specification in specifications:
  375. filenames.extend(self.make(specification, options))
  376. return filenames