Remove a ?? in the description of Mac OS support.
[python/dscho.git] / Doc / tools / mkhowto
blob1537bd01fe26d64ec2ec6c5d4ed44dcf6611fb5e
1 #! /usr/bin/env python
2 # -*- Python -*-
3 """usage: %(program)s [options...] file ...
5 Options specifying formats to build:
6 --html HyperText Markup Language
7 --pdf Portable Document Format (default)
8 --ps PostScript
9 --dvi 'DeVice Indepentent' format from TeX
10 --text ASCII text (requires lynx)
12 More than one output format may be specified, or --all.
14 HTML options:
15 --address, -a Specify an address for page footers.
16 --link Specify the number of levels to include on each page.
17 --split, -s Specify a section level for page splitting, default: %(max_split_depth)s.
18 --iconserver, -i Specify location of icons (default: ../).
19 --image-type Specify the image type to use in HTML output;
20 values: gif (default), png.
21 --numeric Don't rename the HTML files; just keep node#.html for
22 the filenames.
23 --style Specify the CSS file to use for the output (filename,
24 not a URL).
25 --up-link URL to a parent document.
26 --up-title Title of a parent document.
28 Other options:
29 --a4 Format for A4 paper.
30 --letter Format for US letter paper (the default).
31 --help, -H Show this text.
32 --logging, -l Log stdout and stderr to a file (*.how).
33 --debugging, -D Echo commands as they are executed.
34 --keep, -k Keep temporary files around.
35 --quiet, -q Do not print command output to stdout.
36 (stderr is also lost, sorry; see *.how for errors)
37 """
39 import getopt
40 import glob
41 import os
42 import re
43 import shutil
44 import string
45 import sys
46 import tempfile
49 MYDIR = os.path.abspath(sys.path[0])
50 TOPDIR = os.path.dirname(MYDIR)
52 ISTFILE = os.path.join(TOPDIR, "texinputs", "python.ist")
53 NODE2LABEL_SCRIPT = os.path.join(MYDIR, "node2label.pl")
54 L2H_INIT_FILE = os.path.join(TOPDIR, "perl", "l2hinit.perl")
56 BIBTEX_BINARY = "bibtex"
57 DVIPS_BINARY = "dvips"
58 LATEX_BINARY = "latex"
59 LATEX2HTML_BINARY = "latex2html"
60 LYNX_BINARY = "lynx"
61 MAKEINDEX_BINARY = "makeindex"
62 PDFLATEX_BINARY = "pdflatex"
63 PERL_BINARY = "perl"
64 PYTHON_BINARY = "python"
67 def usage(options):
68 print __doc__ % options
70 def error(options, message, err=2):
71 sys.stdout = sys.stderr
72 print message
73 print
74 usage(options)
75 sys.exit(2)
78 class Options:
79 program = os.path.basename(sys.argv[0])
81 address = ''
82 debugging = 0
83 discard_temps = 1
84 have_temps = 0
85 icon_server = None
86 image_type = "gif"
87 logging = 0
88 max_link_depth = 3
89 max_split_depth = 6
90 paper = "letter"
91 quiet = 0
92 runs = 0
93 numeric = 0
94 style_file = os.path.join(TOPDIR, "html", "style.css")
95 about_file = os.path.join(TOPDIR, "html", "about.dat")
96 up_link = None
97 up_title = None
99 DEFAULT_FORMATS = ("pdf",)
100 ALL_FORMATS = ("dvi", "html", "pdf", "ps", "text")
102 def __init__(self):
103 self.formats = []
104 self.l2h_init_files = []
106 def __getitem__(self, key):
107 # This is used when formatting the usage message.
108 try:
109 return getattr(self, key)
110 except AttributeError:
111 raise KeyError, key
113 def parse(self, args):
114 opts, args = getopt.getopt(args, "Hi:a:s:lDkqr:",
115 ["all", "postscript", "help", "iconserver=",
116 "address=", "a4", "letter", "l2h-init=",
117 "link=", "split=", "logging", "debugging",
118 "keep", "quiet", "runs=", "image-type=",
119 "about=", "numeric", "style=",
120 "up-link=", "up-title="]
121 + list(self.ALL_FORMATS))
122 for opt, arg in opts:
123 if opt == "--all":
124 self.formats = list(self.ALL_FORMATS)
125 elif opt in ("-H", "--help"):
126 usage(self)
127 sys.exit()
128 elif opt == "--iconserver":
129 self.icon_server = arg
130 elif opt in ("-a", "--address"):
131 self.address = arg
132 elif opt == "--a4":
133 self.paper = "a4"
134 elif opt == "--letter":
135 self.paper = "letter"
136 elif opt == "--link":
137 self.max_link_depth = int(arg)
138 elif opt in ("-s", "--split"):
139 self.max_split_depth = int(arg)
140 elif opt in ("-l", "--logging"):
141 self.logging = self.logging + 1
142 elif opt in ("-D", "--debugging"):
143 self.debugging = self.debugging + 1
144 elif opt in ("-k", "--keep"):
145 self.discard_temps = 0
146 elif opt in ("-q", "--quiet"):
147 self.quiet = 1
148 elif opt in ("-r", "--runs"):
149 self.runs = int(arg)
150 elif opt == "--image-type":
151 self.image_type = arg
152 elif opt == "--about":
153 # always make this absolute:
154 self.about_file = os.path.normpath(
155 os.path.abspath(arg))
156 elif opt == "--numeric":
157 self.numeric = 1
158 elif opt == "--style":
159 self.style_file = os.path.abspath(arg)
160 elif opt == "--l2h-init":
161 self.l2h_init_files.append(os.path.abspath(arg))
162 elif opt == "--up-link":
163 self.up_link = arg
164 elif opt == "--up-title":
165 self.up_title = arg
167 # Format specifiers:
169 elif opt[2:] in self.ALL_FORMATS:
170 self.add_format(opt[2:])
171 elif opt == "--postscript":
172 # synonym for --ps
173 self.add_format("ps")
174 self.initialize()
176 # return the args to allow the caller access:
178 return args
180 def add_format(self, format):
181 """Add a format to the formats list if not present."""
182 if not format in self.formats:
183 self.formats.append(format)
185 def initialize(self):
186 """Complete initialization. This is needed if parse() isn't used."""
187 # add the default format if no formats were specified:
188 if not self.formats:
189 self.formats = self.DEFAULT_FORMATS
190 # determine the base set of texinputs directories:
191 texinputs = string.split(os.environ.get("TEXINPUTS", ""), os.pathsep)
192 if not texinputs:
193 texinputs = ['']
194 self.base_texinputs = [
195 os.path.join(TOPDIR, "paper-" + self.paper),
196 os.path.join(TOPDIR, "texinputs"),
197 ] + texinputs
200 class Job:
201 latex_runs = 0
203 def __init__(self, options, path):
204 self.options = options
205 self.doctype = get_doctype(path)
206 self.filedir, self.doc = split_pathname(path)
207 self.log_filename = self.doc + ".how"
208 if os.path.exists(self.log_filename):
209 os.unlink(self.log_filename)
210 if os.path.exists(self.doc + ".l2h"):
211 self.l2h_aux_init_file = tempfile.mktemp()
212 else:
213 self.l2h_aux_init_file = self.doc + ".l2h"
214 self.write_l2h_aux_init_file()
216 def build(self):
217 self.setup_texinputs()
218 formats = self.options.formats
219 if "dvi" in formats or "ps" in formats:
220 self.build_dvi()
221 if "pdf" in formats:
222 self.build_pdf()
223 if "ps" in formats:
224 self.build_ps()
225 if "html" in formats:
226 self.require_temps()
227 self.build_html(self.doc)
228 if self.options.icon_server == ".":
229 pattern = os.path.join(TOPDIR, "html", "icons",
230 "*." + self.options.image_type)
231 imgs = glob.glob(pattern)
232 if not imgs:
233 self.warning(
234 "Could not locate support images of type %s."
235 % `self.options.image_type`)
236 for fn in imgs:
237 new_fn = os.path.join(self.doc, os.path.basename(fn))
238 shutil.copyfile(fn, new_fn)
239 if "text" in formats:
240 self.require_temps()
241 tempdir = self.doc
242 need_html = "html" not in formats
243 if self.options.max_split_depth != 1:
244 fp = open(self.l2h_aux_init_file, "a")
245 fp.write("# re-hack this file for --text:\n")
246 l2hoption(fp, "MAX_SPLIT_DEPTH", "1")
247 fp.write("1;\n")
248 fp.close()
249 tempdir = self.doc + "-temp-html"
250 need_html = 1
251 if need_html:
252 self.build_html(tempdir, max_split_depth=1)
253 self.build_text(tempdir)
254 if self.options.discard_temps:
255 self.cleanup()
257 def setup_texinputs(self):
258 texinputs = [self.filedir] + list(self.options.base_texinputs)
259 os.environ["TEXINPUTS"] = string.join(texinputs, os.pathsep)
260 self.message("TEXINPUTS=" + os.environ["TEXINPUTS"])
262 def build_aux(self, binary=None):
263 if binary is None:
264 binary = LATEX_BINARY
265 new_index( "%s.ind" % self.doc, "genindex")
266 new_index("mod%s.ind" % self.doc, "modindex")
267 self.run("%s %s" % (binary, self.doc))
268 self.use_bibtex = check_for_bibtex(self.doc + ".aux")
269 self.latex_runs = 1
271 def build_dvi(self):
272 self.use_latex(LATEX_BINARY)
274 def build_pdf(self):
275 self.use_latex(PDFLATEX_BINARY)
277 def use_latex(self, binary):
278 self.require_temps(binary=binary)
279 if self.latex_runs < 2:
280 if os.path.isfile("mod%s.idx" % self.doc):
281 self.run("%s mod%s.idx" % (MAKEINDEX_BINARY, self.doc))
282 if os.path.isfile(self.doc + ".idx"):
283 # call to Doc/tools/fix_hack omitted; doesn't appear necessary
284 self.run("%s %s.idx" % (MAKEINDEX_BINARY, self.doc))
285 import indfix
286 indfix.process(self.doc + ".ind")
287 if self.use_bibtex:
288 self.run("%s %s" % (BIBTEX_BINARY, self.doc))
289 self.process_synopsis_files()
291 # let the doctype-specific handler do some intermediate work:
293 self.run("%s %s" % (binary, self.doc))
294 self.latex_runs += 1
295 if os.path.isfile("mod%s.idx" % self.doc):
296 self.run("%s -s %s mod%s.idx"
297 % (MAKEINDEX_BINARY, ISTFILE, self.doc))
298 if os.path.isfile(self.doc + ".idx"):
299 self.run("%s -s %s %s.idx"
300 % (MAKEINDEX_BINARY, ISTFILE, self.doc))
301 self.process_synopsis_files()
303 # and now finish it off:
305 if os.path.isfile(self.doc + ".toc") and binary == PDFLATEX_BINARY:
306 import toc2bkm
307 if self.doctype == "manual":
308 bigpart = "chapter"
309 else:
310 bigpart = "section"
311 toc2bkm.process(self.doc + ".toc", self.doc + ".bkm", bigpart)
312 if self.use_bibtex:
313 self.run("%s %s" % (BIBTEX_BINARY, self.doc))
314 self.run("%s %s" % (binary, self.doc))
315 self.latex_runs += 1
317 def process_synopsis_files(self):
318 synopsis_files = glob.glob(self.doc + "*.syn")
319 for path in synopsis_files:
320 uniqify_module_table(path)
322 def build_ps(self):
323 self.run("%s -N0 -o %s.ps %s" % (DVIPS_BINARY, self.doc, self.doc))
325 def build_html(self, builddir=None, max_split_depth=None):
326 if builddir is None:
327 builddir = self.doc
328 if max_split_depth is None:
329 max_split_depth = self.options.max_split_depth
330 texfile = None
331 for p in string.split(os.environ["TEXINPUTS"], os.pathsep):
332 fn = os.path.join(p, self.doc + ".tex")
333 if os.path.isfile(fn):
334 texfile = fn
335 break
336 if not texfile:
337 self.warning("Could not locate %s.tex; aborting." % self.doc)
338 sys.exit(1)
339 # remove leading ./ (or equiv.); might avoid problems w/ dvips
340 if texfile[:2] == os.curdir + os.sep:
341 texfile = texfile[2:]
342 # build the command line and run LaTeX2HTML:
343 if not os.path.isdir(builddir):
344 os.mkdir(builddir)
345 else:
346 for fname in glob.glob(os.path.join(builddir, "*.html")):
347 os.unlink(fname)
348 args = [LATEX2HTML_BINARY,
349 "-init_file", self.l2h_aux_init_file,
350 "-dir", builddir,
351 texfile
353 self.run(string.join(args)) # XXX need quoting!
354 # ... postprocess
355 shutil.copyfile(self.options.style_file,
356 os.path.join(builddir, self.doc + ".css"))
357 shutil.copyfile(os.path.join(builddir, self.doc + ".html"),
358 os.path.join(builddir, "index.html"))
359 if max_split_depth != 1 and not self.options.numeric:
360 pwd = os.getcwd()
361 try:
362 os.chdir(builddir)
363 self.run("%s %s *.html" % (PERL_BINARY, NODE2LABEL_SCRIPT))
364 finally:
365 os.chdir(pwd)
367 def build_text(self, tempdir=None):
368 if tempdir is None:
369 tempdir = self.doc
370 indexfile = os.path.join(tempdir, "index.html")
371 self.run("%s -nolist -dump %s >%s.txt"
372 % (LYNX_BINARY, indexfile, self.doc))
374 def require_temps(self, binary=None):
375 if not self.latex_runs:
376 self.build_aux(binary=binary)
378 def write_l2h_aux_init_file(self):
379 options = self.options
380 fp = open(self.l2h_aux_init_file, "w")
381 d = string_to_perl(os.path.dirname(L2H_INIT_FILE))
382 fp.write("package main;\n"
383 "push (@INC, '%s');\n"
384 "$mydir = '%s';\n"
385 % (d, d))
386 fp.write(open(L2H_INIT_FILE).read())
387 for filename in options.l2h_init_files:
388 fp.write("\n# initialization code incorporated from:\n# ")
389 fp.write(filename)
390 fp.write("\n")
391 fp.write(open(filename).read())
392 fp.write("\n"
393 "# auxillary init file for latex2html\n"
394 "# generated by mkhowto\n"
395 "$NO_AUTO_LINK = 1;\n"
397 l2hoption(fp, "ABOUT_FILE", options.about_file)
398 l2hoption(fp, "ICONSERVER", options.icon_server)
399 l2hoption(fp, "IMAGE_TYPE", options.image_type)
400 l2hoption(fp, "ADDRESS", options.address)
401 l2hoption(fp, "MAX_LINK_DEPTH", options.max_link_depth)
402 l2hoption(fp, "MAX_SPLIT_DEPTH", options.max_split_depth)
403 l2hoption(fp, "EXTERNAL_UP_LINK", options.up_link)
404 l2hoption(fp, "EXTERNAL_UP_TITLE", options.up_title)
405 fp.write("1;\n")
406 fp.close()
408 def cleanup(self):
409 self.__have_temps = 0
410 for pattern in ("%s.aux", "%s.log", "%s.out", "%s.toc", "%s.bkm",
411 "%s.idx", "%s.ilg", "%s.ind", "%s.pla",
412 "%s.bbl", "%s.blg",
413 "mod%s.idx", "mod%s.ind", "mod%s.ilg",
415 safe_unlink(pattern % self.doc)
416 map(safe_unlink, glob.glob(self.doc + "*.syn"))
417 for spec in ("IMG*", "*.pl", "WARNINGS", "index.dat", "modindex.dat"):
418 pattern = os.path.join(self.doc, spec)
419 map(safe_unlink, glob.glob(pattern))
420 if "dvi" not in self.options.formats:
421 safe_unlink(self.doc + ".dvi")
422 if os.path.isdir(self.doc + "-temp-html"):
423 shutil.rmtree(self.doc + "-temp-html", ignore_errors=1)
424 if not self.options.logging:
425 os.unlink(self.log_filename)
426 if not self.options.debugging:
427 os.unlink(self.l2h_aux_init_file)
429 def run(self, command):
430 self.message(command)
431 rc = os.system("(%s) </dev/null >>%s 2>&1"
432 % (command, self.log_filename))
433 if rc:
434 self.warning(
435 "Session transcript and error messages are in %s."
436 % self.log_filename)
437 sys.exit(rc)
439 def message(self, msg):
440 msg = "+++ " + msg
441 if not self.options.quiet:
442 print msg
443 self.log(msg + "\n")
445 def warning(self, msg):
446 msg = "*** %s\n" % msg
447 sys.stderr.write(msg)
448 self.log(msg)
450 def log(self, msg):
451 fp = open(self.log_filename, "a")
452 fp.write(msg)
453 fp.close()
456 def safe_unlink(path):
457 try:
458 os.unlink(path)
459 except os.error:
460 pass
463 def split_pathname(path):
464 path = os.path.normpath(os.path.join(os.getcwd(), path))
465 dirname, basename = os.path.split(path)
466 if basename[-4:] == ".tex":
467 basename = basename[:-4]
468 return dirname, basename
471 _doctype_rx = re.compile(r"\\documentclass(?:\[[^]]*\])?{([a-zA-Z]*)}")
472 def get_doctype(path):
473 fp = open(path)
474 doctype = None
475 while 1:
476 line = fp.readline()
477 if not line:
478 break
479 m = _doctype_rx.match(line)
480 if m:
481 doctype = m.group(1)
482 break
483 fp.close()
484 return doctype
487 def main():
488 options = Options()
489 try:
490 args = options.parse(sys.argv[1:])
491 except getopt.error, msg:
492 error(options, msg)
493 if not args:
494 # attempt to locate single .tex file in current directory:
495 args = glob.glob("*.tex")
496 if not args:
497 error(options, "No file to process.")
498 if len(args) > 1:
499 error(options, "Could not deduce which files should be processed.")
501 # parameters are processed, let's go!
503 for path in args:
504 Job(options, path).build()
507 def l2hoption(fp, option, value):
508 if value:
509 fp.write('$%s = "%s";\n' % (option, string_to_perl(str(value))))
512 _to_perl = {}
513 for c in map(chr, range(1, 256)):
514 _to_perl[c] = c
515 _to_perl["@"] = "\\@"
516 _to_perl["$"] = "\\$"
517 _to_perl['"'] = '\\"'
519 def string_to_perl(s):
520 return string.join(map(_to_perl.get, s), '')
523 def check_for_bibtex(filename):
524 fp = open(filename)
525 pos = string.find(fp.read(), r"\bibdata{")
526 fp.close()
527 return pos >= 0
529 def uniqify_module_table(filename):
530 lines = open(filename).readlines()
531 if len(lines) > 1:
532 if lines[-1] == lines[-2]:
533 del lines[-1]
534 open(filename, "w").writelines(lines)
537 def new_index(filename, label="genindex"):
538 fp = open(filename, "w")
539 fp.write(r"""\
540 \begin{theindex}
541 \label{%s}
542 \end{theindex}
543 """ % label)
544 fp.close()
547 if __name__ == "__main__":
548 main()