root / trunk / twisted / python / _release.py

Revision 26148, 36.0 kB (checked in by radix, 5 months ago)

Merge api-script-3637

Author: radix
Reviewer: glyph
Fixes: #3637

There is now a command line tool for generating the API docs of Twisted.

Line 
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 sys
14 import os
15 from tempfile import mkdtemp
16 import tarfile
17
18 # Popen4 isn't available on Windows.  BookBuilder won't work on Windows, but
19 # we don't care. -exarkun
20 try:
21     from popen2 import Popen4
22 except ImportError:
23     Popen4 = None
24
25 from twisted.python.versions import Version
26 from twisted.python.filepath import FilePath
27 from twisted.python.dist import twisted_subprojects
28
29 # This import is an example of why you shouldn't use this module unless you're
30 # radix
31 try:
32     from twisted.lore.scripts import lore
33 except ImportError:
34     pass
35
36 # The offset between a year and the corresponding major version number.
37 VERSION_OFFSET = 2000
38
39
40 def runCommand(args):
41     """
42     Execute a vector of arguments.
43
44     @type args: C{list} of C{str}
45     @param args: A list of arguments, the first of which will be used as the
46         executable to run.
47
48     @rtype: C{str}
49     @return: All of the standard output.
50
51     @raise CommandFailed: when the program exited with a non-0 exit code.
52     """
53     process = Popen4(args)
54     stdout = process.fromchild.read()
55     exitCode = process.wait()
56     if os.WIFSIGNALED(exitCode) or os.WEXITSTATUS(exitCode):
57         raise CommandFailed(exitCode, stdout)
58     return stdout
59
60
61 class CommandFailed(Exception):
62     """
63     Raised when a child process exits unsuccessfully.
64
65     @type exitCode: C{int}
66     @ivar exitCode: The exit code for the child process.
67
68     @type output: C{str}
69     @ivar output: The bytes read from stdout and stderr of the child process.
70     """
71     def __init__(self, exitCode, output):
72         Exception.__init__(self, exitCode, output)
73         self.exitCode = exitCode
74         self.output = output
75
76
77
78 def _changeVersionInFile(old, new, filename):
79     """
80     Replace the C{old} version number with the C{new} one in the given
81     C{filename}.
82     """
83     replaceInFile(filename, {old.base(): new.base()})
84
85
86
87 def getNextVersion(version, now=None):
88     """
89     Calculate the version number for a new release of Twisted based on
90     the previous version number.
91
92     @param version: The previous version number.
93     @param now: (optional) The current date.
94     """
95     # XXX: This has no way of incrementing the patch number. Currently, we
96     # don't need it. See bug 2915. Jonathan Lange, 2007-11-20.
97     if now is None:
98         now = date.today()
99     major = now.year - VERSION_OFFSET
100     if major != version.major:
101         minor = 0
102     else:
103         minor = version.minor + 1
104     return Version(version.package, major, minor, 0)
105
106
107 def changeAllProjectVersions(root, versionTemplate):
108     """
109     Change the version of all projects (including core and all subprojects).
110
111     @type root: L{FilePath}
112     @param root: The root of the Twisted source tree.
113     @type versionTemplate: L{Version}
114     @param versionTemplate: The version of all projects.  The name will be
115         replaced for each respective project.
116     """
117     for project in findTwistedProjects(root):
118         if project.directory.basename() == "twisted":
119             packageName = "twisted"
120         else:
121             packageName = "twisted." + project.directory.basename()
122         version = Version(packageName, versionTemplate.major,
123                           versionTemplate.minor, versionTemplate.micro,
124                           prerelease=versionTemplate.prerelease)
125
126         # The placement of the top-level README with respect to other files (eg
127         # _version.py) is sufficiently different from the others that we just
128         # have to handle it specially.
129         if packageName == "twisted":
130             _changeVersionInFile(
131                 project.getVersion(), version, root.child('README').path)
132
133         project.updateVersion(version)
134
135
136
137
138 class Project(object):
139     """
140     A representation of a project that has a version.
141
142     @ivar directory: A L{twisted.python.filepath.FilePath} pointing to the base
143         directory of a Twisted-style Python package. The package should contain
144         a C{_version.py} file and a C{topfiles} directory that contains a
145         C{README} file.
146     """
147
148     def __init__(self, directory):
149         self.directory = directory
150
151
152     def __repr__(self):
153         return '%s(%r)' % (
154             self.__class__.__name__, self.directory)
155
156
157     def getVersion(self):
158         """
159         @return: A L{Version} specifying the version number of the project
160         based on live python modules.
161         """
162         namespace = {}
163         execfile(self.directory.child("_version.py").path, namespace)
164         return namespace["version"]
165
166
167     def updateVersion(self, version):
168         """
169         Replace the existing version numbers in _version.py and README files
170         with the specified version.
171         """
172         oldVersion = self.getVersion()
173         replaceProjectVersion(self.directory.child("_version.py").path,
174                               version)
175         _changeVersionInFile(
176             oldVersion, version,
177             self.directory.child("topfiles").child("README").path)
178
179
180
181 def findTwistedProjects(baseDirectory):
182     """
183     Find all Twisted-style projects beneath a base directory.
184
185     @param baseDirectory: A L{twisted.python.filepath.FilePath} to look inside.
186     @return: A list of L{Project}.
187     """
188     projects = []
189     for filePath in baseDirectory.walk():
190         if filePath.basename() == 'topfiles':
191             projectDirectory = filePath.parent()
192             projects.append(Project(projectDirectory))
193     return projects
194
195
196
197 def updateTwistedVersionInformation(baseDirectory, now):
198     """
199     Update the version information for Twisted and all subprojects to the
200     date-based version number.
201
202     @param baseDirectory: Where to look for Twisted. If None, the function
203         infers the information from C{twisted.__file__}.
204     @param now: The current date (as L{datetime.date}). If None, it defaults
205         to today.
206     """
207     for project in findTwistedProjects(baseDirectory):
208         project.updateVersion(getNextVersion(project.getVersion(), now=now))
209
210
211 def generateVersionFileData(version):
212     """
213     Generate the data to be placed into a _version.py file.
214
215     @param version: A version object.
216     """
217     if version.prerelease is not None:
218         prerelease = ", prerelease=%r" % (version.prerelease,)
219     else:
220         prerelease = ""
221     data = '''\
222 # This is an auto-generated file. Do not edit it.
223 from twisted.python import versions
224 version = versions.Version(%r, %s, %s, %s%s)
225 ''' % (version.package, version.major, version.minor, version.micro, prerelease)
226     return data
227
228
229 def replaceProjectVersion(filename, newversion):
230     """
231     Write version specification code into the given filename, which
232     sets the version to the given version number.
233
234     @param filename: A filename which is most likely a "_version.py"
235         under some Twisted project.
236     @param newversion: A version object.
237     """
238     # XXX - this should be moved to Project and renamed to writeVersionFile.
239     # jml, 2007-11-15.
240     f = open(filename, 'w')
241     f.write(generateVersionFileData(newversion))
242     f.close()
243
244
245
246 def replaceInFile(filename, oldToNew):
247     """
248     I replace the text `oldstr' with `newstr' in `filename' using science.
249     """
250     os.rename(filename, filename+'.bak')
251     f = open(filename+'.bak')
252     d = f.read()
253     f.close()
254     for k,v in oldToNew.items():
255         d = d.replace(k, v)
256     f = open(filename + '.new', 'w')
257     f.write(d)
258     f.close()
259     os.rename(filename+'.new', filename)
260     os.unlink(filename+'.bak')
261
262
263
264 class NoDocumentsFound(Exception):
265     """
266     Raised when no input documents are found.
267     """
268
269
270
271 class LoreBuilderMixin(object):
272     """
273     Base class for builders which invoke lore.
274     """
275     def lore(self, arguments):
276         """
277         Run lore with the given arguments.
278
279         @param arguments: A C{list} of C{str} giving command line arguments to
280             lore which should be used.
281         """
282         options = lore.Options()
283         options.parseOptions(["--null"] + arguments)
284         lore.runGivenOptions(options)
285
286
287
288 class DocBuilder(LoreBuilderMixin):
289     """
290     Generate HTML documentation for projects.
291     """
292
293     def build(self, version, resourceDir, docDir, template, apiBaseURL=None,
294               deleteInput=False):
295         """
296         Build the documentation in C{docDir} with Lore.
297
298         Input files ending in .xhtml will be considered. Output will written as
299         .html files.
300
301         @param version: the version of the documentation to pass to lore.
302         @type version: C{str}
303
304         @param resourceDir: The directory which contains the toplevel index and
305             stylesheet file for this section of documentation.
306         @type resourceDir: L{twisted.python.filepath.FilePath}
307
308         @param docDir: The directory of the documentation.
309         @type docDir: L{twisted.python.filepath.FilePath}
310
311         @param template: The template used to generate the documentation.
312         @type template: L{twisted.python.filepath.FilePath}
313
314         @type apiBaseURL: C{str} or C{NoneType}
315         @param apiBaseURL: A format string which will be interpolated with the
316             fully-qualified Python name for each API link.  For example, to
317             generate the Twisted 8.0.0 documentation, pass
318             C{"http://twistedmatrix.com/documents/8.0.0/api/%s.html"}.
319
320         @param deleteInput: If True, the input documents will be deleted after
321             their output is generated.
322         @type deleteInput: C{bool}
323
324         @raise NoDocumentsFound: When there are no .xhtml files in the given
325             C{docDir}.
326         """
327         linkrel = self.getLinkrel(resourceDir, docDir)
328         inputFiles = docDir.globChildren("*.xhtml")
329         filenames = [x.path for x in inputFiles]
330         if not filenames:
331             raise NoDocumentsFound("No input documents found in %s" % (docDir,))
332         if apiBaseURL is not None:
333             arguments = ["--config", "baseurl=" + apiBaseURL]
334         else:
335             arguments = []
336         arguments.extend(["--config", "template=%s" % (template.path,),
337                           "--config", "ext=.html",
338                           "--config", "version=%s" % (version,),
339                           "--linkrel", linkrel] + filenames)
340         self.lore(arguments)
341         if deleteInput:
342             for inputFile in inputFiles:
343                 inputFile.remove()
344
345
346     def getLinkrel(self, resourceDir, docDir):
347         """
348         Calculate a value appropriate for Lore's --linkrel option.
349
350         Lore's --linkrel option defines how to 'find' documents that are
351         linked to from TEMPLATE files (NOT document bodies). That is, it's a
352         prefix for links ('a' and 'link') in the template.
353
354         @param resourceDir: The directory which contains the toplevel index and
355             stylesheet file for this section of documentation.
356         @type resourceDir: L{twisted.python.filepath.FilePath}
357
358         @param docDir: The directory containing documents that must link to
359             C{resourceDir}.
360         @type docDir: L{twisted.python.filepath.FilePath}
361         """
362         if resourceDir != docDir:
363             return '/'.join(filePathDelta(docDir, resourceDir)) + "/"
364         else:
365             return ""
366
367
368
369 class ManBuilder(LoreBuilderMixin):
370     """
371     Generate man pages of the different existing scripts.
372     """
373
374     def build(self, manDir):
375         """
376         Generate Lore input files from the man pages in C{manDir}.
377
378         Input files ending in .1 will be considered. Output will written as
379         -man.xhtml files.
380
381         @param manDir: The directory of the man pages.
382         @type manDir: L{twisted.python.filepath.FilePath}
383
384         @raise NoDocumentsFound: When there are no .1 files in the given
385             C{manDir}.
386         """
387         inputFiles = manDir.globChildren("*.1")
388         filenames = [x.path for x in inputFiles]
389         if not filenames:
390             raise NoDocumentsFound("No manual pages found in %s" % (manDir,))
391         arguments = ["--input", "man",
392                      "--output", "lore",
393                      "--config", "ext=-man.xhtml"] + filenames
394         self.lore(arguments)
395
396
397
398 class APIBuilder(object):
399     """
400     Generate API documentation from source files using
401     U{pydoctor<http://codespeak.net/~mwh/pydoctor/>}.  This requires
402     pydoctor to be installed and usable (which means you won't be able to
403     use it with Python 2.3).
404     """
405     def build(self, projectName, projectURL, sourceURL, packagePath,
406               outputPath):
407         """
408         Call pydoctor's entry point with options which will generate HTML
409         documentation for the specified package's API.
410
411         @type projectName: C{str}
412         @param projectName: The name of the package for which to generate
413             documentation.
414
415         @type projectURL: C{str}
416         @param projectURL: The location (probably an HTTP URL) of the project
417             on the web.
418
419         @type sourceURL: C{str}
420         @param sourceURL: The location (probably an HTTP URL) of the root of
421             the source browser for the project.
422
423         @type packagePath: L{FilePath}
424         @param packagePath: The path to the top-level of the package named by
425             C{projectName}.
426
427         @type outputPath: L{FilePath}
428         @param outputPath: An existing directory to which the generated API
429             documentation will be written.
430         """
431         from pydoctor.driver import main
432         main(
433             ["--project-name", projectName,
434              "--project-url", projectURL,
435              "--system-class", "pydoctor.twistedmodel.TwistedSystem",
436              "--project-base-dir", packagePath.parent().path,
437              "--html-viewsource-base", sourceURL,
438              "--add-package", packagePath.path,
439              "--html-output", outputPath.path,
440              "--html-write-function-pages", "--quiet", "--make-html"])
441
442
443
444 class BookBuilder(LoreBuilderMixin):
445     """
446     Generate the LaTeX and PDF documentation.
447
448     The book is built by assembling a number of LaTeX documents.  Only the
449     overall document which describes how to assemble the documents is stored
450     in LaTeX in the source.  The rest of the documentation is generated from
451     Lore input files.  These are primarily XHTML files (of the particular
452     Lore subset), but man pages are stored in GROFF format.  BookBuilder
453     expects all of its input to be Lore XHTML format, so L{ManBuilder}
454     should be invoked first if the man pages are to be included in the
455     result (this is determined by the book LaTeX definition file).
456     Therefore, a sample usage of BookBuilder may look something like this::
457
458         man = ManBuilder()
459         man.build(FilePath("doc/core/man"))
460         book = BookBuilder()
461         book.build(
462             FilePath('doc/core/howto'),
463             [FilePath('doc/core/howto'), FilePath('doc/core/howto/tutorial'),
464              FilePath('doc/core/man'), FilePath('doc/core/specifications')],
465             FilePath('doc/core/howto/book.tex'), FilePath('/tmp/book.pdf'))
466     """
467     def run(self, command):
468         """
469         Execute a command in a child process and return the output.
470
471         @type command: C{str}
472         @param command: The shell command to run.
473
474         @raise CommandFailed: If the child process exits with an error.
475         """
476         return runCommand(command)
477
478
479     def buildTeX(self, howtoDir):
480         """
481         Build LaTeX files for lore input files in the given directory.
482
483         Input files ending in .xhtml will be considered. Output will written as
484         .tex files.
485
486         @type howtoDir: L{FilePath}
487         @param howtoDir: A directory containing lore input files.
488
489         @raise ValueError: If C{howtoDir} does not exist.
490         """
491         if not howtoDir.exists():
492             raise ValueError("%r does not exist." % (howtoDir.path,))
493         self.lore(
494             ["--output", "latex",
495              "--config", "section"] +
496             [child.path for child in howtoDir.globChildren("*.xhtml")])
497
498
499     def buildPDF(self, bookPath, inputDirectory, outputPath):
500         """
501         Build a PDF from the given a LaTeX book document.
502
503         @type bookPath: L{FilePath}
504         @param bookPath: The location of a LaTeX document defining a book.
505
506         @type inputDirectory: L{FilePath}
507         @param inputDirectory: The directory which the inputs of the book are
508             relative to.
509
510         @type outputPath: L{FilePath}
511         @param outputPath: The location to which to write the resulting book.
512         """
513         if not bookPath.basename().endswith(".tex"):
514             raise ValueError("Book filename must end with .tex")
515
516         workPath = FilePath(mkdtemp())
517         try:
518             startDir = os.getcwd()
519             try:
520                 os.chdir(inputDirectory.path)
521
522                 texToDVI = (
523                     "latex -interaction=nonstopmode "
524                     "-output-directory=%s %s") % (
525                     workPath.path, bookPath.path)
526
527                 # What I tell you three times is true!
528                 # The first two invocations of latex on the book file allows it
529                 # correctly create page numbers for in-text references.  Why this is
530                 # the case, I could not tell you. -exarkun
531                 for i in range(3):
532                     self.run(texToDVI)
533
534                 bookBaseWithoutExtension = bookPath.basename()[:-4]
535                 dviPath = workPath.child(bookBaseWithoutExtension + ".dvi")
536                 psPath = workPath.child(bookBaseWithoutExtension + ".ps")
537                 pdfPath = workPath.child(bookBaseWithoutExtension + ".pdf")
538                 self.run(
539                     "dvips -o %(postscript)s -t letter -Ppdf %(dvi)s" % {
540                         'postscript': psPath.path,
541                         'dvi': dviPath.path})
542                 self.run("ps2pdf13 %(postscript)s %(pdf)s" % {
543                         'postscript': psPath.path,
544                         'pdf': pdfPath.path})
545                 pdfPath.moveTo(outputPath)
546                 workPath.remove()
547             finally:
548                 os.chdir(startDir)
549         except:
550             workPath.moveTo(bookPath.parent().child(workPath.basename()))
551             raise
552
553
554     def build(self, baseDirectory, inputDirectories, bookPath, outputPath):
555         """
556         Build a PDF book from the given TeX book definition and directories
557         containing lore inputs.
558
559         @type baseDirectory: L{FilePath}
560         @param baseDirectory: The directory which the inputs of the book are
561             relative to.
562
563         @type inputDirectories: C{list} of L{FilePath}
564         @param inputDirectories: The paths which contain lore inputs to be
565             converted to LaTeX.
566
567         @type bookPath: L{FilePath}
568         @param bookPath: The location of a LaTeX document defining a book.
569
570         @type outputPath: L{FilePath}
571         @param outputPath: The location to which to write the resulting book.
572         """
573         for inputDir in inputDirectories:
574             self.buildTeX(inputDir)
575         self.buildPDF(bookPath, baseDirectory, outputPath)
576         for inputDirectory in inputDirectories:
577             for child in inputDirectory.children():
578                 if child.splitext()[1] == ".tex" and child != bookPath:
579                     child.remove()
580
581
582
583 def filePathDelta(origin, destination):
584     """
585     Return a list of strings that represent C{destination} as a path relative
586     to C{origin}.
587
588     It is assumed that both paths represent directories, not files. That is to
589     say, the delta of L{twisted.python.filepath.FilePath} /foo/bar to
590     L{twisted.python.filepath.FilePath} /foo/baz will be C{../baz},
591     not C{baz}.
592
593     @type origin: L{twisted.python.filepath.FilePath}
594     @param origin: The origin of the relative path.
595
596     @type destination: L{twisted.python.filepath.FilePath}
597     @param destination: The destination of the relative path.
598     """
599     commonItems = 0
600     path1 = origin.path.split(os.sep)
601     path2 = destination.path.split(os.sep)
602     for elem1, elem2 in zip(path1, path2):
603         if elem1 == elem2:
604             commonItems += 1
605     path = [".."] * (len(path1) - commonItems)
606     return path + path2[commonItems:]
607
608
609
610 class DistributionBuilder(object):
611     """
612     A builder of Twisted distributions.
613
614     This knows how to build tarballs for Twisted and all of its subprojects.
615
616     @type blacklist: C{list} of C{str}
617     @cvar blacklist: The list subproject names to exclude from the main Twisted
618         tarball and for which no individual project tarballs will be built.
619     """
620
621     from twisted.python.dist import twisted_subprojects as subprojects
622     blacklist = ["vfs", "web2"]
623
624     def __init__(self, rootDirectory, outputDirectory, apiBaseURL=None):
625         """
626         Create a distribution builder.
627
628         @param rootDirectory: root of a Twisted export which will populate
629             subsequent tarballs.
630         @type rootDirectory: L{FilePath}.
631
632         @param outputDirectory: The directory in which to create the tarballs.
633         @type outputDirectory: L{FilePath}
634
635         @type apiBaseURL: C{str} or C{NoneType}
636         @param apiBaseURL: A format string which will be interpolated with the
637             fully-qualified Python name for each API link.  For example, to
638             generate the Twisted 8.0.0 documentation, pass
639             C{"http://twistedmatrix.com/documents/8.0.0/api/%s.html"}.
640         """
641         self.rootDirectory = rootDirectory
642         self.outputDirectory = outputDirectory
643         self.apiBaseURL = apiBaseURL
644         self.manBuilder = ManBuilder()
645         self.docBuilder = DocBuilder()
646
647
648     def _buildDocInDir(self, path, version, howtoPath):
649         """
650         Generate documentation in the given path, building man pages first if
651         necessary and swallowing errors (so that directories without lore
652         documentation in them are ignored).
653
654         @param path: The path containing documentation to build.
655         @type path: L{FilePath}
656         @param version: The version of the project to include in all generated
657             pages.
658         @type version: C{str}
659         @param howtoPath: The "resource path" as L{DocBuilder} describes it.
660         @type howtoPath: L{FilePath}
661         """
662         templatePath = self.rootDirectory.child("doc").child("core"
663             ).child("howto").child("template.tpl")
664         if path.basename() == "man":
665             self.manBuilder.build(path)
666         if path.isdir():
667             try:
668                 self.docBuilder.build(version, howtoPath, path,
669                     templatePath, self.apiBaseURL, True)
670             except NoDocumentsFound:
671                 pass
672
673
674     def buildTwisted(self, version):
675         """
676         Build the main Twisted distribution in C{Twisted-<version>.tar.bz2}.
677
678         Projects listed in in L{blacklist} will not have their plugins, code,
679         documentation, or bin directories included.
680
681         bin/admin is also excluded.
682
683         @type version: C{str}
684         @param version: The version of Twisted to build.
685
686         @return: The tarball file.
687         @rtype: L{FilePath}.
688         """
689         releaseName = "Twisted-%s" % (version,)
690         buildPath = lambda *args: '/'.join((releaseName,) + args)
691
692         outputFile = self.outputDirectory.child(releaseName + ".tar.bz2")
693         tarball = tarfile.TarFile.open(outputFile.path, 'w:bz2')
694
695         docPath = self.rootDirectory.child("doc")
696
697         # Generate docs!
698         if docPath.isdir():
699             for subProjectDir in docPath.children():
700                 if (subProjectDir.isdir()
701                     and subProjectDir.basename() not in self.blacklist):
702                     for child in subProjectDir.walk():
703                         self._buildDocInDir(child, version,
704                             subProjectDir.child("howto"))
705
706         # Now, this part is nasty.  We need to exclude blacklisted subprojects
707         # from the main Twisted distribution. This means we need to exclude
708         # their bin directories, their documentation directories, their
709         # plugins, and their python packages. Given that there's no "add all
710         # but exclude these particular paths" functionality in tarfile, we have
711         # to walk through all these directories and add things that *aren't*
712         # part of the blacklisted projects.
713
714         for binthing in self.rootDirectory.child("bin").children():
715             # bin/admin should also not be included.
716             if binthing.basename() not in self.blacklist + ["admin"]:
717                 tarball.add(binthing.path,
718                             buildPath("bin", binthing.basename()))
719
720         bad_plugins = ["twisted_%s.py" % (blacklisted,)
721                        for blacklisted in self.blacklist]
722
723         for submodule in self.rootDirectory.child("twisted").children():
724             if submodule.basename() == "plugins":
725                 for plugin in submodule.children():
726                     if plugin.basename() not in bad_plugins:
727                         tarball.add(plugin.path, buildPath("twisted", "plugins",
728                                                            plugin.basename()))
729             elif submodule.basename() not in self.blacklist:
730                 tarball.add(submodule.path, buildPath("twisted",
731                                                       submodule.basename()))
732
733         for docDir in self.rootDirectory.child("doc").children():
734             if docDir.basename() not in self.blacklist:
735                 tarball.add(docDir.path, buildPath("doc", docDir.basename()))
736
737         for toplevel in self.rootDirectory.children():
738             if not toplevel.isdir():
739                 tarball.add(toplevel.path, buildPath(toplevel.basename()))
740
741         tarball.close()
742
743         return outputFile
744
745
746     def buildCore(self, version):
747         """
748         Build a core distribution in C{TwistedCore-<version>.tar.bz2}.
749
750         This is very similar to L{buildSubProject}, but core tarballs and the
751         input are laid out slightly differently.
752
753          - scripts are in the top level of the C{bin} directory.
754          - code is included directly from the C{twisted} directory, excluding
755            subprojects.
756          - all plugins except the subproject plugins are included.
757
758         @type version: C{str}
759         @param version: The version of Twisted to build.
760
761         @return: The tarball file.
762         @rtype: L{FilePath}.
763         """
764         releaseName = "TwistedCore-%s" % (version,)
765         outputFile = self.outputDirectory.child(releaseName + ".tar.bz2")
766         buildPath = lambda *args: '/'.join((releaseName,) + args)
767         tarball = self._createBasicSubprojectTarball(
768             "core", version, outputFile)
769
770         # Include the bin directory for the subproject.
771         for path in self.rootDirectory.child("bin").children():
772             if not path.isdir():
773                 tarball.add(path.path, buildPath("bin", path.basename()))
774
775         # Include all files within twisted/ that aren't part of a subproject.
776         for path in self.rootDirectory.child("twisted").children():
777             if path.basename() == "plugins":
778                 for plugin in path.children():
779                     for subproject in self.subprojects:
780                         if plugin.basename() == "twisted_%s.py" % (subproject,):
781                             break
782                     else:
783                         tarball.add(plugin.path,
784                                     buildPath("twisted", "plugins",
785                                               plugin.basename()))
786             elif not path.basename() in self.subprojects + ["topfiles"]:
787                 tarball.add(path.path, buildPath("twisted", path.basename()))
788
789         tarball.add(self.rootDirectory.child("twisted").child("topfiles").path,
790                     releaseName)
791         tarball.close()
792
793         return outputFile
794
795
796     def buildSubProject(self, projectName, version):
797         """
798         Build a subproject distribution in
799         C{Twisted<Projectname>-<version>.tar.bz2}.
800
801         @type projectName: C{str}
802         @param projectName: The lowercase name of the subproject to build.
803         @type version: C{str}
804         @param version: The version of Twisted to build.
805
806         @return: The tarball file.
807         @rtype: L{FilePath}.
808         """
809         releaseName = "Twisted%s-%s" % (projectName.capitalize(), version)
810         outputFile = self.outputDirectory.child(releaseName + ".tar.bz2")
811         buildPath = lambda *args: '/'.join((releaseName,) + args)
812         subProjectDir = self.rootDirectory.child("twisted").child(projectName)
813
814         tarball = self._createBasicSubprojectTarball(projectName, version,
815                                                      outputFile)
816
817         tarball.add(subProjectDir.child("topfiles").path, releaseName)
818
819         # Include all files in the subproject package except for topfiles.
820         for child in subProjectDir.children():
821             name = child.basename()
822             if name != "topfiles":
823                 tarball.add(
824                     child.path,
825                     buildPath("twisted", projectName, name))
826
827         pluginsDir = self.rootDirectory.child("twisted").child("plugins")
828         # Include the plugin for the subproject.
829         pluginFileName = "twisted_%s.py" % (projectName,)
830         pluginFile = pluginsDir.child(pluginFileName)
831         if pluginFile.exists():
832             tarball.add(pluginFile.path,
833                         buildPath("twisted", "plugins", pluginFileName))
834
835         # Include the bin directory for the subproject.
836         binPath = self.rootDirectory.child("bin").child(projectName)
837         if binPath.isdir():
838             tarball.add(binPath.path, buildPath("bin"))
839         tarball.close()
840
841         return outputFile
842
843
844     def _createBasicSubprojectTarball(self, projectName, version, outputFile):
845         """
846         Helper method to create and fill a tarball with things common between
847         subprojects and core.
848
849         @param projectName: The subproject's name.
850         @type projectName: C{str}
851         @param version: The version of the release.
852         @type version: C{str}
853         @param outputFile: The location of the tar file to create.
854         @type outputFile: L{FilePath}
855         """
856         releaseName = "Twisted%s-%s" % (projectName.capitalize(), version)
857         buildPath = lambda *args: '/'.join((releaseName,) + args)
858
859         tarball = tarfile.TarFile.open(outputFile.path, 'w:bz2')
860
861         tarball.add(self.rootDirectory.child("LICENSE").path,
862                     buildPath("LICENSE"))
863
864         docPath = self.rootDirectory.child("doc").child(projectName)
865
866         if docPath.isdir():
867             for child in docPath.walk():
868                 self._buildDocInDir(child, version, docPath.child("howto"))
869             tarball.add(docPath.path, buildPath("doc"))
870
871         return tarball
872
873
874
875 class UncleanWorkingDirectory(Exception):
876     """
877     Raised when the working directory of an SVN checkout is unclean.
878     """
879
880
881 class NotWorkingDirectory(Exception):
882     """
883     Raised when a directory does not appear to be an SVN working directory.
884     """
885
886
887 def buildAllTarballs(checkout, destination):
888     """
889     Build complete tarballs (including documentation) for Twisted and all
890     subprojects.
891
892     This should be called after the version numbers have been updated and
893     NEWS files created.
894
895     @type checkout: L{FilePath}
896     @param checkout: The SVN working copy from which a pristine source tree
897         will be exported.
898     @type destination: L{FilePath}
899     @param destination: The directory in which tarballs will be placed.
900
901     @raise UncleanWorkingDirectory: if there are modifications to the
902         working directory of C{checkout}.
903     @raise NotWorkingDirectory: if the checkout path is not an SVN checkout.
904     """
905     if not checkout.child(".svn").exists():
906         raise NotWorkingDirectory(
907             "%s does not appear to be an SVN working directory."
908             % (checkout.path,))
909     if runCommand(["svn", "st", checkout.path]).strip():
910         raise UncleanWorkingDirectory(
911             "There are local modifications to the SVN checkout in %s."
912              % (checkout.path,))
913
914     workPath = FilePath(mkdtemp())
915     export = workPath.child("export")
916     runCommand(["svn", "export", checkout.path, export.path])
917     twistedPath = export.child("twisted")
918     version = Project(twistedPath).getVersion()
919     versionString = version.base()
920
921     apiBaseURL = "http://twistedmatrix.com/documents/%s/api/%%s.html" % (
922         versionString)
923     if not destination.exists():
924         destination.createDirectory()
925     db = DistributionBuilder(export, destination, apiBaseURL=apiBaseURL)
926
927     db.buildCore(versionString)
928     for subproject in twisted_subprojects:
929         if (subproject not in db.blacklist
930             and twistedPath.child(subproject).exists()):
931             db.buildSubProject(subproject, versionString)
932
933     db.buildTwisted(versionString)
934     workPath.remove()
935
936
937 class ChangeVersionsScript(object):
938     """
939     A thing for changing version numbers. See L{main}.
940     """
941     changeAllProjectVersions = staticmethod(changeAllProjectVersions)
942
943     def main(self, args):
944         """
945         Given a list of command-line arguments, change all the Twisted versions
946         in the current directory.
947
948         @type args: list of str
949         @param args: List of command line arguments.  This should only
950             contain the version number.
951         """
952         version_format = (
953             "Version should be in a form kind of like '1.2.3[pre4]'")
954         if len(args) != 1:
955             sys.exit("Must specify exactly one argument to change-versions")
956         version = args[0]
957         try:
958             major, minor, micro_and_pre = version.split(".")
959         except ValueError:
960             raise SystemExit(version_format)
961         if "pre" in micro_and_pre:
962             micro, pre = micro_and_pre.split("pre")
963         else:
964             micro = micro_and_pre
965             pre = None
966         try:
967             major = int(major)
968             minor = int(minor)
969             micro = int(micro)
970             if pre is not None:
971                 pre = int(pre)
972         except ValueError:
973             raise SystemExit(version_format)
974         version_template = Version("Whatever",
975                                    major, minor, micro, prerelease=pre)
976         self.changeAllProjectVersions(FilePath("."), version_template)
977
978
979
980 class BuildTarballsScript(object):
981     """
982     A thing for building release tarballs. See L{main}.
983     """
984     buildAllTarballs = staticmethod(buildAllTarballs)
985
986     def main(self, args):
987         """
988         Build all release tarballs.
989
990         @type args: list of str
991         @param args: The command line arguments to process.  This must contain
992             two strings: the checkout directory and the destination directory.
993         """
994         if len(args) != 2:
995             sys.exit("Must specify two arguments: "
996                      "Twisted checkout and destination path")
997         self.buildAllTarballs(FilePath(args[0]), FilePath(args[1]))
998
999
1000
1001 class BuildAPIDocsScript(object):
1002     """
1003     A thing for building API documentation. See L{main}.
1004     """
1005
1006     def buildAPIDocs(self, projectRoot, output):
1007         """
1008         Build the API documentation of Twisted, with our project policy.
1009
1010         @param projectRoot: A L{FilePath} representing the root of the Twisted
1011             checkout.
1012         @param output: A L{FilePath} pointing to the desired output directory.
1013         """
1014         version = Project(projectRoot.child("twisted")).getVersion()
1015         versionString = version.base()
1016         sourceURL = ("http://twistedmatrix.com/trac/browser/tags/releases/"
1017                      "twisted-%s" % (versionString,))
1018         apiBuilder = APIBuilder()
1019         apiBuilder.build(
1020             "Twisted",
1021             "http://twistedmatrix.com/",
1022             sourceURL,
1023             projectRoot.child("twisted"),
1024             output)
1025
1026
1027     def main(self, args):
1028         """
1029         Build API documentation.
1030
1031         @type args: list of str
1032         @param args: The command line arguments to process.  This must contain
1033             two strings: the path to the root of the Twisted checkout, and a
1034             path to an output directory.
1035         """
1036         if len(args) != 2:
1037             sys.exit("Must specify two arguments: "
1038                      "Twisted checkout and distination path")
1039         self.buildAPIDocs(FilePath(args[0]), FilePath(args[1]))
Note: See TracBrowser for help on using the browser.