# This file is part of Rubber and thus covered by the GPL
# (c) Emmanuel Beffara, 2002--2006
"""
BibTeX support for Rubber
This module is a special one: it is triggered by the macros \\bibliography and
\\bibliographystyle and not as a package, so the main system knows about it.
The module provides the following commands:
path
= adds to the search path for databases
stylepath = adds to the search path for styles
"""
# Stop python 2.2 from calling "yield" statements syntax errors.
from __future__ import generators
import os, sys
from os.path import *
import re, string
import subprocess
#from grubber import _
#from grubber import *
from msg import _, msg
from plugins import TexModule
re_bibdata = re.compile(r"\\bibdata{(?P.*)}")
re_citation = re.compile(r"\\citation{(?P.*)}")
re_undef = re.compile("LaTeX Warning: Citation `(?P.*)' .*undefined.*")
# The regular expression that identifies errors in BibTeX log files is heavily
# heuristic. The remark is that all error messages end with a text of the form
# "---line xxx of file yyy" or "---while reading file zzz". The actual error
# is either the text before the dashes or the text on the previous line.
re_error = re.compile(
"---(line (?P[0-9]+) of|while reading) file (?P.*)")
class BibTex(TexModule):
"""
This class is the module that handles BibTeX in Rubber. It provides the
funcionality required when compiling documents as well as material to
parse blg files for diagnostics.
"""
def __init__ (self, doc, dict, base=None):
"""
Initialize the state of the module and register appropriate functions
in the main process. The extra arugment 'base' can be used to specify
the base name of the aux file, it defaults to the document name.
"""
self.doc = doc
self.env = doc.env
if not(base):
self.base = doc.src_base
else:
self.base = base
self.bblfile = self.base + ".bbl"
self.blgfile = self.base + ".blg"
self.auxfile = self.base + ".aux"
# cwd = self.env.vars["cwd"]
# cwd = ""
# self.bib_path = [cwd]
# if doc.src_path != cwd:
# self.bib_path.append(doc.src_path)
# self.bst_path = [cwd]
self.bib_path = []
self.bst_path = []
self.undef_cites = None
self.used_cites = None
self.style = None
self.set_style("plain")
self.db = {}
self.sorted = 1
self.run_needed = 0
#
# The following method are used to specify the various datafiles that
# BibTeX uses.
#
def do_path (self, path):
self.bib_path.append(self.doc.abspath(path))
def do_stylepath (self, path):
self.bst_path.append(self.doc.abspath(path))
def do_sorted (self, mode):
self.sorted = mode in ("true", "yes", "1")
def add_db (self, name):
"""
Register a bibliography database file.
"""
for dir in self.bib_path:
bib = join(dir, name + ".bib")
if exists(bib):
self.db[name] = bib
self.doc.sources[bib] = DependLeaf(self.env, bib)
self.doc.not_included.append(bib)
return
def set_style (self, style):
"""
Define the bibliography style used. This method is called when
\\bibliographystyle is found. If the style file is found in the
current directory, it is considered a dependency.
"""
if self.style:
old_bst = self.style + ".bst"
if exists(old_bst) and self.doc.sources.has_key(old_bst):
del self.doc.sources[old_bst]
self.style = style
for dir in self.bst_path:
new_bst = join(dir, style + ".bst")
if exists(new_bst):
self.bst_file = new_bst
self.doc.sources[new_bst] = DependLeaf(self.env, new_bst)
return
self.bst_file = None
#
# The following methods are responsible of detecting when running BibTeX
# is needed and actually running it.
#
def pre_compile (self):
"""
Run BibTeX if needed before the first compilation. This function also
checks if BibTeX has been run by someone else, and in this case it
tells the system that it should recompile the document.
"""
if exists(self.doc.auxfile):
self.used_cites, self.prev_dbs = self.parse_aux()
else:
self.prev_dbs = None
if self.doc.log.lines:
self.undef_cites = self.list_undefs()
self.run_needed = self.first_run_needed()
if self.doc.must_compile:
# If a LaTeX compilation is going to happen, it is not necessary
# to bother with BibTeX yet.
return 0
if self.run_needed:
return self.run()
if (exists(self.bblfile) and
getmtime(self.bblfile) > getmtime(self.doc.logfile)):
self.doc.must_compile = 1
return 0
def first_run_needed (self):
"""
The condition is only on the database files' modification dates, but
it would be more clever to check if the results have changed.
BibTeXing is also needed when the last run of BibTeX failed, and in
the very particular case when the style has changed since last
compilation.
"""
if not exists(self.auxfile):
return 0
if not exists(self.blgfile):
return 1
dtime = getmtime(self.blgfile)
for db in self.db.values():
if getmtime(db) > dtime:
msg.log(_("bibliography database %s was modified") % db,
pkg="bibtex")
return 1
blg = open(self.blgfile)
for line in blg.readlines():
if re_error.search(line):
blg.close()
msg.log(_("last BibTeXing failed"), pkg="bibtex")
return 1
blg.close()
if self.style_changed():
return 1
if self.bst_file and getmtime(self.bst_file) > dtime:
msg.log(_("the bibliography style file was modified"), pkg="bibtex")
return 1
return 0
def parse_aux (self):
"""
Parse the aux files and return the list of all defined citations and
the list of databases used.
"""
last = 0
cites = {}
dbs = []
auxfiles = [self.doc.auxfile]
if self.auxfile != self.doc.auxfile:
auxfiles.append(self.auxfile)
for auxname in auxfiles:
aux = open(auxname)
for line in aux:
m = re_citation.match(line)
if m:
cite = m.group("cite")
if not cites.has_key(cite):
last = last + 1
cites[cite] = last
continue
m = re_bibdata.match(line)
if m:
dbs.extend(m.group("data").split(","))
aux.close()
dbs.sort()
if self.sorted:
list = cites.keys()
list.sort()
return list, dbs
else:
list = [(n,c) for (c,n) in cites.items()]
list.sort()
return [c for (n,c) in list], dbs
def list_undefs (self):
"""
Return the list of all undefined citations.
"""
cites = {}
for line in self.doc.log.lines:
match = re_undef.match(line)
if match:
cites[match.group("cite")] = None
list = cites.keys()
list.sort()
return list
def post_compile (self):
"""
This method runs BibTeX if needed to solve undefined citations. If it
was run, then force a new LaTeX compilation.
"""
if not self.bibtex_needed():
msg.log(_("no BibTeXing needed"), pkg="bibtex")
return 0
return self.run()
def run (self):
"""
This method actually runs BibTeX with the appropriate environment
variables set.
"""
msg.progress(_("running BibTeX on %s") % self.base)
doc = {}
if len(self.bib_path) != 1:
os.environ["BIBINPUTS"] = string.join(self.bib_path +
[os.getenv("BIBINPUTS", "")], ":")
if len(self.bst_path) != 1:
os.environ["BSTINPUTS"] = string.join(self.bst_path +
[os.getenv("BSTINPUTS", "")], ":")
rc = subprocess.call(["bibtex", self.base], stdout=msg.stdout)
if rc != 0:
msg.error(_("There were errors making the bibliography."))
return 1
self.run_needed = 0
self.doc.must_compile = 1
return 0
def bibtex_needed (self):
"""
Return true if BibTeX must be run.
"""
if self.run_needed:
return 1
msg.log(_("checking if BibTeX must be run..."), pkg="bibtex")
newcites, dbs = self.parse_aux()
# If there was a list of used citations, we check if it has
# changed. If it has, we have to rerun.
if self.prev_dbs is not None and self.prev_dbs != dbs:
msg.log(_("the set of databases changed"), pkg="bibtex")
self.prev_dbs = dbs
self.used_cites = newcites
self.undef_cites = self.list_undefs()
return 1
self.prev_dbs = dbs
# If there was a list of used citations, we check if it has
# changed. If it has, we have to rerun.
if self.used_cites and newcites != self.used_cites:
msg.log(_("the list of citations changed"), pkg="bibtex")
self.used_cites = newcites
self.undef_cites = self.list_undefs()
return 1
self.used_cites = newcites
# If there was a list of undefined citations, we check if it has
# changed. If it has and it is not empty, we have to rerun.
if self.undef_cites:
new = self.list_undefs()
if new == []:
msg.log(_("no more undefined citations"), pkg="bibtex")
self.undef_cites = new
else:
for cite in new:
if cite in self.undef_cites:
continue
msg.log(_("there are new undefined citations"), pkg="bibtex")
self.undef_cites = new
return 1
msg.log(_("there is no new undefined citation"), pkg="bibtex")
self.undef_cites = new
return 0
else:
self.undef_cites = self.list_undefs()
# At this point we don't know if undefined citations changed. If
# BibTeX has not been run before (i.e. there is no log file) we know
# that it has to be run now.
if not exists(self.blgfile):
msg.log(_("no BibTeX log file"), pkg="bibtex")
return 1
# Here, BibTeX has been run before but we don't know if undefined
# citations changed.
if self.undef_cites == []:
msg.log(_("no undefined citations"), pkg="bibtex")
return 0
if getmtime(self.blgfile) < getmtime(self.doc.logfile):
msg.log(_("BibTeX's log is older than the main log"), pkg="bibtex")
return 1
return 0
def clean (self):
self.doc.remove_suffixes([".bbl", ".blg"])
#
# The following method extract information from BibTeX log files.
#
def style_changed (self):
"""
Read the log file if it exists and check if the style used is the one
specified in the source. This supposes that the style is mentioned on
a line with the form 'The style file: foo.bst'.
"""
if not exists(self.blgfile):
return 0
log = open(self.blgfile)
line = log.readline()
while line != "":
if line.startswith("The style file: "):
if line.rstrip()[16:-4] != self.style:
msg.log(_("the bibliography style was changed"), pkg="bibtex")
log.close()
return 1
line = log.readline()
log.close()
return 0
def get_errors (self):
"""
Read the log file, identify error messages and report them.
"""
if not exists(self.blgfile):
return
log = open(self.blgfile)
last_line = ""
for line in log:
m = re_error.search(line)
if m:
# TODO: it would be possible to report the offending code.
if m.start() == 0:
text = string.strip(last_line)
else:
text = string.strip(line[:m.start()])
line = m.group("line")
if line: line = int(line)
d = {
"pkg": "bibtex",
"kind": "error",
"text": text
}
d.update( m.groupdict() )
# BibTeX does not report the path of the database in its log.
file = d["file"]
if file[-4:] == ".bib":
file = file[:-4]
if self.db.has_key(file):
d["file"] = self.db[file]
elif self.db.has_key(file + ".bib"):
d["file"] = self.db[file + ".bib"]
yield d
last_line = line
log.close()
class Module(BibTex):
"""
Module to load to handle a bibtex
"""