build_py.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. """distutils.command.build_py
  2. Implements the Distutils 'build_py' command."""
  3. import os
  4. import importlib.util
  5. import sys
  6. import glob
  7. from ..core import Command
  8. from ..errors import DistutilsOptionError, DistutilsFileError
  9. from ..util import convert_path
  10. from distutils._log import log
  11. class build_py(Command):
  12. description = "\"build\" pure Python modules (copy to build directory)"
  13. user_options = [
  14. ('build-lib=', 'd', "directory to \"build\" (copy) to"),
  15. ('compile', 'c', "compile .py to .pyc"),
  16. ('no-compile', None, "don't compile .py files [default]"),
  17. (
  18. 'optimize=',
  19. 'O',
  20. "also compile with optimization: -O1 for \"python -O\", "
  21. "-O2 for \"python -OO\", and -O0 to disable [default: -O0]",
  22. ),
  23. ('force', 'f', "forcibly build everything (ignore file timestamps)"),
  24. ]
  25. boolean_options = ['compile', 'force']
  26. negative_opt = {'no-compile': 'compile'}
  27. def initialize_options(self):
  28. self.build_lib = None
  29. self.py_modules = None
  30. self.package = None
  31. self.package_data = None
  32. self.package_dir = None
  33. self.compile = 0
  34. self.optimize = 0
  35. self.force = None
  36. def finalize_options(self):
  37. self.set_undefined_options(
  38. 'build', ('build_lib', 'build_lib'), ('force', 'force')
  39. )
  40. # Get the distribution options that are aliases for build_py
  41. # options -- list of packages and list of modules.
  42. self.packages = self.distribution.packages
  43. self.py_modules = self.distribution.py_modules
  44. self.package_data = self.distribution.package_data
  45. self.package_dir = {}
  46. if self.distribution.package_dir:
  47. for name, path in self.distribution.package_dir.items():
  48. self.package_dir[name] = convert_path(path)
  49. self.data_files = self.get_data_files()
  50. # Ick, copied straight from install_lib.py (fancy_getopt needs a
  51. # type system! Hell, *everything* needs a type system!!!)
  52. if not isinstance(self.optimize, int):
  53. try:
  54. self.optimize = int(self.optimize)
  55. assert 0 <= self.optimize <= 2
  56. except (ValueError, AssertionError):
  57. raise DistutilsOptionError("optimize must be 0, 1, or 2")
  58. def run(self):
  59. # XXX copy_file by default preserves atime and mtime. IMHO this is
  60. # the right thing to do, but perhaps it should be an option -- in
  61. # particular, a site administrator might want installed files to
  62. # reflect the time of installation rather than the last
  63. # modification time before the installed release.
  64. # XXX copy_file by default preserves mode, which appears to be the
  65. # wrong thing to do: if a file is read-only in the working
  66. # directory, we want it to be installed read/write so that the next
  67. # installation of the same module distribution can overwrite it
  68. # without problems. (This might be a Unix-specific issue.) Thus
  69. # we turn off 'preserve_mode' when copying to the build directory,
  70. # since the build directory is supposed to be exactly what the
  71. # installation will look like (ie. we preserve mode when
  72. # installing).
  73. # Two options control which modules will be installed: 'packages'
  74. # and 'py_modules'. The former lets us work with whole packages, not
  75. # specifying individual modules at all; the latter is for
  76. # specifying modules one-at-a-time.
  77. if self.py_modules:
  78. self.build_modules()
  79. if self.packages:
  80. self.build_packages()
  81. self.build_package_data()
  82. self.byte_compile(self.get_outputs(include_bytecode=0))
  83. def get_data_files(self):
  84. """Generate list of '(package,src_dir,build_dir,filenames)' tuples"""
  85. data = []
  86. if not self.packages:
  87. return data
  88. for package in self.packages:
  89. # Locate package source directory
  90. src_dir = self.get_package_dir(package)
  91. # Compute package build directory
  92. build_dir = os.path.join(*([self.build_lib] + package.split('.')))
  93. # Length of path to strip from found files
  94. plen = 0
  95. if src_dir:
  96. plen = len(src_dir) + 1
  97. # Strip directory from globbed filenames
  98. filenames = [file[plen:] for file in self.find_data_files(package, src_dir)]
  99. data.append((package, src_dir, build_dir, filenames))
  100. return data
  101. def find_data_files(self, package, src_dir):
  102. """Return filenames for package's data files in 'src_dir'"""
  103. globs = self.package_data.get('', []) + self.package_data.get(package, [])
  104. files = []
  105. for pattern in globs:
  106. # Each pattern has to be converted to a platform-specific path
  107. filelist = glob.glob(
  108. os.path.join(glob.escape(src_dir), convert_path(pattern))
  109. )
  110. # Files that match more than one pattern are only added once
  111. files.extend(
  112. [fn for fn in filelist if fn not in files and os.path.isfile(fn)]
  113. )
  114. return files
  115. def build_package_data(self):
  116. """Copy data files into build directory"""
  117. for package, src_dir, build_dir, filenames in self.data_files:
  118. for filename in filenames:
  119. target = os.path.join(build_dir, filename)
  120. self.mkpath(os.path.dirname(target))
  121. self.copy_file(
  122. os.path.join(src_dir, filename), target, preserve_mode=False
  123. )
  124. def get_package_dir(self, package):
  125. """Return the directory, relative to the top of the source
  126. distribution, where package 'package' should be found
  127. (at least according to the 'package_dir' option, if any)."""
  128. path = package.split('.')
  129. if not self.package_dir:
  130. if path:
  131. return os.path.join(*path)
  132. else:
  133. return ''
  134. else:
  135. tail = []
  136. while path:
  137. try:
  138. pdir = self.package_dir['.'.join(path)]
  139. except KeyError:
  140. tail.insert(0, path[-1])
  141. del path[-1]
  142. else:
  143. tail.insert(0, pdir)
  144. return os.path.join(*tail)
  145. else:
  146. # Oops, got all the way through 'path' without finding a
  147. # match in package_dir. If package_dir defines a directory
  148. # for the root (nameless) package, then fallback on it;
  149. # otherwise, we might as well have not consulted
  150. # package_dir at all, as we just use the directory implied
  151. # by 'tail' (which should be the same as the original value
  152. # of 'path' at this point).
  153. pdir = self.package_dir.get('')
  154. if pdir is not None:
  155. tail.insert(0, pdir)
  156. if tail:
  157. return os.path.join(*tail)
  158. else:
  159. return ''
  160. def check_package(self, package, package_dir):
  161. # Empty dir name means current directory, which we can probably
  162. # assume exists. Also, os.path.exists and isdir don't know about
  163. # my "empty string means current dir" convention, so we have to
  164. # circumvent them.
  165. if package_dir != "":
  166. if not os.path.exists(package_dir):
  167. raise DistutilsFileError(
  168. "package directory '%s' does not exist" % package_dir
  169. )
  170. if not os.path.isdir(package_dir):
  171. raise DistutilsFileError(
  172. "supposed package directory '%s' exists, "
  173. "but is not a directory" % package_dir
  174. )
  175. # Directories without __init__.py are namespace packages (PEP 420).
  176. if package:
  177. init_py = os.path.join(package_dir, "__init__.py")
  178. if os.path.isfile(init_py):
  179. return init_py
  180. # Either not in a package at all (__init__.py not expected), or
  181. # __init__.py doesn't exist -- so don't return the filename.
  182. return None
  183. def check_module(self, module, module_file):
  184. if not os.path.isfile(module_file):
  185. log.warning("file %s (for module %s) not found", module_file, module)
  186. return False
  187. else:
  188. return True
  189. def find_package_modules(self, package, package_dir):
  190. self.check_package(package, package_dir)
  191. module_files = glob.glob(os.path.join(glob.escape(package_dir), "*.py"))
  192. modules = []
  193. setup_script = os.path.abspath(self.distribution.script_name)
  194. for f in module_files:
  195. abs_f = os.path.abspath(f)
  196. if abs_f != setup_script:
  197. module = os.path.splitext(os.path.basename(f))[0]
  198. modules.append((package, module, f))
  199. else:
  200. self.debug_print("excluding %s" % setup_script)
  201. return modules
  202. def find_modules(self):
  203. """Finds individually-specified Python modules, ie. those listed by
  204. module name in 'self.py_modules'. Returns a list of tuples (package,
  205. module_base, filename): 'package' is a tuple of the path through
  206. package-space to the module; 'module_base' is the bare (no
  207. packages, no dots) module name, and 'filename' is the path to the
  208. ".py" file (relative to the distribution root) that implements the
  209. module.
  210. """
  211. # Map package names to tuples of useful info about the package:
  212. # (package_dir, checked)
  213. # package_dir - the directory where we'll find source files for
  214. # this package
  215. # checked - true if we have checked that the package directory
  216. # is valid (exists, contains __init__.py, ... ?)
  217. packages = {}
  218. # List of (package, module, filename) tuples to return
  219. modules = []
  220. # We treat modules-in-packages almost the same as toplevel modules,
  221. # just the "package" for a toplevel is empty (either an empty
  222. # string or empty list, depending on context). Differences:
  223. # - don't check for __init__.py in directory for empty package
  224. for module in self.py_modules:
  225. path = module.split('.')
  226. package = '.'.join(path[0:-1])
  227. module_base = path[-1]
  228. try:
  229. (package_dir, checked) = packages[package]
  230. except KeyError:
  231. package_dir = self.get_package_dir(package)
  232. checked = 0
  233. if not checked:
  234. init_py = self.check_package(package, package_dir)
  235. packages[package] = (package_dir, 1)
  236. if init_py:
  237. modules.append((package, "__init__", init_py))
  238. # XXX perhaps we should also check for just .pyc files
  239. # (so greedy closed-source bastards can distribute Python
  240. # modules too)
  241. module_file = os.path.join(package_dir, module_base + ".py")
  242. if not self.check_module(module, module_file):
  243. continue
  244. modules.append((package, module_base, module_file))
  245. return modules
  246. def find_all_modules(self):
  247. """Compute the list of all modules that will be built, whether
  248. they are specified one-module-at-a-time ('self.py_modules') or
  249. by whole packages ('self.packages'). Return a list of tuples
  250. (package, module, module_file), just like 'find_modules()' and
  251. 'find_package_modules()' do."""
  252. modules = []
  253. if self.py_modules:
  254. modules.extend(self.find_modules())
  255. if self.packages:
  256. for package in self.packages:
  257. package_dir = self.get_package_dir(package)
  258. m = self.find_package_modules(package, package_dir)
  259. modules.extend(m)
  260. return modules
  261. def get_source_files(self):
  262. return [module[-1] for module in self.find_all_modules()]
  263. def get_module_outfile(self, build_dir, package, module):
  264. outfile_path = [build_dir] + list(package) + [module + ".py"]
  265. return os.path.join(*outfile_path)
  266. def get_outputs(self, include_bytecode=1):
  267. modules = self.find_all_modules()
  268. outputs = []
  269. for package, module, module_file in modules:
  270. package = package.split('.')
  271. filename = self.get_module_outfile(self.build_lib, package, module)
  272. outputs.append(filename)
  273. if include_bytecode:
  274. if self.compile:
  275. outputs.append(
  276. importlib.util.cache_from_source(filename, optimization='')
  277. )
  278. if self.optimize > 0:
  279. outputs.append(
  280. importlib.util.cache_from_source(
  281. filename, optimization=self.optimize
  282. )
  283. )
  284. outputs += [
  285. os.path.join(build_dir, filename)
  286. for package, src_dir, build_dir, filenames in self.data_files
  287. for filename in filenames
  288. ]
  289. return outputs
  290. def build_module(self, module, module_file, package):
  291. if isinstance(package, str):
  292. package = package.split('.')
  293. elif not isinstance(package, (list, tuple)):
  294. raise TypeError(
  295. "'package' must be a string (dot-separated), list, or tuple"
  296. )
  297. # Now put the module source file into the "build" area -- this is
  298. # easy, we just copy it somewhere under self.build_lib (the build
  299. # directory for Python source).
  300. outfile = self.get_module_outfile(self.build_lib, package, module)
  301. dir = os.path.dirname(outfile)
  302. self.mkpath(dir)
  303. return self.copy_file(module_file, outfile, preserve_mode=0)
  304. def build_modules(self):
  305. modules = self.find_modules()
  306. for package, module, module_file in modules:
  307. # Now "build" the module -- ie. copy the source file to
  308. # self.build_lib (the build directory for Python source).
  309. # (Actually, it gets copied to the directory for this package
  310. # under self.build_lib.)
  311. self.build_module(module, module_file, package)
  312. def build_packages(self):
  313. for package in self.packages:
  314. # Get list of (package, module, module_file) tuples based on
  315. # scanning the package directory. 'package' is only included
  316. # in the tuple so that 'find_modules()' and
  317. # 'find_package_tuples()' have a consistent interface; it's
  318. # ignored here (apart from a sanity check). Also, 'module' is
  319. # the *unqualified* module name (ie. no dots, no package -- we
  320. # already know its package!), and 'module_file' is the path to
  321. # the .py file, relative to the current directory
  322. # (ie. including 'package_dir').
  323. package_dir = self.get_package_dir(package)
  324. modules = self.find_package_modules(package, package_dir)
  325. # Now loop over the modules we found, "building" each one (just
  326. # copy it to self.build_lib).
  327. for package_, module, module_file in modules:
  328. assert package == package_
  329. self.build_module(module, module_file, package)
  330. def byte_compile(self, files):
  331. if sys.dont_write_bytecode:
  332. self.warn('byte-compiling is disabled, skipping.')
  333. return
  334. from ..util import byte_compile
  335. prefix = self.build_lib
  336. if prefix[-1] != os.sep:
  337. prefix = prefix + os.sep
  338. # XXX this code is essentially the same as the 'byte_compile()
  339. # method of the "install_lib" command, except for the determination
  340. # of the 'prefix' string. Hmmm.
  341. if self.compile:
  342. byte_compile(
  343. files, optimize=0, force=self.force, prefix=prefix, dry_run=self.dry_run
  344. )
  345. if self.optimize > 0:
  346. byte_compile(
  347. files,
  348. optimize=self.optimize,
  349. force=self.force,
  350. prefix=prefix,
  351. dry_run=self.dry_run,
  352. )