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