| OLD | NEW |
| (Empty) |
| 1 # -*- test-case-name: twisted.python.test.test_release -*- | |
| 2 # Copyright (c) 2007-2008 Twisted Matrix Laboratories. | |
| 3 # See LICENSE for details. | |
| 4 | |
| 5 """ | |
| 6 Twisted's automated release system. | |
| 7 | |
| 8 This module is only for use within Twisted's release system. If you are anyone | |
| 9 else, do not use it. The interface and behaviour will change without notice. | |
| 10 """ | |
| 11 | |
| 12 from datetime import date | |
| 13 import os | |
| 14 from tempfile import mkdtemp | |
| 15 import tarfile | |
| 16 | |
| 17 # Popen4 isn't available on Windows. BookBuilder won't work on Windows, but | |
| 18 # we don't care. -exarkun | |
| 19 try: | |
| 20 from popen2 import Popen4 | |
| 21 except ImportError: | |
| 22 Popen4 = None | |
| 23 | |
| 24 from twisted.python.versions import Version | |
| 25 from twisted.python.filepath import FilePath | |
| 26 | |
| 27 # This import is an example of why you shouldn't use this module unless you're | |
| 28 # radix | |
| 29 try: | |
| 30 from twisted.lore.scripts import lore | |
| 31 except ImportError: | |
| 32 pass | |
| 33 | |
| 34 # The offset between a year and the corresponding major version number. | |
| 35 VERSION_OFFSET = 2000 | |
| 36 | |
| 37 | |
| 38 class CommandFailed(Exception): | |
| 39 """ | |
| 40 Raised when a child process exits unsuccessfully. | |
| 41 | |
| 42 @type exitCode: C{int} | |
| 43 @ivar exitCode: The exit code for the child process. | |
| 44 | |
| 45 @type output: C{str} | |
| 46 @ivar output: The bytes read from stdout and stderr of the child process. | |
| 47 """ | |
| 48 def __init__(self, exitCode, output): | |
| 49 Exception.__init__(self, exitCode, output) | |
| 50 self.exitCode = exitCode | |
| 51 self.output = output | |
| 52 | |
| 53 | |
| 54 | |
| 55 def _changeVersionInFile(old, new, filename): | |
| 56 """ | |
| 57 Replace the C{old} version number with the C{new} one in the given | |
| 58 C{filename}. | |
| 59 """ | |
| 60 replaceInFile(filename, {old.base(): new.base()}) | |
| 61 | |
| 62 | |
| 63 | |
| 64 def getNextVersion(version, now=None): | |
| 65 """ | |
| 66 Calculate the version number for a new release of Twisted based on | |
| 67 the previous version number. | |
| 68 | |
| 69 @param version: The previous version number. | |
| 70 @param now: (optional) The current date. | |
| 71 """ | |
| 72 # XXX: This has no way of incrementing the patch number. Currently, we | |
| 73 # don't need it. See bug 2915. Jonathan Lange, 2007-11-20. | |
| 74 if now is None: | |
| 75 now = date.today() | |
| 76 major = now.year - VERSION_OFFSET | |
| 77 if major != version.major: | |
| 78 minor = 0 | |
| 79 else: | |
| 80 minor = version.minor + 1 | |
| 81 return Version(version.package, major, minor, 0) | |
| 82 | |
| 83 | |
| 84 | |
| 85 class Project(object): | |
| 86 """ | |
| 87 A representation of a project that has a version. | |
| 88 | |
| 89 @ivar directory: A L{twisted.python.filepath.FilePath} pointing to the base | |
| 90 directory of a Twisted-style Python package. The package should contain | |
| 91 a C{_version.py} file and a C{topfiles} directory that contains a | |
| 92 C{README} file. | |
| 93 """ | |
| 94 | |
| 95 def __init__(self, directory): | |
| 96 self.directory = directory | |
| 97 | |
| 98 | |
| 99 def __repr__(self): | |
| 100 return '%s(%r)' % ( | |
| 101 self.__class__.__name__, self.directory) | |
| 102 | |
| 103 | |
| 104 def getVersion(self): | |
| 105 """ | |
| 106 @return: A L{Version} specifying the version number of the project | |
| 107 based on live python modules. | |
| 108 """ | |
| 109 namespace = {} | |
| 110 execfile(self.directory.child("_version.py").path, namespace) | |
| 111 return namespace["version"] | |
| 112 | |
| 113 | |
| 114 def updateVersion(self, version): | |
| 115 """ | |
| 116 Replace the existing version numbers in _version.py and README files | |
| 117 with the specified version. | |
| 118 """ | |
| 119 oldVersion = self.getVersion() | |
| 120 replaceProjectVersion(oldVersion.package, | |
| 121 self.directory.child("_version.py").path, | |
| 122 version) | |
| 123 _changeVersionInFile( | |
| 124 oldVersion, version, | |
| 125 self.directory.child("topfiles").child("README").path) | |
| 126 | |
| 127 | |
| 128 | |
| 129 def findTwistedProjects(baseDirectory): | |
| 130 """ | |
| 131 Find all Twisted-style projects beneath a base directory. | |
| 132 | |
| 133 @param baseDirectory: A L{twisted.python.filepath.FilePath} to look inside. | |
| 134 @return: A list of L{Project}. | |
| 135 """ | |
| 136 projects = [] | |
| 137 for filePath in baseDirectory.walk(): | |
| 138 if filePath.basename() == 'topfiles': | |
| 139 projectDirectory = filePath.parent() | |
| 140 projects.append(Project(projectDirectory)) | |
| 141 return projects | |
| 142 | |
| 143 | |
| 144 | |
| 145 def updateTwistedVersionInformation(baseDirectory, now): | |
| 146 """ | |
| 147 Update the version information for Twisted and all subprojects to the | |
| 148 date-based version number. | |
| 149 | |
| 150 @param baseDirectory: Where to look for Twisted. If None, the function | |
| 151 infers the information from C{twisted.__file__}. | |
| 152 @param now: The current date (as L{datetime.date}). If None, it defaults | |
| 153 to today. | |
| 154 """ | |
| 155 for project in findTwistedProjects(baseDirectory): | |
| 156 project.updateVersion(getNextVersion(project.getVersion(), now=now)) | |
| 157 | |
| 158 | |
| 159 | |
| 160 def replaceProjectVersion(name, filename, newversion): | |
| 161 """ | |
| 162 Write version specification code into the given filename, which | |
| 163 sets the version to the given version number. | |
| 164 | |
| 165 @param filename: A filename which is most likely a "_version.py" | |
| 166 under some Twisted project. | |
| 167 @param newversion: A version object. | |
| 168 """ | |
| 169 # XXX - this should be moved to Project and renamed to writeVersionFile. | |
| 170 # jml, 2007-11-15. | |
| 171 f = open(filename, 'w') | |
| 172 if newversion.prerelease is not None: | |
| 173 prerelease = ", prerelease=%r" % (newversion.prerelease,) | |
| 174 else: | |
| 175 prerelease = "" | |
| 176 f.write('''\ | |
| 177 # This is an auto-generated file. Do not edit it. | |
| 178 from twisted.python import versions | |
| 179 version = versions.Version(%r, %s, %s, %s%s) | |
| 180 ''' % (name, newversion.major, newversion.minor, newversion.micro, prerelease)) | |
| 181 f.close() | |
| 182 | |
| 183 | |
| 184 | |
| 185 def replaceInFile(filename, oldToNew): | |
| 186 """ | |
| 187 I replace the text `oldstr' with `newstr' in `filename' using science. | |
| 188 """ | |
| 189 os.rename(filename, filename+'.bak') | |
| 190 f = open(filename+'.bak') | |
| 191 d = f.read() | |
| 192 f.close() | |
| 193 for k,v in oldToNew.items(): | |
| 194 d = d.replace(k, v) | |
| 195 f = open(filename + '.new', 'w') | |
| 196 f.write(d) | |
| 197 f.close() | |
| 198 os.rename(filename+'.new', filename) | |
| 199 os.unlink(filename+'.bak') | |
| 200 | |
| 201 | |
| 202 | |
| 203 class NoDocumentsFound(Exception): | |
| 204 """ | |
| 205 Raised when no input documents are found. | |
| 206 """ | |
| 207 | |
| 208 | |
| 209 | |
| 210 class LoreBuilderMixin(object): | |
| 211 """ | |
| 212 Base class for builders which invoke lore. | |
| 213 """ | |
| 214 def lore(self, arguments): | |
| 215 """ | |
| 216 Run lore with the given arguments. | |
| 217 | |
| 218 @param arguments: A C{list} of C{str} giving command line arguments to | |
| 219 lore which should be used. | |
| 220 """ | |
| 221 options = lore.Options() | |
| 222 options.parseOptions(["--null"] + arguments) | |
| 223 lore.runGivenOptions(options) | |
| 224 | |
| 225 | |
| 226 | |
| 227 class DocBuilder(LoreBuilderMixin): | |
| 228 """ | |
| 229 Generate HTML documentation for projects. | |
| 230 """ | |
| 231 | |
| 232 def build(self, version, resourceDir, docDir, template, apiBaseURL=None, del
eteInput=False): | |
| 233 """ | |
| 234 Build the documentation in C{docDir} with Lore. | |
| 235 | |
| 236 Input files ending in .xhtml will be considered. Output will written as | |
| 237 .html files. | |
| 238 | |
| 239 @param version: the version of the documentation to pass to lore. | |
| 240 @type version: C{str} | |
| 241 | |
| 242 @param resourceDir: The directory which contains the toplevel index and | |
| 243 stylesheet file for this section of documentation. | |
| 244 @type resourceDir: L{twisted.python.filepath.FilePath} | |
| 245 | |
| 246 @param docDir: The directory of the documentation. | |
| 247 @type docDir: L{twisted.python.filepath.FilePath} | |
| 248 | |
| 249 @param template: The template used to generate the documentation. | |
| 250 @type template: L{twisted.python.filepath.FilePath} | |
| 251 | |
| 252 @type apiBaseURL: C{str} or C{NoneType} | |
| 253 @param apiBaseURL: A format string which will be interpolated with the | |
| 254 fully-qualified Python name for each API link. For example, to | |
| 255 generate the Twisted 8.0.0 documentation, pass | |
| 256 C{"http://twistedmatrix.com/documents/8.0.0/api/%s.html"}. | |
| 257 | |
| 258 @param deleteInput: If True, the input documents will be deleted after | |
| 259 their output is generated. | |
| 260 @type deleteInput: C{bool} | |
| 261 | |
| 262 @raise NoDocumentsFound: When there are no .xhtml files in the given | |
| 263 C{docDir}. | |
| 264 """ | |
| 265 linkrel = self.getLinkrel(resourceDir, docDir) | |
| 266 inputFiles = docDir.globChildren("*.xhtml") | |
| 267 filenames = [x.path for x in inputFiles] | |
| 268 if not filenames: | |
| 269 raise NoDocumentsFound("No input documents found in %s" % (docDir,)) | |
| 270 if apiBaseURL is not None: | |
| 271 arguments = ["--config", "baseurl=" + apiBaseURL] | |
| 272 else: | |
| 273 arguments = [] | |
| 274 arguments.extend(["--config", "template=%s" % (template.path,), | |
| 275 "--config", "ext=.html", | |
| 276 "--config", "version=%s" % (version,), | |
| 277 "--linkrel", linkrel] + filenames) | |
| 278 self.lore(arguments) | |
| 279 if deleteInput: | |
| 280 for inputFile in inputFiles: | |
| 281 inputFile.remove() | |
| 282 | |
| 283 | |
| 284 def getLinkrel(self, resourceDir, docDir): | |
| 285 """ | |
| 286 Calculate a value appropriate for Lore's --linkrel option. | |
| 287 | |
| 288 Lore's --linkrel option defines how to 'find' documents that are | |
| 289 linked to from TEMPLATE files (NOT document bodies). That is, it's a | |
| 290 prefix for links ('a' and 'link') in the template. | |
| 291 | |
| 292 @param resourceDir: The directory which contains the toplevel index and | |
| 293 stylesheet file for this section of documentation. | |
| 294 @type resourceDir: L{twisted.python.filepath.FilePath} | |
| 295 | |
| 296 @param docDir: The directory containing documents that must link to | |
| 297 C{resourceDir}. | |
| 298 @type docDir: L{twisted.python.filepath.FilePath} | |
| 299 """ | |
| 300 if resourceDir != docDir: | |
| 301 return '/'.join(filePathDelta(docDir, resourceDir)) + "/" | |
| 302 else: | |
| 303 return "" | |
| 304 | |
| 305 | |
| 306 | |
| 307 class ManBuilder(LoreBuilderMixin): | |
| 308 """ | |
| 309 Generate man pages of the different existing scripts. | |
| 310 """ | |
| 311 | |
| 312 def build(self, manDir): | |
| 313 """ | |
| 314 Generate Lore input files from the man pages in C{manDir}. | |
| 315 | |
| 316 Input files ending in .1 will be considered. Output will written as | |
| 317 -man.xhtml files. | |
| 318 | |
| 319 @param manDir: The directory of the man pages. | |
| 320 @type manDir: L{twisted.python.filepath.FilePath} | |
| 321 | |
| 322 @raise NoDocumentsFound: When there are no .1 files in the given | |
| 323 C{manDir}. | |
| 324 """ | |
| 325 inputFiles = manDir.globChildren("*.1") | |
| 326 filenames = [x.path for x in inputFiles] | |
| 327 if not filenames: | |
| 328 raise NoDocumentsFound("No manual pages found in %s" % (manDir,)) | |
| 329 arguments = ["--input", "man", | |
| 330 "--output", "lore", | |
| 331 "--config", "ext=-man.xhtml"] + filenames | |
| 332 self.lore(arguments) | |
| 333 | |
| 334 | |
| 335 | |
| 336 class APIBuilder(object): | |
| 337 """ | |
| 338 Generate API documentation from source files using | |
| 339 U{pydoctor<http://codespeak.net/~mwh/pydoctor/>}. This requires | |
| 340 pydoctor to be installed and usable (which means you won't be able to | |
| 341 use it with Python 2.3). | |
| 342 """ | |
| 343 def build(self, projectName, projectURL, sourceURL, packagePath, outputPath)
: | |
| 344 """ | |
| 345 Call pydoctor's entry point with options which will generate HTML | |
| 346 documentation for the specified package's API. | |
| 347 | |
| 348 @type projectName: C{str} | |
| 349 @param projectName: The name of the package for which to generate | |
| 350 documentation. | |
| 351 | |
| 352 @type projectURL: C{str} | |
| 353 @param projectURL: The location (probably an HTTP URL) of the project | |
| 354 on the web. | |
| 355 | |
| 356 @type sourceURL: C{str} | |
| 357 @param sourceURL: The location (probably an HTTP URL) of the root of | |
| 358 the source browser for the project. | |
| 359 | |
| 360 @type packagePath: L{FilePath} | |
| 361 @param packagePath: The path to the top-level of the package named by | |
| 362 C{projectName}. | |
| 363 | |
| 364 @type outputPath: L{FilePath} | |
| 365 @param outputPath: An existing directory to which the generated API | |
| 366 documentation will be written. | |
| 367 """ | |
| 368 from pydoctor.driver import main | |
| 369 main( | |
| 370 ["--project-name", projectName, | |
| 371 "--project-url", projectURL, | |
| 372 "--system-class", "pydoctor.twistedmodel.TwistedSystem", | |
| 373 "--project-base-dir", packagePath.parent().path, | |
| 374 "--html-viewsource-base", sourceURL, | |
| 375 "--add-package", packagePath.path, | |
| 376 "--html-output", outputPath.path, | |
| 377 "--quiet", "--make-html"]) | |
| 378 | |
| 379 | |
| 380 | |
| 381 class BookBuilder(LoreBuilderMixin): | |
| 382 """ | |
| 383 Generate the LaTeX and PDF documentation. | |
| 384 | |
| 385 The book is built by assembling a number of LaTeX documents. Only the | |
| 386 overall document which describes how to assemble the documents is stored | |
| 387 in LaTeX in the source. The rest of the documentation is generated from | |
| 388 Lore input files. These are primarily XHTML files (of the particular | |
| 389 Lore subset), but man pages are stored in GROFF format. BookBuilder | |
| 390 expects all of its input to be Lore XHTML format, so L{ManBuilder} | |
| 391 should be invoked first if the man pages are to be included in the | |
| 392 result (this is determined by the book LaTeX definition file). | |
| 393 Therefore, a sample usage of BookBuilder may look something like this: | |
| 394 | |
| 395 man = ManBuilder() | |
| 396 man.build(FilePath("doc/core/man")) | |
| 397 book = BookBuilder() | |
| 398 book.build( | |
| 399 FilePath('doc/core/howto'), | |
| 400 [FilePath('doc/core/howto'), FilePath('doc/core/howto/tutorial'), | |
| 401 FilePath('doc/core/man'), FilePath('doc/core/specifications')], | |
| 402 FilePath('doc/core/howto/book.tex'), FilePath('/tmp/book.pdf')) | |
| 403 """ | |
| 404 def run(self, command): | |
| 405 """ | |
| 406 Execute a command in a child process and return the output. | |
| 407 | |
| 408 @type command C{str} | |
| 409 @param command: The shell command to run. | |
| 410 | |
| 411 @raise L{RuntimeError}: If the child process exits with an error. | |
| 412 """ | |
| 413 process = Popen4(command) | |
| 414 stdout = process.fromchild.read() | |
| 415 exitCode = process.wait() | |
| 416 if os.WIFSIGNALED(exitCode) or os.WEXITSTATUS(exitCode): | |
| 417 raise CommandFailed(exitCode, stdout) | |
| 418 return stdout | |
| 419 | |
| 420 | |
| 421 def buildTeX(self, howtoDir): | |
| 422 """ | |
| 423 Build LaTeX files for lore input files in the given directory. | |
| 424 | |
| 425 Input files ending in .xhtml will be considered. Output will written as | |
| 426 .tex files. | |
| 427 | |
| 428 @type howtoDir: L{FilePath} | |
| 429 @param howtoDir: A directory containing lore input files. | |
| 430 | |
| 431 @raise ValueError: If C{howtoDir} does not exist. | |
| 432 """ | |
| 433 if not howtoDir.exists(): | |
| 434 raise ValueError("%r does not exist." % (howtoDir.path,)) | |
| 435 self.lore( | |
| 436 ["--output", "latex", | |
| 437 "--config", "section"] + | |
| 438 [child.path for child in howtoDir.globChildren("*.xhtml")]) | |
| 439 | |
| 440 | |
| 441 def buildPDF(self, bookPath, inputDirectory, outputPath): | |
| 442 """ | |
| 443 Build a PDF from the given a LaTeX book document. | |
| 444 | |
| 445 @type bookPath: L{FilePath} | |
| 446 @param bookPath: The location of a LaTeX document defining a book. | |
| 447 | |
| 448 @type inputDirectory: L{FilePath} | |
| 449 @param inputDirectory: The directory which the inputs of the book are | |
| 450 relative to. | |
| 451 | |
| 452 @type outputPath: L{FilePath} | |
| 453 @param outputPath: The location to which to write the resulting book. | |
| 454 """ | |
| 455 if not bookPath.basename().endswith(".tex"): | |
| 456 raise ValueError("Book filename must end with .tex") | |
| 457 | |
| 458 workPath = FilePath(mkdtemp()) | |
| 459 try: | |
| 460 startDir = os.getcwd() | |
| 461 try: | |
| 462 os.chdir(inputDirectory.path) | |
| 463 | |
| 464 texToDVI = ( | |
| 465 "latex -interaction=nonstopmode " | |
| 466 "-output-directory=%s %s") % ( | |
| 467 workPath.path, bookPath.path) | |
| 468 | |
| 469 # What I tell you three times is true! | |
| 470 # The first two invocations of latex on the book file allows it | |
| 471 # correctly create page numbers for in-text references. Why thi
s is | |
| 472 # the case, I could not tell you. -exarkun | |
| 473 for i in range(3): | |
| 474 self.run(texToDVI) | |
| 475 | |
| 476 bookBaseWithoutExtension = bookPath.basename()[:-4] | |
| 477 dviPath = workPath.child(bookBaseWithoutExtension + ".dvi") | |
| 478 psPath = workPath.child(bookBaseWithoutExtension + ".ps") | |
| 479 pdfPath = workPath.child(bookBaseWithoutExtension + ".pdf") | |
| 480 self.run( | |
| 481 "dvips -o %(postscript)s -t letter -Ppdf %(dvi)s" % { | |
| 482 'postscript': psPath.path, | |
| 483 'dvi': dviPath.path}) | |
| 484 self.run("ps2pdf13 %(postscript)s %(pdf)s" % { | |
| 485 'postscript': psPath.path, | |
| 486 'pdf': pdfPath.path}) | |
| 487 pdfPath.moveTo(outputPath) | |
| 488 workPath.remove() | |
| 489 finally: | |
| 490 os.chdir(startDir) | |
| 491 except: | |
| 492 workPath.moveTo(bookPath.parent().child(workPath.basename())) | |
| 493 raise | |
| 494 | |
| 495 | |
| 496 def build(self, baseDirectory, inputDirectories, bookPath, outputPath): | |
| 497 """ | |
| 498 Build a PDF book from the given TeX book definition and directories | |
| 499 containing lore inputs. | |
| 500 | |
| 501 @type baseDirectory: L{FilePath} | |
| 502 @param baseDirectory: The directory which the inputs of the book are | |
| 503 relative to. | |
| 504 | |
| 505 @type inputDirectories: C{list} of L{FilePath} | |
| 506 @param inputDirectories: The paths which contain lore inputs to be | |
| 507 converted to LaTeX. | |
| 508 | |
| 509 @type bookPath: L{FilePath} | |
| 510 @param bookPath: The location of a LaTeX document defining a book. | |
| 511 | |
| 512 @type outputPath: L{FilePath} | |
| 513 @param outputPath: The location to which to write the resulting book. | |
| 514 """ | |
| 515 for inputDir in inputDirectories: | |
| 516 self.buildTeX(inputDir) | |
| 517 self.buildPDF(bookPath, baseDirectory, outputPath) | |
| 518 for inputDirectory in inputDirectories: | |
| 519 for child in inputDirectory.children(): | |
| 520 if child.splitext()[1] == ".tex" and child != bookPath: | |
| 521 child.remove() | |
| 522 | |
| 523 | |
| 524 | |
| 525 def filePathDelta(origin, destination): | |
| 526 """ | |
| 527 Return a list of strings that represent C{destination} as a path relative | |
| 528 to C{origin}. | |
| 529 | |
| 530 It is assumed that both paths represent directories, not files. That is to | |
| 531 say, the delta of L{twisted.python.filepath.FilePath} /foo/bar to | |
| 532 L{twisted.python.filepath.FilePath} /foo/baz will be C{../baz}, | |
| 533 not C{baz}. | |
| 534 | |
| 535 @type origin: L{twisted.python.filepath.FilePath} | |
| 536 @param origin: The origin of the relative path. | |
| 537 | |
| 538 @type destination: L{twisted.python.filepath.FilePath} | |
| 539 @param destination: The destination of the relative path. | |
| 540 """ | |
| 541 commonItems = 0 | |
| 542 path1 = origin.path.split(os.sep) | |
| 543 path2 = destination.path.split(os.sep) | |
| 544 for elem1, elem2 in zip(path1, path2): | |
| 545 if elem1 == elem2: | |
| 546 commonItems += 1 | |
| 547 path = [".."] * (len(path1) - commonItems) | |
| 548 return path + path2[commonItems:] | |
| 549 | |
| 550 | |
| 551 | |
| 552 class DistributionBuilder(object): | |
| 553 """ | |
| 554 A builder of Twisted distributions. | |
| 555 | |
| 556 This knows how to build tarballs for Twisted and all of its subprojects. | |
| 557 """ | |
| 558 | |
| 559 from twisted.python.dist import twisted_subprojects as subprojects | |
| 560 blacklist = ["vfs", "web2"] | |
| 561 | |
| 562 def __init__(self, rootDirectory, outputDirectory, apiBaseURL=None): | |
| 563 """ | |
| 564 Create a distribution builder. | |
| 565 | |
| 566 @param rootDirectory: root of a Twisted export which will populate | |
| 567 subsequent tarballs. | |
| 568 @type rootDirectory: L{FilePath}. | |
| 569 | |
| 570 @param outputDirectory: The directory in which to create the tarballs. | |
| 571 @type outputDirectory: L{FilePath} | |
| 572 | |
| 573 @type apiBaseURL: C{str} or C{NoneType} | |
| 574 @param apiBaseURL: A format string which will be interpolated with the | |
| 575 fully-qualified Python name for each API link. For example, to | |
| 576 generate the Twisted 8.0.0 documentation, pass | |
| 577 C{"http://twistedmatrix.com/documents/8.0.0/api/%s.html"}. | |
| 578 """ | |
| 579 self.rootDirectory = rootDirectory | |
| 580 self.outputDirectory = outputDirectory | |
| 581 self.apiBaseURL = apiBaseURL | |
| 582 self.manBuilder = ManBuilder() | |
| 583 self.docBuilder = DocBuilder() | |
| 584 | |
| 585 | |
| 586 def _buildDocInDir(self, path, version, howtoPath): | |
| 587 """ | |
| 588 Generate documentation in the given path, building man pages first if | |
| 589 necessary and swallowing errors (so that directories without lore | |
| 590 documentation in them are ignored). | |
| 591 | |
| 592 @param path: The path containing documentation to build. | |
| 593 @type path: L{FilePath} | |
| 594 @param version: The version of the project to include in all generated | |
| 595 pages. | |
| 596 @type version: C{str} | |
| 597 @param howtoPath: The "resource path" as L{DocBuilder} describes it. | |
| 598 @type howtoPath: L{FilePath} | |
| 599 """ | |
| 600 templatePath = self.rootDirectory.child("doc").child("core" | |
| 601 ).child("howto").child("template.tpl") | |
| 602 if path.basename() == "man": | |
| 603 self.manBuilder.build(path) | |
| 604 if path.isdir(): | |
| 605 try: | |
| 606 self.docBuilder.build(version, howtoPath, path, | |
| 607 templatePath, self.apiBaseURL, True) | |
| 608 except NoDocumentsFound: | |
| 609 pass | |
| 610 | |
| 611 | |
| 612 def buildTwisted(self, version): | |
| 613 """ | |
| 614 Build the main Twisted distribution in C{Twisted-<version>.tar.bz2}. | |
| 615 | |
| 616 @type version: C{str} | |
| 617 @param version: The version of Twisted to build. | |
| 618 | |
| 619 @return: The tarball file. | |
| 620 @rtype: L{FilePath}. | |
| 621 """ | |
| 622 releaseName = "Twisted-%s" % (version,) | |
| 623 buildPath = lambda *args: '/'.join((releaseName,) + args) | |
| 624 | |
| 625 outputFile = self.outputDirectory.child(releaseName + ".tar.bz2") | |
| 626 tarball = tarfile.TarFile.open(outputFile.path, 'w:bz2') | |
| 627 | |
| 628 docPath = self.rootDirectory.child("doc") | |
| 629 | |
| 630 # Generate docs! | |
| 631 if docPath.isdir(): | |
| 632 for subProjectDir in docPath.children(): | |
| 633 if (subProjectDir.isdir() | |
| 634 and subProjectDir.basename() not in self.blacklist): | |
| 635 for child in subProjectDir.walk(): | |
| 636 self._buildDocInDir(child, version, | |
| 637 subProjectDir.child("howto")) | |
| 638 | |
| 639 # Now, this part is nasty. We need to exclude blacklisted subprojects | |
| 640 # from the main Twisted distribution. This means we need to exclude | |
| 641 # their bin directories, their documentation directories, their | |
| 642 # plugins, and their python packages. Given that there's no "add all | |
| 643 # but exclude these particular paths" functionality in tarfile, we have | |
| 644 # to walk through all these directories and add things that *aren't* | |
| 645 # part of the blacklisted projects. | |
| 646 | |
| 647 for binthing in self.rootDirectory.child("bin").children(): | |
| 648 if binthing.basename() not in self.blacklist: | |
| 649 tarball.add(binthing.path, | |
| 650 buildPath("bin", binthing.basename())) | |
| 651 | |
| 652 bad_plugins = ["twisted_%s.py" % (blacklisted,) | |
| 653 for blacklisted in self.blacklist] | |
| 654 | |
| 655 for submodule in self.rootDirectory.child("twisted").children(): | |
| 656 if submodule.basename() == "plugins": | |
| 657 for plugin in submodule.children(): | |
| 658 if plugin.basename() not in bad_plugins: | |
| 659 tarball.add(plugin.path, buildPath("twisted", "plugins", | |
| 660 plugin.basename())) | |
| 661 elif submodule.basename() not in self.blacklist: | |
| 662 tarball.add(submodule.path, buildPath("twisted", | |
| 663 submodule.basename())) | |
| 664 | |
| 665 for docDir in self.rootDirectory.child("doc").children(): | |
| 666 if docDir.basename() not in self.blacklist: | |
| 667 tarball.add(docDir.path, buildPath("doc", docDir.basename())) | |
| 668 | |
| 669 for toplevel in self.rootDirectory.children(): | |
| 670 if not toplevel.isdir(): | |
| 671 tarball.add(toplevel.path, buildPath(toplevel.basename())) | |
| 672 | |
| 673 tarball.close() | |
| 674 | |
| 675 return outputFile | |
| 676 | |
| 677 | |
| 678 def buildCore(self, version): | |
| 679 """ | |
| 680 Build a core distribution in C{TwistedCore-<version>.tar.bz2}. | |
| 681 | |
| 682 This is very similar to L{buildSubProject}, but core tarballs and the | |
| 683 input are laid out slightly differently. | |
| 684 | |
| 685 - scripts are in the top level of the C{bin} directory. | |
| 686 - code is included directly from the C{twisted} directory, excluding | |
| 687 subprojects. | |
| 688 - all plugins except the subproject plugins are included. | |
| 689 | |
| 690 @type version: C{str} | |
| 691 @param version: The version of Twisted to build. | |
| 692 | |
| 693 @return: The tarball file. | |
| 694 @rtype: L{FilePath}. | |
| 695 """ | |
| 696 releaseName = "TwistedCore-%s" % (version,) | |
| 697 outputFile = self.outputDirectory.child(releaseName + ".tar.bz2") | |
| 698 buildPath = lambda *args: '/'.join((releaseName,) + args) | |
| 699 tarball = self._createBasicSubprojectTarball( | |
| 700 "core", version, outputFile) | |
| 701 | |
| 702 # Include the bin directory for the subproject. | |
| 703 for path in self.rootDirectory.child("bin").children(): | |
| 704 if not path.isdir(): | |
| 705 tarball.add(path.path, buildPath("bin", path.basename())) | |
| 706 | |
| 707 # Include all files within twisted/ that aren't part of a subproject. | |
| 708 for path in self.rootDirectory.child("twisted").children(): | |
| 709 if path.basename() == "plugins": | |
| 710 for plugin in path.children(): | |
| 711 for subproject in self.subprojects: | |
| 712 if plugin.basename() == "twisted_%s.py" % (subproject,): | |
| 713 break | |
| 714 else: | |
| 715 tarball.add(plugin.path, | |
| 716 buildPath("twisted", "plugins", | |
| 717 plugin.basename())) | |
| 718 elif not path.basename() in self.subprojects + ["topfiles"]: | |
| 719 tarball.add(path.path, buildPath("twisted", path.basename())) | |
| 720 | |
| 721 tarball.add(self.rootDirectory.child("twisted").child("topfiles").path, | |
| 722 releaseName) | |
| 723 tarball.close() | |
| 724 | |
| 725 return outputFile | |
| 726 | |
| 727 | |
| 728 def buildSubProject(self, projectName, version): | |
| 729 """ | |
| 730 Build a subproject distribution in | |
| 731 C{Twisted<Projectname>-<version>.tar.bz2}. | |
| 732 | |
| 733 @type projectName: C{str} | |
| 734 @param projectName: The lowercase name of the subproject to build. | |
| 735 @type version: C{str} | |
| 736 @param version: The version of Twisted to build. | |
| 737 | |
| 738 @return: The tarball file. | |
| 739 @rtype: L{FilePath}. | |
| 740 """ | |
| 741 releaseName = "Twisted%s-%s" % (projectName.capitalize(), version) | |
| 742 outputFile = self.outputDirectory.child(releaseName + ".tar.bz2") | |
| 743 buildPath = lambda *args: '/'.join((releaseName,) + args) | |
| 744 subProjectDir = self.rootDirectory.child("twisted").child(projectName) | |
| 745 | |
| 746 tarball = self._createBasicSubprojectTarball(projectName, version, | |
| 747 outputFile) | |
| 748 | |
| 749 tarball.add(subProjectDir.child("topfiles").path, releaseName) | |
| 750 | |
| 751 # Include all files in the subproject package except for topfiles. | |
| 752 for child in subProjectDir.children(): | |
| 753 name = child.basename() | |
| 754 if name != "topfiles": | |
| 755 tarball.add( | |
| 756 child.path, | |
| 757 buildPath("twisted", projectName, name)) | |
| 758 | |
| 759 pluginsDir = self.rootDirectory.child("twisted").child("plugins") | |
| 760 # Include the plugin for the subproject. | |
| 761 pluginFileName = "twisted_%s.py" % (projectName,) | |
| 762 pluginFile = pluginsDir.child(pluginFileName) | |
| 763 if pluginFile.exists(): | |
| 764 tarball.add(pluginFile.path, | |
| 765 buildPath("twisted", "plugins", pluginFileName)) | |
| 766 | |
| 767 # Include the bin directory for the subproject. | |
| 768 binPath = self.rootDirectory.child("bin").child(projectName) | |
| 769 if binPath.isdir(): | |
| 770 tarball.add(binPath.path, buildPath("bin")) | |
| 771 tarball.close() | |
| 772 | |
| 773 return outputFile | |
| 774 | |
| 775 | |
| 776 def _createBasicSubprojectTarball(self, projectName, version, outputFile): | |
| 777 """ | |
| 778 Helper method to create and fill a tarball with things common between | |
| 779 subprojects and core. | |
| 780 | |
| 781 @param projectName: The subproject's name. | |
| 782 @type projectName: C{str} | |
| 783 @param version: The version of the release. | |
| 784 @type version: C{str} | |
| 785 @param outputFile: The location of the tar file to create. | |
| 786 @type outputFile: L{FilePath} | |
| 787 """ | |
| 788 releaseName = "Twisted%s-%s" % (projectName.capitalize(), version) | |
| 789 buildPath = lambda *args: '/'.join((releaseName,) + args) | |
| 790 | |
| 791 tarball = tarfile.TarFile.open(outputFile.path, 'w:bz2') | |
| 792 | |
| 793 tarball.add(self.rootDirectory.child("LICENSE").path, | |
| 794 buildPath("LICENSE")) | |
| 795 | |
| 796 docPath = self.rootDirectory.child("doc").child(projectName) | |
| 797 | |
| 798 if docPath.isdir(): | |
| 799 for child in docPath.walk(): | |
| 800 self._buildDocInDir(child, version, docPath.child("howto")) | |
| 801 tarball.add(docPath.path, buildPath("doc")) | |
| 802 | |
| 803 return tarball | |
| OLD | NEW |