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 |