├── README.md ├── dbuilder.py └── dbuilder.txt /README.md: -------------------------------------------------------------------------------- 1 | dbuilder 2 | ======== 3 | 4 | A django executable builder for windows 5 | 6 | imported from original website: 7 | http://www.methods.co.nz/django/dbuilder.html 8 | -------------------------------------------------------------------------------- /dbuilder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Build a Win32 distribution for a Django project. Optionally include a 4 | stripped down Python runtime distribution and the Django distribution. 5 | 6 | Copyright: Stuart Rackham (c) 2008 7 | License: MIT 8 | Email: srackham@methods.co.nz 9 | 10 | """ 11 | 12 | VERSION = '0.7.0' 13 | 14 | import sys 15 | import os 16 | import os.path 17 | import fnmatch 18 | import shutil 19 | import py_compile 20 | import subprocess 21 | import glob 22 | import tarfile 23 | import re 24 | 25 | ###################################################################### 26 | # Default configuration parameters and match lists. 27 | # To override the parameters or augment the match lists 28 | # create a separate configuration file (see --conf-file option). 29 | # 30 | # Don't change DJANGO_RUNTIME_DIR else your project won't see Django. 31 | ###################################################################### 32 | 33 | # Default file and directory names. 34 | DIST_DIR = 'dist' # Distribution target directory relatve to PROJECT_DIR. 35 | ISS_FILE = 'setup/setup.iss' # Default --iss-file=- option value (relatve to PROJECT_DIR). 36 | TARBALL_FILE = None # Default --tarball=- option value (relatve to PROJECT_DIR). 37 | PYTHON_RUNTIME_DIR = 'python' # Destination relative to DIST_DIR. 38 | DJANGO_RUNTIME_DIR = 'django' # Destination relative to DIST_DIR. 39 | INNO_SETUP_COMPILER = 'c:/Program Files/Inno Setup 5/ISCC.exe' 40 | CONF_FILE = 'dbuilder.conf' # Default optional configuration file name. 41 | # Source directories for Python and Django runtime files. 42 | # If relative path names are used they are relative to PROJECT_DIR. 43 | PYTHON_DIR = 'c:/python25' 44 | DJANGO_DIR = os.path.join(PYTHON_DIR,'Lib/site-packages/django') 45 | 46 | # Project file (relative to PROJECT_DIR). 47 | PROJECT_COPY_FILES = [ 48 | '*', 49 | '!tags', '!*/tags', 50 | '!*.BAK', 51 | '!*.OLD', 52 | '!*.orig', 53 | '!*~', 54 | '!COMMIT', '!TODO', 55 | '!Session.vim', '!*/Session.vim', 56 | '!BAK/*', '!*/BAK/*', 57 | '!OLD/*', '!*/OLD/*', 58 | '!doc/*', 59 | '!setup/*', 60 | '!tarballs/*', 61 | ] 62 | 63 | # Files for Django runtime (relative to DJANGO_DIR). 64 | DJANGO_COPY_FILES = [ 65 | '*', 66 | # Exclude all locales apart from English. 67 | '!conf/locale/*', 68 | 'conf/locale/en/*', 69 | ] 70 | 71 | # Files from the Python Windows install directory required by the Python 72 | # runtime. The files selected will depend on both the Python version and Django 73 | # project requirements. 74 | PYTHON_COPY_FILES = [ 75 | '*', 76 | '!Doc/*', 77 | '!include/*', 78 | '!Lib/bsddb/*', 79 | '!Lib/compiler/*', 80 | '!Lib/ctypes/*', 81 | '!Lib/curses/*', 82 | '!Lib/distutils/*', 83 | '!Lib/hotshot/*', 84 | '!Lib/idlelib/*', 85 | '!Lib/lib-tk/*', 86 | '!Lib/logging/*', 87 | '!Lib/msilib/*', 88 | '!Lib/site-packages/*', 89 | 'Lib/site-packages/markdown.py', 90 | '!Lib/test/*', '!Lib/*/test/*', 91 | '!Lib/xml/*', 92 | '!LICENSE.txt', 93 | '!NEWS.txt', 94 | '!py2exe-wininst.log', 95 | '!pywin32-wininst.log', 96 | '!README.txt', 97 | '!Removepy2exe.exe', 98 | '!Removepywin32.exe', 99 | '!Scripts/*', 100 | '!tcl/*', 101 | '!Tools/*', 102 | ] 103 | 104 | # Files from the Windows system32 directory that are required by the Python 105 | # runtime. These files are Python version dependent. 106 | PYTHON_SYSTEM_FILES = [ 107 | 'c:/windows/system32/python25.dll', 108 | 'c:/windows/system32/msvcr71.dll', 109 | ] 110 | 111 | # Executed before distribution is built. 112 | # NOTE: Executed regardless of whether OPTIONS.dry_run is True or False. 113 | def pre_build(): 114 | pass 115 | 116 | # Executed after distribution has been built but before Inno Setup is run. 117 | # NOTE: Executed regardless of whether OPTIONS.dry_run is True or False. 118 | def post_build(): 119 | pass 120 | 121 | ###################################################################### 122 | # End of configuration parameters. 123 | ###################################################################### 124 | 125 | 126 | OPTIONS = None # Parsed command-line options OptionParser object. 127 | 128 | 129 | ##################### 130 | # Utility functions # 131 | ##################### 132 | 133 | def errmsg(msg): 134 | sys.stderr.write('%s\n' % msg) 135 | 136 | def infomsg(msg): 137 | print msg 138 | 139 | def die(msg): 140 | errmsg('\nERROR: %s' % msg) 141 | errmsg(" view options with '%s --help'" % os.path.basename(__file__)) 142 | sys.exit(1) 143 | 144 | def verbose(msg): 145 | if OPTIONS.verbose or OPTIONS.dry_run: 146 | infomsg(msg) 147 | 148 | def load_conf(conf_file): 149 | """ 150 | Import optional configuration file which is used to override global 151 | configuration settings. 152 | """ 153 | execfile(conf_file, globals()) 154 | 155 | def matches(path, match_list, context_dir): 156 | """ 157 | Return true if the path matches any of the wildcards in 'match_list'. 158 | Relative 'match_list' wildcards are relative to the 'context_dir'. 159 | 160 | A match list is an ordered set of wildcards (optionally prefixed with a ! 161 | character) and are used to filter a set of path names. 162 | 163 | A path is matched by matching in order against all wildcards in the list, 164 | if a path matches a wilcard it is considered matched unless it is 165 | subsequently unmatched by an exclusion (! prefixed) wildcard. 166 | 167 | Wildcards conform to the fnmatch module's notion of wildcards. 168 | 169 | """ 170 | result = False 171 | for m in match_list: 172 | if m.startswith('!'): 173 | is_match = False 174 | pattern = m[1:] 175 | else: 176 | is_match = True 177 | pattern = m 178 | pattern = os.path.join(context_dir, pattern) 179 | if fnmatch.fnmatch(path, pattern): 180 | result = is_match 181 | return result 182 | 183 | def dst_path(path, src_dir, dst_dir): 184 | """ 185 | Translate source path to destination path. 186 | 'path' must lie within the 'src_dir'. 187 | Return absoute path name. 188 | """ 189 | path = os.path.abspath(path) 190 | src_dir = os.path.abspath(src_dir) 191 | dst_dir = os.path.abspath(dst_dir) 192 | return dst_dir + path[len(src_dir):] 193 | 194 | 195 | ########### 196 | # Helpers # 197 | ########### 198 | 199 | # For use by pre_build and post_build conf file functions. 200 | 201 | def expand_path(filename, default_dir): 202 | """ 203 | Return expanded file path. 204 | If filename is relative it is relative to default_dir. 205 | Expand %(project_dir) and %(dist_dir) in filename. 206 | """ 207 | result = filename % { 208 | 'project_dir': OPTIONS.project_dir, 209 | 'dist_dir': OPTIONS.dist_dir} 210 | if not os.path.isabs(result): 211 | result = os.path.join(default_dir, result) 212 | return os.path.normpath(result) 213 | 214 | def rename_dist_file(src, dst): 215 | """ 216 | Rename file in distribution directory. 217 | If src or dst are relative they are relative to DIST_DIR. 218 | src and dst expand %(project_dir) and %(dist_dir). 219 | Honors --dry-run and --verbose command=line options. 220 | """ 221 | src = expand_path(src, OPTIONS.dist_dir) 222 | dst = expand_path(dst, OPTIONS.dist_dir) 223 | verbose('mv %s %s' % (src, dst)) 224 | if not OPTIONS.dry_run: 225 | os.rename(src, dst) 226 | 227 | def copy_dist_files(src, dst): 228 | """ 229 | Copy src files (wildcards allowed) to dst. 230 | If single src file dst can be an existing directory else file name assumed. 231 | If multiple src files dst is directory path. 232 | Missing directories will be created if they don't exist. 233 | If src is relative it is relative to PROJECT_DIR. 234 | If dst is relative it is relative to DIST_DIR. 235 | src and dst expand %(project_dir) and %(dist_dir). 236 | Honors --dry-run and --verbose command=line options. 237 | Error exit if there are no source files to copy. 238 | """ 239 | src = expand_path(src, OPTIONS.project_dir) 240 | dst = expand_path(dst, OPTIONS.dist_dir) 241 | filenames = glob.glob(src) 242 | count = 0 243 | if len(filenames) > 1 or os.path.isdir(dst): 244 | if not os.path.isdir(dst): 245 | verbose('mkdir -p %s' % dst) 246 | if not OPTIONS.dry_run: 247 | os.makedirs(dst) 248 | for f in filenames: 249 | g = os.path.join(dst, os.path.basename(f)) 250 | verbose('cp %s %s' % (f, g)) 251 | if not OPTIONS.dry_run: 252 | shutil.copyfile(f, g) 253 | count += 1 254 | else: 255 | dst_dir = os.path.dirname(dst) 256 | if not os.path.isdir(dst_dir): 257 | verbose('mkdir -p %s' % dst_dir) 258 | if not OPTIONS.dry_run: 259 | os.makedirs(dst_dir) 260 | verbose('cp %s %s' % (src, dst)) 261 | if not OPTIONS.dry_run: 262 | shutil.copyfile(src, dst) 263 | count += 1 264 | if count == 0: 265 | die('missing source files: %s' % src) 266 | 267 | 268 | #################### 269 | # Application code # 270 | #################### 271 | 272 | def copy_dist(src_dir, 273 | dst_dir, 274 | src_copy_files=[], # Match list of source files to copy. 275 | dst_keep_files=[], # Match list of destination files to keep. 276 | ): 277 | """ 278 | Copy files matching the src_copy_files match list from src_dir to dst_dir 279 | directory. 280 | 281 | Prior to copying clear dst_dir but don't delete paths matching the 282 | dst_keep_files match list. 283 | 284 | Source directory and file names starting with . are implicitly excluded. 285 | Symlinks in source directory (UNIX only) are skipped. 286 | 287 | If the --compile command-line option is set compile Python files and remove 288 | .py files from destination. 289 | 290 | """ 291 | src_dir = os.path.abspath(src_dir) 292 | dst_dir = os.path.abspath(dst_dir) 293 | src_copy_files = src_copy_files + ['!.*', '!*/.*', '!*.pyc', '!*.pyo'] 294 | 295 | # Remove existing destination files (unless they are kept). 296 | # Walk from bottom to ensure directories are empty prior to removal. 297 | infomsg('deleting files from %s' % dst_dir) 298 | for dirpath, dirnames, filenames in os.walk(dst_dir, topdown=False): 299 | for filename in filenames: 300 | filename = os.path.join(dirpath, filename) 301 | if not matches(filename, dst_keep_files, dst_dir): 302 | verbose('rm %s' % filename) 303 | if not OPTIONS.dry_run: 304 | os.remove(filename) 305 | for dirname in dirnames: 306 | dirname = os.path.join(dirpath, dirname) 307 | # Remove empty directories unless explicitly kept. 308 | if len(os.listdir(dirname)) == 0 \ 309 | and not matches(dirname, dst_keep_files, dst_dir): 310 | if os.path.islink(dirname): 311 | verbose('rm symlink %s' % dirname) 312 | if not OPTIONS.dry_run: 313 | os.remove(dirname) 314 | else: 315 | verbose('rmdir %s' % dirname) 316 | if not OPTIONS.dry_run: 317 | os.rmdir(dirname) 318 | # Copy source files to destination. 319 | infomsg('copying files from %s to %s' % (src_dir, dst_dir)) 320 | if not os.path.isdir(dst_dir): 321 | verbose('mkdir %s' % dst_dir) 322 | if not OPTIONS.dry_run: 323 | os.mkdir(dst_dir) 324 | for dirpath, dirnames, filenames in os.walk(src_dir): 325 | for filename in filenames: 326 | filename = os.path.join(dirpath, filename) 327 | if matches(filename, src_copy_files, src_dir): 328 | dst_dirname = dst_path(os.path.dirname(filename), 329 | src_dir, dst_dir) 330 | if not os.path.isdir(dst_dirname): 331 | verbose('mkdir %s' % dst_dirname) 332 | if not OPTIONS.dry_run: 333 | os.makedirs(dst_dirname) 334 | dst_filename = dst_path(filename, src_dir, dst_dir) 335 | verbose('cp %s %s' % (filename, dst_filename)) 336 | if not OPTIONS.dry_run: 337 | shutil.copy(filename, dst_filename) 338 | # Compile destination source files and then delete them. 339 | if OPTIONS.compile: 340 | infomsg('compiling files in %s' % dst_dir) 341 | for dirpath, dirnames, filenames in os.walk(dst_dir): 342 | for filename in filenames: 343 | filename = os.path.join(dirpath, filename) 344 | if not matches(filename, dst_keep_files, dst_dir): 345 | if fnmatch.fnmatch(filename, '*.py'): 346 | verbose('compiling %s' % filename) 347 | if not OPTIONS.dry_run: 348 | py_compile.compile(filename, doraise=True) 349 | verbose('rm %s' % filename) 350 | if not OPTIONS.dry_run: 351 | os.remove(filename) 352 | 353 | class Manifest(object): 354 | """ 355 | Read/write/compare MANIFEST file with distribution files. 356 | """ 357 | 358 | def __init__(self, dist_dir): 359 | self.dist_dir = os.path.abspath(dist_dir) 360 | self.manifest_file = os.path.join(self.dist_dir, 'MANIFEST') 361 | 362 | 363 | def _read_write(self, mode, files=None): 364 | """ 365 | Read (mode = 'r') or write (mode = 'w') files list to/from MANIFEST 366 | file. 367 | """ 368 | f = open(self.manifest_file, mode) 369 | try: 370 | if mode == 'w': 371 | files = ['%s\n' % filename for filename in files] 372 | f.writelines(files) 373 | else: 374 | files = f.readlines() 375 | return [filename.strip() for filename in files] # Strip \n. 376 | finally: 377 | f.close() 378 | 379 | def dist_files(self): 380 | """ 381 | Read relative names of files in distribution directory and return in 382 | list. Path name separators normalized to UNIX. 383 | """ 384 | result = [] 385 | for dirpath, dirnames, filenames in os.walk(self.dist_dir): 386 | for filename in filenames: 387 | filename = os.path.join(dirpath, filename) 388 | filename = filename[len(self.dist_dir)+1:] 389 | if sys.platform == 'win32': 390 | filename = filename.replace(os.sep, '/') 391 | result.append(filename) 392 | return result 393 | 394 | def read(self): 395 | """ 396 | Return list of file names from MANIFEST file. 397 | """ 398 | return self._read_write('r') 399 | 400 | def write(self): 401 | """ 402 | Write MANIFEST file containing relative names of all files in the 403 | distribution directory. 404 | """ 405 | infomsg('writing manifest: %s' % self.manifest_file) 406 | self._read_write('w', self.dist_files()) 407 | 408 | def compare(self): 409 | """ 410 | Compare the files in the MANIFEST with the files in the distribution 411 | directory and print any differences. 412 | Returns False if no MANIFEST or there are differences. 413 | """ 414 | if not os.path.isfile(self.manifest_file): 415 | return False 416 | infomsg('comparing manifest: %s' % self.manifest_file) 417 | dist_files = set(self.dist_files()) 418 | dist_files.discard('MANIFEST') 419 | manifest_files = set(self.read()) 420 | manifest_files.discard('MANIFEST') 421 | result = True 422 | diff = [i for i in manifest_files.difference(dist_files)] 423 | diff.sort() 424 | for filename in diff: 425 | errmsg('-' + filename) # File in manifest but not in distribution. 426 | result = False 427 | diff = [i for i in dist_files.difference(manifest_files)] 428 | diff.sort() 429 | for filename in diff: 430 | errmsg('+' + filename) # File in distribution but not in manifest. 431 | result = False 432 | return result 433 | 434 | def build_project_runtime(): 435 | """ 436 | Copy all project files to distribution directory. 437 | Don't copy distribution directory and don't overwrite Django or Python 438 | runtimes. 439 | """ 440 | copy_dist(OPTIONS.project_dir, OPTIONS.dist_dir, 441 | src_copy_files = PROJECT_COPY_FILES + 442 | ['!' + os.path.abspath(os.path.join(OPTIONS.dist_dir, '*'))], 443 | dst_keep_files = [os.path.join(d, '*') 444 | for d in (PYTHON_RUNTIME_DIR, DJANGO_RUNTIME_DIR)] + 445 | [Manifest(OPTIONS.dist_dir).manifest_file], 446 | ) 447 | 448 | def build_django_runtime(): 449 | copy_dist(DJANGO_DIR, 450 | os.path.join(OPTIONS.dist_dir, DJANGO_RUNTIME_DIR), 451 | src_copy_files = DJANGO_COPY_FILES, 452 | ) 453 | 454 | def build_python_runtime(): 455 | dst_dir = os.path.abspath( 456 | os.path.join(OPTIONS.dist_dir, PYTHON_RUNTIME_DIR)) 457 | copy_dist(PYTHON_DIR, 458 | dst_dir, 459 | src_copy_files = PYTHON_COPY_FILES, 460 | ) 461 | for filename in PYTHON_SYSTEM_FILES: 462 | filename = os.path.abspath(filename) 463 | verbose('cp %s %s' % (filename, dst_dir)) 464 | if not OPTIONS.dry_run: 465 | shutil.copy(filename, dst_dir) 466 | 467 | def exec_inno_setup(iss_file): 468 | infomsg('compiling setup script %s' % iss_file) 469 | if not OPTIONS.dry_run: 470 | args = [INNO_SETUP_COMPILER, '/Q', iss_file] 471 | if OPTIONS.verbose: 472 | del args[1] # Delete quiet option. 473 | subprocess.check_call(args) 474 | 475 | TARBALL_FILE_RE = r'^(.+)\.((tar\.gz)|(tgz)|(tar\.bz2))$' 476 | 477 | def make_tarball(filename): 478 | """ 479 | Make a tarball containing files in the distribution directory. 480 | The stored file root directory is the filename base. 481 | """ 482 | # Strip directory name and tarball extensions from file name. 483 | basename = os.path.basename(filename) 484 | basename = re.match(TARBALL_FILE_RE, basename).group(1) 485 | # Get list of file in distribution directory excluding the manifest file. 486 | dist_files = Manifest(OPTIONS.dist_dir).dist_files() 487 | try: 488 | dist_files.remove('MANIFEST') 489 | except ValueError: 490 | pass 491 | infomsg('creating tarball: %s' % filename) 492 | if not OPTIONS.dry_run: 493 | if filename.endswith('.bz2'): 494 | mode = 'w:bz2' 495 | else: 496 | mode = 'w:gz' 497 | tar = tarfile.open(filename, mode) 498 | for distfile in dist_files: 499 | name = os.path.join(OPTIONS.dist_dir, distfile) 500 | arname = '%s/%s' % (basename, distfile) 501 | verbose('archiving: %s' % arname) 502 | if not OPTIONS.dry_run: 503 | tarinfo = tar.gettarinfo(name, arname) 504 | tarinfo.uid = 0 505 | tarinfo.gid = 0 506 | tarinfo.uname = 'root' 507 | tarinfo.gname = 'root' 508 | tar.addfile(tarinfo, file(name, 'rb')) 509 | if not OPTIONS.dry_run: 510 | tar.close() 511 | 512 | 513 | if __name__ == "__main__": 514 | description = """Build a self contained Win32 distribution for the Django 515 | project in the PROJECT_DIR. Optionally build Python and Django runtimes. 516 | Distribution files are written to DIST_DIR directory (default 'PROJECT_DIR/%s'). 517 | Python runtime written to 'DIST_DIR/%s'. 518 | Django runtime written to 'DIST_DIR/%s'.""" % \ 519 | (DIST_DIR, PYTHON_RUNTIME_DIR, DJANGO_RUNTIME_DIR) 520 | 521 | from optparse import OptionParser 522 | parser = OptionParser(usage='usage: %prog [OPTIONS] PROJECT_DIR', 523 | version='%prog ' + VERSION, 524 | description=description) 525 | parser.add_option('-d', '--dist-dir', 526 | dest='dist_dir', default=None, metavar='DIST_DIR', 527 | help='distribution destination directory') 528 | parser.add_option('-f', '--conf-file', 529 | dest='conf_file', default=None, metavar='CONF_FILE', 530 | help='configuration file') 531 | parser.add_option('-p', '--python-runtime', 532 | action='store_true', dest='python_runtime', default=False, 533 | help='copy a Python runtime from PYTHON_DIR') 534 | parser.add_option('-j', '--django-runtime', 535 | action='store_true', dest='django_runtime', default=False, 536 | help='copy a Django runtime from DJANGO_DIR') 537 | parser.add_option('-c', '--compile', 538 | action='store_true', dest='compile', default=False, 539 | help='distribute compiled .pyc files') 540 | parser.add_option('-i', '--iss-file', 541 | dest='iss_file', default=None, metavar='ISS_FILE', 542 | help='create install wizard using Inno Setup compiler') 543 | parser.add_option('-t', '--tarball', 544 | dest='tarball', default=None, metavar='TARBALL_FILE', 545 | help='create TARBALL_FILE of distribution directory') 546 | parser.add_option('-m', '--manifest', 547 | action='store_true', dest='manifest', default=False, 548 | help='write MANIFEST file and exit') 549 | parser.add_option('-C', '--check-manifest', 550 | action='store_true', dest='check_manifest', default=False, 551 | help='check distribution against MANIFEST file and exit') 552 | parser.add_option('-n', '--dry-run', 553 | action='store_true', dest='dry_run', default=False, 554 | help='show what would have been done') 555 | parser.add_option('-v', '--verbose', 556 | action='store_true', dest='verbose', default=False, 557 | help='increase verbosity') 558 | if len(sys.argv) == 1: 559 | parser.parse_args(['--help']) 560 | OPTIONS, args = parser.parse_args() 561 | # Validate PROJECT_DIR argument. 562 | if len(args) != 1: 563 | die('too few or too many arguments') 564 | project_dir = args[0] 565 | if not os.path.isdir(project_dir): 566 | die('PROJECT_DIR not found: %s' % project_dir) 567 | project_dir = os.path.abspath(project_dir) 568 | OPTIONS.__dict__['project_dir'] = project_dir 569 | # Read configuration file. 570 | if OPTIONS.conf_file is not None: 571 | if not os.path.isfile(OPTIONS.conf_file): 572 | die('configuration file not found: %s' % OPTIONS.conf_file) 573 | load_conf(OPTIONS.conf_file) 574 | else: 575 | # If conf file exists in project directory load it. 576 | conf_file = os.path.join(project_dir, CONF_FILE) 577 | if os.path.isfile(conf_file): 578 | load_conf(conf_file) 579 | # Validate command options. 580 | if OPTIONS.tarball is not None: 581 | tarball = OPTIONS.tarball 582 | if tarball == '-': # Use conf value. 583 | tarball = TARBALL_FILE 584 | if tarball is None: 585 | die('TARBALL_FILE default is None') 586 | if not os.path.isabs(tarball): 587 | tarball = os.path.join(project_dir, tarball) 588 | tarball = os.path.normpath(tarball) 589 | if not os.path.isdir(os.path.dirname(tarball)): 590 | die('missing tarball directory: %s' % os.path.dirname(tarball)) 591 | if not re.match(TARBALL_FILE_RE, tarball): 592 | die('illegal tarball file name extension: %s' % tarball) 593 | OPTIONS.__dict__['tarball'] = tarball 594 | if OPTIONS.iss_file is not None: 595 | if sys.platform != 'win32': 596 | die('Inno setup compiler requires win32 platform') 597 | if not os.path.isfile(INNO_SETUP_COMPILER): 598 | die('Inno Setup compiler not found: %s' % INNO_SETUP_COMPILER) 599 | iss_file = OPTIONS.iss_file 600 | if iss_file == '-': # Use conf value. 601 | iss_file = ISS_FILE 602 | if iss_file is None: 603 | die('ISS_FILE default is None') 604 | if not os.path.isabs(iss_file): 605 | iss_file = os.path.join(project_dir, iss_file) 606 | iss_file = os.path.normpath(iss_file) 607 | if not os.path.isfile(iss_file): 608 | die('Inno Setup script not found: %s' % iss_file) 609 | OPTIONS.__dict__['iss_file'] = iss_file 610 | if OPTIONS.dist_dir is None: 611 | OPTIONS.__dict__['dist_dir'] = os.path.join(project_dir, DIST_DIR) 612 | if OPTIONS.django_runtime: 613 | if not os.path.isabs(DJANGO_DIR): 614 | DJANGO_DIR = os.path.join(project_dir, DJANGO_DIR) 615 | if not os.path.isdir(DJANGO_DIR): 616 | die('DJANGO_DIR not found: %s' % DJANGO_DIR) 617 | if OPTIONS.python_runtime: 618 | if not os.path.isabs(PYTHON_DIR): 619 | PYTHON_DIR = os.path.join(project_dir, PYTHON_DIR) 620 | if not os.path.isdir(PYTHON_DIR): 621 | die('PYTHON_DIR not found: %s' % PYTHON_DIR) 622 | # Do the work. 623 | if OPTIONS.manifest: 624 | Manifest(OPTIONS.dist_dir).write() 625 | sys.exit() 626 | manifest = Manifest(OPTIONS.dist_dir) 627 | if OPTIONS.check_manifest: 628 | if not os.path.isfile(manifest.manifest_file): 629 | die('missing MANIFEST file: %s' % manifest.manifest_file) 630 | if not manifest.compare(): 631 | sys.exit(2) 632 | sys.exit() 633 | infomsg('executing pre_build') 634 | pre_build() 635 | build_project_runtime() 636 | if OPTIONS.django_runtime: 637 | build_django_runtime() 638 | if OPTIONS.python_runtime: 639 | build_python_runtime() 640 | infomsg('executing post_build') 641 | post_build() 642 | if os.path.isfile(manifest.manifest_file): 643 | if OPTIONS.dry_run: 644 | infomsg('dry run: skipping manifest comparision') 645 | elif not manifest.compare(): 646 | die('MANIFEST file differences') 647 | if OPTIONS.iss_file is not None: 648 | exec_inno_setup(OPTIONS.iss_file) 649 | if OPTIONS.tarball is not None: 650 | make_tarball(OPTIONS.tarball) 651 | -------------------------------------------------------------------------------- /dbuilder.txt: -------------------------------------------------------------------------------- 1 | dbuilder.py -- build stand-alone Django projects 2 | ================================================ 3 | Stuart Rackham 4 | v0.7.0, April 2008 5 | 6 | `dbuilder.py` is a Python utility that will take your Django project 7 | and marshal the required files to create a stand-alone distribution. 8 | You have the option of including Python and Django runtimes in the 9 | distribution and you also have the option of packaging the 10 | distribution into either a Windows setup wizard using Inno Setup or a 11 | tarball file. 12 | 13 | dbuilder.py command 14 | ------------------- 15 | Run `python dbuilder.py --help` for help with program options: 16 | 17 | --------------------------------------------------------------------- 18 | Usage: dbuilder.py [OPTIONS] PROJECT_DIR 19 | 20 | Build a self contained Win32 distribution for the Django project in the 21 | PROJECT_DIR. Optionally build Python and Django runtimes. Distribution files 22 | are written to DIST_DIR directory (default 'PROJECT_DIR/dist'). Python runtime 23 | written to 'DIST_DIR/python'. Django runtime written to 'DIST_DIR/django'. 24 | 25 | Options: 26 | --version show program's version number and exit 27 | -h, --help show this help message and exit 28 | -d DIST_DIR, --dist-dir=DIST_DIR 29 | distribution destination directory 30 | -f CONF_FILE, --conf-file=CONF_FILE 31 | configuration file 32 | -p, --python-runtime copy a Python runtime from PYTHON_DIR 33 | -j, --django-runtime copy a Django runtime from DJANGO_DIR 34 | -c, --compile distribute compiled .pyc files 35 | -i ISS_FILE, --iss-file=ISS_FILE 36 | create install wizard using Inno Setup compiler 37 | -t TARBALL_FILE, --tarball=TARBALL_FILE 38 | create TARBALL_FILE of distribution directory 39 | -m, --manifest write MANIFEST file and exit 40 | -C, --check-manifest check distribution against MANIFEST file and exit 41 | -n, --dry-run show what would have been done 42 | -v, --verbose increase verbosity 43 | --------------------------------------------------------------------- 44 | 45 | .Notes 46 | - If a configuration file is not specified with the `--conf-file` 47 | option and if a configuration file named `dbuilder.conf` is in the 48 | `PROJECT_DIR` directory then it is loaded. 49 | - The target areas of the `./dist` distribution directory are cleared 50 | before files are copied. 51 | - Project files are copied to the `./dist` distribution directory. 52 | - If the `--python-runtime` option is specified the Python runtime 53 | files are copied to `./dist/python` using the configuration 54 | `PYTHON_COPY_FILES` <>. 55 | - If the `--django-runtime` option is specified the Django runtime 56 | files are copied to `./dist/django` using the configuration 57 | `DJANGO_COPY_FILES` <>. 58 | - You normally won't need to use the `--django-runtime` or 59 | `--python-runtime` options again unless you update either Python or 60 | Django. 61 | - Directory and file names starting with a period character are not 62 | copied (so your Mercurial or Subversion repo meta data does not get 63 | copied). 64 | - Symlinks (UNIX only) are skipped. 65 | - No `.pyc`, `.pyo` files are copied. 66 | - If the `--iss-file=ISS_FILE` option is specified then the Inno Setup 67 | script `ISS_FILE` is used by the Inno Setup Compiler to create a 68 | installer wizard. 69 | - If the `ISS_FILE` option value is `-` then the `ISS_FILE` 70 | configuration parameter value is used, if it is a relative file name 71 | then it is assumed relative to the `PROJECT_DIR`. 72 | - The default `ISS_FILE` configuration parameter value is 73 | `./setup/setup.iss`. 74 | - If the `--tarball=TARBALL_FILE` option is specified then a tarball 75 | containing the distribution files is created. For example the 76 | tarball filename `myproj_1.0.1.tar.gz` will create a tarball file 77 | named `myproj_1.0.1.tar.gz` and all archive file names will be 78 | prefixed by `myproj_1.0.1/`. 79 | - The `TARBALL_FILE` option value must be prefixed with one of the 80 | following file name extensions: `.tar.gz`, `.tgz`, `.tar.bz2`. If 81 | the file has a `.bz2` extension 'bzip2' compression is used 82 | otherwise 'gzip' compression is used. 83 | - If the `TARBALL_FILE` option value is set to `-` then the default 84 | `TARBALL_FILE` configuration parameter value is used, if it is a 85 | relative file name then it is assumed relative to the `PROJECT_DIR`. 86 | 87 | .Examples 88 | `dbuilder -c projects/myproject`:: 89 | Copy project files in `projects/myproject` to distribution 90 | directory `projects/myproject/dist` then compile `.py` files to 91 | `.pyc` files. 92 | 93 | `dbuilder -cjt tarballs/myproject_1.0.1.tar.gz projects/myproject`:: 94 | Same as previous example plus copy Django runtime to distribution 95 | directory then create a distribution tarball. 96 | 97 | `dbuilder -cpji - projects/myproject`:: 98 | Marshall and compile distribution containing project files (to 99 | `projects/myproject/dist`) along with Python and Django runtimes 100 | (to `projects/myproject/dist/python` and 101 | `projects/myproject/dist/django` respectively) then create an Inno 102 | Setup installer using the `ISS_FILE` configuration parameter. 103 | 104 | 105 | Download 106 | -------- 107 | The current version is 0.7.0 and a distribution Zip file can be 108 | downloaded from http://hg.sharesource.org/dbuilder/[] 109 | 110 | The 'dbuilder' Mercurial repository is hosted by 111 | http://sharesource.org[ShareSource]. ShareSource is a Mercurial 112 | friendly website for hosting Open Source projects. 113 | 114 | To browse the repo go to http://hg.sharesource.org/dbuilder/[]. 115 | 116 | 117 | dbuilder and Linux 118 | ------------------ 119 | Although the examples and rationale in this document mostly relate to 120 | building stand-alone Windows installers 'dbuilder' is not limited to 121 | the Windows platform -- 'dbuilder' will quite happily build a 122 | distribution on Linux with the following caveats: 123 | 124 | - The `--iss-file=ISS_FILE` option is specific to Windows, use the 125 | `--tarball=TARBALL_FILE` option instead. 126 | - Most Linux distributions install Python by default so you probably 127 | won't want to use the `--python-runtime` option, if you do you will 128 | probably only use it to copy non-standard libraries. 129 | 130 | Delivering a tarball distribution to Linux platforms will ensure your 131 | project includes the correct version of Django and that it has all the 132 | necessary libraries, though your project startup code will need to 133 | check that a compatible version of the Python interpreter is 134 | installed. 135 | 136 | 137 | Why 138 | --- 139 | This project started when I needed to distribute a self contained user 140 | installable Windows demo of a Django application. My first thought was 141 | to use 'py2exe' to create a distribution and then package it in a 142 | setup wizard using the 'Inno Setup compiler' (I had used both tools 143 | before and they are excellent). 144 | 145 | I tried using 'py2exe' but this approach did not work because Django 146 | dynamically imports modules using the `\__import__` method and because 147 | of the way Django currently initializes (you will get an `ImportError: 148 | No module named library.zip` error when you run the 'py2exe' generated 149 | executable). These issues have been raised in Django tickets: 150 | 151 | - http://code.djangoproject.com/ticket/6013[Django ticket 6013] 152 | - http://code.djangoproject.com/ticket/5825[Django ticket 5825] 153 | 154 | My solution was to side-step automated package build tools and simply 155 | copy the required Django, Python and project files to a (distribution) 156 | directory which is then packaged using the Inno Setup compiler. The 157 | advantage of this approach is that it is completely transparent -- the 158 | distributed environment is the identical to the development 159 | environment, there's no intervening magic and no hidden surprises. 160 | The drawbacks are: 161 | 162 | - The runtime is larger than the optimized and compressed 'py2exe' 163 | runtime. A few extra megabytes is not really a problem relative to 164 | modern disk capacities, the larger setup wizard size is mitigated by 165 | installer compression. 166 | - You need to manually configure the files to be included in the 167 | distributed application. Using `dbuilder.py` you configure lists of 168 | wildcards to include or exclude files from your distribution. 169 | 170 | 171 | How 172 | --- 173 | First you <> to specify the files you want 174 | in your distribution. 175 | 176 | You next run the `dbuilder.py` command: 177 | 178 | . The optional `pre_build` function is executed (`pre_build` and 179 | `post_build` functions provide hooks for arbitrary Python code to 180 | customise distribution assembly). 181 | . Files specified in the configuration <> are copied 182 | from the project directory to the distribution directory. The 183 | distribution directory is named `dist` and resides in the project 184 | directory, it is automatically created if it does not already exist. 185 | . If the `--compile` command-line option was specified all Python 186 | source files (`.py` files) in the distribution are replaced by 187 | compiled `.pyc` files. 188 | . The optional `post_build` function is executed. 189 | . If a <> file exists all files in the distribution 190 | directory are checked against it. At this point the distribution 191 | assembly is complete and the contents of the distribution directory 192 | exactly mirrors the target install directory. 193 | . If the `--iss-file=ISS_FILE` option is specified the distribution is 194 | packaged by the Inno Setup compiler. 195 | . If the `--tarball=TARBALL_FILE` option is specified as distribution 196 | tarball is created. 197 | 198 | 199 | Prerequisites 200 | ------------- 201 | - Python (I wrote and tested `dbuilder.py` using Python 2.5). 202 | - Inno Setup compiler if the `--iss-file=ISS_FILE` option is used. 203 | 204 | See the <> at the bottom. 205 | 206 | 207 | [[X4]] 208 | Configuring dbuilder.py 209 | ----------------------- 210 | The `dbuilder.py` default configuration is customized by editing a 211 | dbuilder configuration file (default name `dbuilder.conf` in the same 212 | directory as `dbuilder.py` or set with the `--conf-file` command-line 213 | option). 214 | 215 | A dbuilder configuration file is just Python source code that overrides 216 | or augments the configuration settings defined in the `dbuilder.py` 217 | file. 218 | 219 | - Edit the following <> to reflect the files in your 220 | project and you projects Python requirements: 221 | 222 | PROJECT_COPY_FILES 223 | DJANGO_COPY_FILES 224 | PYTHON_COPY_FILES 225 | + 226 | TIP: The default match lists in `dbuilder.py` are probably fairly close 227 | and it's probably easier to simply append to them rather that starting 228 | from scratch). 229 | 230 | - Test your match lists by running `dbuilder.py` with the `--dry-run` 231 | command-line option. 232 | - Check that the source directories for Python and Django runtime 233 | files (`PYTHON_DIR` and `DJANGO_DIR` configuration options) are set 234 | correctly. If relative path names are used they are relative to the 235 | project directory. 236 | - You should also check the `INNO_SETUP_COMPILER` setting is correct 237 | for your Inno Setup install. 238 | - If you have any non-standard things you need to do pre or post build 239 | then implement the `pre_build` and `post_build` functions (see the 240 | stubs in `dbuilder.py`). 241 | 242 | .`dbuilder.conf` file 243 | --------------------------------------------------------------------- 244 | PROJECT_COPY_FILES = PROJECT_COPY_FILES + [ 245 | 'doc/myproj.html', 246 | '!*settings.py', 247 | 'settings.py', 248 | 'win32_settings.py', 249 | '!django/*', 250 | '!bin/*', 251 | 'bin/sqlite3.exe', 252 | 'bin/migration/*', 253 | '!bin/migration/data/*', 254 | '!*.conf', 255 | '!db/*', 256 | '!updates/*', 257 | ] 258 | 259 | PYTHON_COPY_FILES = PYTHON_COPY_FILES + [ 260 | 'Lib/site-packages/pywin32.pth', 261 | 'Lib/site-packages/win32/*', 262 | '!Lib/site-packages/win32/Demos/*', 263 | '!Lib/site-packages/win32/include/*', 264 | '!Lib/site-packages/win32/scripts/*', 265 | '!Lib/site-packages/win32/test/*', 266 | ] 267 | 268 | def post_build(): 269 | # A couple of files need to be renamed in target distribution. 270 | copy_dist_files('db/myproj_demo.db', 'db/myproj_production.db') 271 | copy_dist_files('win32_runner.conf', 'runner.conf') 272 | # Copy dupdater script from dupdater project directory. 273 | copy_dist_files('../../dupdater/trunk/dupdater.pyc', 'bin') 274 | # Because updater.py cannot read .pyc files. 275 | copy_dist_files('updates/*.py', 'updates') 276 | --------------------------------------------------------------------- 277 | 278 | [[X5]] 279 | Match lists 280 | ~~~~~~~~~~~ 281 | - A match list is an ordered list of path name wildcards (optionally prefixed 282 | with a `!` character) and is used to select file for inclusion in 283 | the distribution. 284 | - A path is matched, in order, against all wildcards in the Match 285 | list, if a path matches a wildcard it is considered matched unless 286 | it is subsequently unmatched by an exclusion (`!` prefixed) 287 | wildcard. The final match outcome is determined once all wildcards 288 | have been processed. 289 | - Wildcards conform to the 290 | http://docs.python.org/lib/module-fnmatch.html[fnmatch] module's 291 | notion of wildcards. 292 | - Relative wildcards are relative to the copy source directory. 293 | 294 | Helpers 295 | ~~~~~~~ 296 | The following helper functions in `dbuilder.py` can be used in 297 | configuration file `pre_build` and `post_build` functions: 298 | `expand_path`, `rename_dist_file`, `copy_dist_files`. These functions 299 | are documented in the `dbuilder.py` source code. 300 | 301 | 302 | [[X3]] 303 | Manifest functionality 304 | ---------------------- 305 | The `--manifest` option writes a file named `MANIFEST` in the 306 | distribution directory that contains the relative file names (one per 307 | line) of all other files in the distribution directory. 308 | 309 | Whenever you run `dbuilder.py` to rebuild your project it will compare 310 | the contents of manifest file to the names of the files in the 311 | distribution directory -- any differences are printed to stderr. 312 | Missing files are prefixed with a `-` character, new files are 313 | prefixed with a `+` character. 314 | 315 | Once you've got a working distribution you should create a manifest 316 | file -- from then on you will always be warned of any discrepancies 317 | during subsequent builds. If you have added new file or removed old 318 | one from your distribution refresh the manifest file by rerunning 319 | dbuilder with the `--manifest` option. 320 | 321 | To just check the manifest and exit use the `--check-manifest` 322 | command-line option -- it will return an exit value of 2 if there are 323 | any differences. 324 | 325 | 326 | [[X2]] 327 | Changelog 328 | --------- 329 | 2008-04-08: Version 0.7.0 released:: \ 330 | - 'dbuilder' now hosted at http://sharesource.org[ShareSource]. 331 | - Changed project name from 'builder' to 'dbuilder'. 332 | - Replaced `--inno-setup` option with `--iss-file=ISS_FILE` option. 333 | - Normalized `MANIFEST` file entries path separators to UNIX. 334 | - Build stops if `MANIFEST` file differences are detected. 335 | - Sorted manifest compare output. 336 | - `DIST_DIR` configuration file entry now works. 337 | - Added `copy_dist_files` helper. 338 | - Added `--tarball=TARBALL_FILE` option. 339 | - The default configuration file is loaded from the `PROJECT_DIR` not 340 | the `builder.py` directory. 341 | 342 | 2008-02-28: Version 0.6.2 released:: \ 343 | - Added <>. 344 | 345 | 2008-02-26: Version 0.6.1 released:: \ 346 | - Added `pre_build` and `post_build` configuration functions. 347 | 348 | 2008-02-25: Version 0.6.0 released:: \ 349 | - The `--django-dir=DJANGO_DIR` and `--python-dir=PYTHON_DIR` options 350 | have been replaced by `--django-runtime` and `--python-runtime` 351 | boolean options. The source directories for Python and Django 352 | runtime files are set by the new `PYTHON_DIR` and `DJANGO_DIR` 353 | configuration parameters. If relative path names are used they are 354 | relative to the project directory. 355 | - The optional configuration file name has changed from 356 | `builder_conf.py` to `builder.conf`. 357 | 358 | 2008-02-21: Version 0.5.1 released:: \ 359 | - First public release. 360 | 361 | 362 | [[X1]] 363 | References 364 | ---------- 365 | - http://www.djangoproject.com/[Django web framework] 366 | - http://python.org/[Python] 367 | - http://www.jrsoftware.org/isinfo.php[Inno Setup] 368 | - http://effbot.org/zone/exemaker.htm[exemaker] 369 | - http://www.py2exe.org/[py2exe] 370 | - http://cx-freeze.sourceforge.net/[cx_freeze] 371 | - http://www.instantdjango.com/[Standalone Windows installer] 372 | - http://thinkhole.org/wp/2006/04/03/django-on-windows-howto/[Article 373 | on installing and configuring DAPP (Django, Apache, PostgreSQL and 374 | Python) on Windows] 375 | - http://docs.python.org/lib/module-pycompile.html[Compile Python 376 | source files] 377 | - http://docs.python.org/lib/module-compileall.html[the Python 378 | compileall module] 379 | - http://www.silverstripesoftware.com/blog/archives/51[Deploying a 380 | Django app on the desktop] 381 | --------------------------------------------------------------------------------