compileall.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. """Module/script to byte-compile all .py files to .pyc files.
  2. When called as a script with arguments, this compiles the directories
  3. given as arguments recursively; the -l option prevents it from
  4. recursing into directories.
  5. Without arguments, it compiles all modules on sys.path, without
  6. recursing into subdirectories. (Even though it should do so for
  7. packages -- for now, you'll have to deal with packages separately.)
  8. See module py_compile for details of the actual byte-compilation.
  9. """
  10. import os
  11. import sys
  12. import importlib.util
  13. import py_compile
  14. import struct
  15. import filecmp
  16. from functools import partial
  17. from pathlib import Path
  18. __all__ = ["compile_dir","compile_file","compile_path"]
  19. def _walk_dir(dir, maxlevels, quiet=0):
  20. if quiet < 2 and isinstance(dir, os.PathLike):
  21. dir = os.fspath(dir)
  22. if not quiet:
  23. print('Listing {!r}...'.format(dir))
  24. try:
  25. names = os.listdir(dir)
  26. except OSError:
  27. if quiet < 2:
  28. print("Can't list {!r}".format(dir))
  29. names = []
  30. names.sort()
  31. for name in names:
  32. if name == '__pycache__':
  33. continue
  34. fullname = os.path.join(dir, name)
  35. if not os.path.isdir(fullname):
  36. yield fullname
  37. elif (maxlevels > 0 and name != os.curdir and name != os.pardir and
  38. os.path.isdir(fullname) and not os.path.islink(fullname)):
  39. yield from _walk_dir(fullname, maxlevels=maxlevels - 1,
  40. quiet=quiet)
  41. def compile_dir(dir, maxlevels=None, ddir=None, force=False,
  42. rx=None, quiet=0, legacy=False, optimize=-1, workers=1,
  43. invalidation_mode=None, *, stripdir=None,
  44. prependdir=None, limit_sl_dest=None, hardlink_dupes=False):
  45. """Byte-compile all modules in the given directory tree.
  46. Arguments (only dir is required):
  47. dir: the directory to byte-compile
  48. maxlevels: maximum recursion level (default `sys.getrecursionlimit()`)
  49. ddir: the directory that will be prepended to the path to the
  50. file as it is compiled into each byte-code file.
  51. force: if True, force compilation, even if timestamps are up-to-date
  52. quiet: full output with False or 0, errors only with 1,
  53. no output with 2
  54. legacy: if True, produce legacy pyc paths instead of PEP 3147 paths
  55. optimize: int or list of optimization levels or -1 for level of
  56. the interpreter. Multiple levels leads to multiple compiled
  57. files each with one optimization level.
  58. workers: maximum number of parallel workers
  59. invalidation_mode: how the up-to-dateness of the pyc will be checked
  60. stripdir: part of path to left-strip from source file path
  61. prependdir: path to prepend to beginning of original file path, applied
  62. after stripdir
  63. limit_sl_dest: ignore symlinks if they are pointing outside of
  64. the defined path
  65. hardlink_dupes: hardlink duplicated pyc files
  66. """
  67. ProcessPoolExecutor = None
  68. if ddir is not None and (stripdir is not None or prependdir is not None):
  69. raise ValueError(("Destination dir (ddir) cannot be used "
  70. "in combination with stripdir or prependdir"))
  71. if ddir is not None:
  72. stripdir = dir
  73. prependdir = ddir
  74. ddir = None
  75. if workers < 0:
  76. raise ValueError('workers must be greater or equal to 0')
  77. if workers != 1:
  78. # Check if this is a system where ProcessPoolExecutor can function.
  79. from concurrent.futures.process import _check_system_limits
  80. try:
  81. _check_system_limits()
  82. except NotImplementedError:
  83. workers = 1
  84. else:
  85. from concurrent.futures import ProcessPoolExecutor
  86. if maxlevels is None:
  87. maxlevels = sys.getrecursionlimit()
  88. files = _walk_dir(dir, quiet=quiet, maxlevels=maxlevels)
  89. success = True
  90. if workers != 1 and ProcessPoolExecutor is not None:
  91. import multiprocessing
  92. if multiprocessing.get_start_method() == 'fork':
  93. mp_context = multiprocessing.get_context('forkserver')
  94. else:
  95. mp_context = None
  96. # If workers == 0, let ProcessPoolExecutor choose
  97. workers = workers or None
  98. with ProcessPoolExecutor(max_workers=workers,
  99. mp_context=mp_context) as executor:
  100. results = executor.map(partial(compile_file,
  101. ddir=ddir, force=force,
  102. rx=rx, quiet=quiet,
  103. legacy=legacy,
  104. optimize=optimize,
  105. invalidation_mode=invalidation_mode,
  106. stripdir=stripdir,
  107. prependdir=prependdir,
  108. limit_sl_dest=limit_sl_dest,
  109. hardlink_dupes=hardlink_dupes),
  110. files)
  111. success = min(results, default=True)
  112. else:
  113. for file in files:
  114. if not compile_file(file, ddir, force, rx, quiet,
  115. legacy, optimize, invalidation_mode,
  116. stripdir=stripdir, prependdir=prependdir,
  117. limit_sl_dest=limit_sl_dest,
  118. hardlink_dupes=hardlink_dupes):
  119. success = False
  120. return success
  121. def compile_file(fullname, ddir=None, force=False, rx=None, quiet=0,
  122. legacy=False, optimize=-1,
  123. invalidation_mode=None, *, stripdir=None, prependdir=None,
  124. limit_sl_dest=None, hardlink_dupes=False):
  125. """Byte-compile one file.
  126. Arguments (only fullname is required):
  127. fullname: the file to byte-compile
  128. ddir: if given, the directory name compiled in to the
  129. byte-code file.
  130. force: if True, force compilation, even if timestamps are up-to-date
  131. quiet: full output with False or 0, errors only with 1,
  132. no output with 2
  133. legacy: if True, produce legacy pyc paths instead of PEP 3147 paths
  134. optimize: int or list of optimization levels or -1 for level of
  135. the interpreter. Multiple levels leads to multiple compiled
  136. files each with one optimization level.
  137. invalidation_mode: how the up-to-dateness of the pyc will be checked
  138. stripdir: part of path to left-strip from source file path
  139. prependdir: path to prepend to beginning of original file path, applied
  140. after stripdir
  141. limit_sl_dest: ignore symlinks if they are pointing outside of
  142. the defined path.
  143. hardlink_dupes: hardlink duplicated pyc files
  144. """
  145. if ddir is not None and (stripdir is not None or prependdir is not None):
  146. raise ValueError(("Destination dir (ddir) cannot be used "
  147. "in combination with stripdir or prependdir"))
  148. success = True
  149. fullname = os.fspath(fullname)
  150. stripdir = os.fspath(stripdir) if stripdir is not None else None
  151. name = os.path.basename(fullname)
  152. dfile = None
  153. if ddir is not None:
  154. dfile = os.path.join(ddir, name)
  155. if stripdir is not None:
  156. fullname_parts = fullname.split(os.path.sep)
  157. stripdir_parts = stripdir.split(os.path.sep)
  158. ddir_parts = list(fullname_parts)
  159. for spart, opart in zip(stripdir_parts, fullname_parts):
  160. if spart == opart:
  161. ddir_parts.remove(spart)
  162. dfile = os.path.join(*ddir_parts)
  163. if prependdir is not None:
  164. if dfile is None:
  165. dfile = os.path.join(prependdir, fullname)
  166. else:
  167. dfile = os.path.join(prependdir, dfile)
  168. if isinstance(optimize, int):
  169. optimize = [optimize]
  170. # Use set() to remove duplicates.
  171. # Use sorted() to create pyc files in a deterministic order.
  172. optimize = sorted(set(optimize))
  173. if hardlink_dupes and len(optimize) < 2:
  174. raise ValueError("Hardlinking of duplicated bytecode makes sense "
  175. "only for more than one optimization level")
  176. if rx is not None:
  177. mo = rx.search(fullname)
  178. if mo:
  179. return success
  180. if limit_sl_dest is not None and os.path.islink(fullname):
  181. if Path(limit_sl_dest).resolve() not in Path(fullname).resolve().parents:
  182. return success
  183. opt_cfiles = {}
  184. if os.path.isfile(fullname):
  185. for opt_level in optimize:
  186. if legacy:
  187. opt_cfiles[opt_level] = fullname + 'c'
  188. else:
  189. if opt_level >= 0:
  190. opt = opt_level if opt_level >= 1 else ''
  191. cfile = (importlib.util.cache_from_source(
  192. fullname, optimization=opt))
  193. opt_cfiles[opt_level] = cfile
  194. else:
  195. cfile = importlib.util.cache_from_source(fullname)
  196. opt_cfiles[opt_level] = cfile
  197. head, tail = name[:-3], name[-3:]
  198. if tail == '.py':
  199. if not force:
  200. try:
  201. mtime = int(os.stat(fullname).st_mtime)
  202. expect = struct.pack('<4sLL', importlib.util.MAGIC_NUMBER,
  203. 0, mtime & 0xFFFF_FFFF)
  204. for cfile in opt_cfiles.values():
  205. with open(cfile, 'rb') as chandle:
  206. actual = chandle.read(12)
  207. if expect != actual:
  208. break
  209. else:
  210. return success
  211. except OSError:
  212. pass
  213. if not quiet:
  214. print('Compiling {!r}...'.format(fullname))
  215. try:
  216. for index, opt_level in enumerate(optimize):
  217. cfile = opt_cfiles[opt_level]
  218. ok = py_compile.compile(fullname, cfile, dfile, True,
  219. optimize=opt_level,
  220. invalidation_mode=invalidation_mode)
  221. if index > 0 and hardlink_dupes:
  222. previous_cfile = opt_cfiles[optimize[index - 1]]
  223. if filecmp.cmp(cfile, previous_cfile, shallow=False):
  224. os.unlink(cfile)
  225. os.link(previous_cfile, cfile)
  226. except py_compile.PyCompileError as err:
  227. success = False
  228. if quiet >= 2:
  229. return success
  230. elif quiet:
  231. print('*** Error compiling {!r}...'.format(fullname))
  232. else:
  233. print('*** ', end='')
  234. # escape non-printable characters in msg
  235. encoding = sys.stdout.encoding or sys.getdefaultencoding()
  236. msg = err.msg.encode(encoding, errors='backslashreplace').decode(encoding)
  237. print(msg)
  238. except (SyntaxError, UnicodeError, OSError) as e:
  239. success = False
  240. if quiet >= 2:
  241. return success
  242. elif quiet:
  243. print('*** Error compiling {!r}...'.format(fullname))
  244. else:
  245. print('*** ', end='')
  246. print(e.__class__.__name__ + ':', e)
  247. else:
  248. if ok == 0:
  249. success = False
  250. return success
  251. def compile_path(skip_curdir=1, maxlevels=0, force=False, quiet=0,
  252. legacy=False, optimize=-1,
  253. invalidation_mode=None):
  254. """Byte-compile all module on sys.path.
  255. Arguments (all optional):
  256. skip_curdir: if true, skip current directory (default True)
  257. maxlevels: max recursion level (default 0)
  258. force: as for compile_dir() (default False)
  259. quiet: as for compile_dir() (default 0)
  260. legacy: as for compile_dir() (default False)
  261. optimize: as for compile_dir() (default -1)
  262. invalidation_mode: as for compiler_dir()
  263. """
  264. success = True
  265. for dir in sys.path:
  266. if (not dir or dir == os.curdir) and skip_curdir:
  267. if quiet < 2:
  268. print('Skipping current directory')
  269. else:
  270. success = success and compile_dir(
  271. dir,
  272. maxlevels,
  273. None,
  274. force,
  275. quiet=quiet,
  276. legacy=legacy,
  277. optimize=optimize,
  278. invalidation_mode=invalidation_mode,
  279. )
  280. return success
  281. def main():
  282. """Script main program."""
  283. import argparse
  284. parser = argparse.ArgumentParser(
  285. description='Utilities to support installing Python libraries.')
  286. parser.add_argument('-l', action='store_const', const=0,
  287. default=None, dest='maxlevels',
  288. help="don't recurse into subdirectories")
  289. parser.add_argument('-r', type=int, dest='recursion',
  290. help=('control the maximum recursion level. '
  291. 'if `-l` and `-r` options are specified, '
  292. 'then `-r` takes precedence.'))
  293. parser.add_argument('-f', action='store_true', dest='force',
  294. help='force rebuild even if timestamps are up to date')
  295. parser.add_argument('-q', action='count', dest='quiet', default=0,
  296. help='output only error messages; -qq will suppress '
  297. 'the error messages as well.')
  298. parser.add_argument('-b', action='store_true', dest='legacy',
  299. help='use legacy (pre-PEP3147) compiled file locations')
  300. parser.add_argument('-d', metavar='DESTDIR', dest='ddir', default=None,
  301. help=('directory to prepend to file paths for use in '
  302. 'compile-time tracebacks and in runtime '
  303. 'tracebacks in cases where the source file is '
  304. 'unavailable'))
  305. parser.add_argument('-s', metavar='STRIPDIR', dest='stripdir',
  306. default=None,
  307. help=('part of path to left-strip from path '
  308. 'to source file - for example buildroot. '
  309. '`-d` and `-s` options cannot be '
  310. 'specified together.'))
  311. parser.add_argument('-p', metavar='PREPENDDIR', dest='prependdir',
  312. default=None,
  313. help=('path to add as prefix to path '
  314. 'to source file - for example / to make '
  315. 'it absolute when some part is removed '
  316. 'by `-s` option. '
  317. '`-d` and `-p` options cannot be '
  318. 'specified together.'))
  319. parser.add_argument('-x', metavar='REGEXP', dest='rx', default=None,
  320. help=('skip files matching the regular expression; '
  321. 'the regexp is searched for in the full path '
  322. 'of each file considered for compilation'))
  323. parser.add_argument('-i', metavar='FILE', dest='flist',
  324. help=('add all the files and directories listed in '
  325. 'FILE to the list considered for compilation; '
  326. 'if "-", names are read from stdin'))
  327. parser.add_argument('compile_dest', metavar='FILE|DIR', nargs='*',
  328. help=('zero or more file and directory names '
  329. 'to compile; if no arguments given, defaults '
  330. 'to the equivalent of -l sys.path'))
  331. parser.add_argument('-j', '--workers', default=1,
  332. type=int, help='Run compileall concurrently')
  333. invalidation_modes = [mode.name.lower().replace('_', '-')
  334. for mode in py_compile.PycInvalidationMode]
  335. parser.add_argument('--invalidation-mode',
  336. choices=sorted(invalidation_modes),
  337. help=('set .pyc invalidation mode; defaults to '
  338. '"checked-hash" if the SOURCE_DATE_EPOCH '
  339. 'environment variable is set, and '
  340. '"timestamp" otherwise.'))
  341. parser.add_argument('-o', action='append', type=int, dest='opt_levels',
  342. help=('Optimization levels to run compilation with. '
  343. 'Default is -1 which uses the optimization level '
  344. 'of the Python interpreter itself (see -O).'))
  345. parser.add_argument('-e', metavar='DIR', dest='limit_sl_dest',
  346. help='Ignore symlinks pointing outsite of the DIR')
  347. parser.add_argument('--hardlink-dupes', action='store_true',
  348. dest='hardlink_dupes',
  349. help='Hardlink duplicated pyc files')
  350. args = parser.parse_args()
  351. compile_dests = args.compile_dest
  352. if args.rx:
  353. import re
  354. args.rx = re.compile(args.rx)
  355. if args.limit_sl_dest == "":
  356. args.limit_sl_dest = None
  357. if args.recursion is not None:
  358. maxlevels = args.recursion
  359. else:
  360. maxlevels = args.maxlevels
  361. if args.opt_levels is None:
  362. args.opt_levels = [-1]
  363. if len(args.opt_levels) == 1 and args.hardlink_dupes:
  364. parser.error(("Hardlinking of duplicated bytecode makes sense "
  365. "only for more than one optimization level."))
  366. if args.ddir is not None and (
  367. args.stripdir is not None or args.prependdir is not None
  368. ):
  369. parser.error("-d cannot be used in combination with -s or -p")
  370. # if flist is provided then load it
  371. if args.flist:
  372. try:
  373. with (sys.stdin if args.flist=='-' else
  374. open(args.flist, encoding="utf-8")) as f:
  375. for line in f:
  376. compile_dests.append(line.strip())
  377. except OSError:
  378. if args.quiet < 2:
  379. print("Error reading file list {}".format(args.flist))
  380. return False
  381. if args.invalidation_mode:
  382. ivl_mode = args.invalidation_mode.replace('-', '_').upper()
  383. invalidation_mode = py_compile.PycInvalidationMode[ivl_mode]
  384. else:
  385. invalidation_mode = None
  386. success = True
  387. try:
  388. if compile_dests:
  389. for dest in compile_dests:
  390. if os.path.isfile(dest):
  391. if not compile_file(dest, args.ddir, args.force, args.rx,
  392. args.quiet, args.legacy,
  393. invalidation_mode=invalidation_mode,
  394. stripdir=args.stripdir,
  395. prependdir=args.prependdir,
  396. optimize=args.opt_levels,
  397. limit_sl_dest=args.limit_sl_dest,
  398. hardlink_dupes=args.hardlink_dupes):
  399. success = False
  400. else:
  401. if not compile_dir(dest, maxlevels, args.ddir,
  402. args.force, args.rx, args.quiet,
  403. args.legacy, workers=args.workers,
  404. invalidation_mode=invalidation_mode,
  405. stripdir=args.stripdir,
  406. prependdir=args.prependdir,
  407. optimize=args.opt_levels,
  408. limit_sl_dest=args.limit_sl_dest,
  409. hardlink_dupes=args.hardlink_dupes):
  410. success = False
  411. return success
  412. else:
  413. return compile_path(legacy=args.legacy, force=args.force,
  414. quiet=args.quiet,
  415. invalidation_mode=invalidation_mode)
  416. except KeyboardInterrupt:
  417. if args.quiet < 2:
  418. print("\n[interrupted]")
  419. return False
  420. return True
  421. if __name__ == '__main__':
  422. exit_status = int(not main())
  423. sys.exit(exit_status)