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