Unverified Commit b40191ca authored by Vanille-N's avatar Vanille-N
Browse files

tex configuration

parent b7f60515
build/
*.tex.mk
TWICE = true
DOC = mail
all: $(DOC:%=%.pdf)
doc_%.tex.mk: cfg_%.tmk texmake.py
./texmake.py name=$* fail=WARNING
common.tex.mk: texmake.py
./texmake.py common
-include $(DOC:%=doc_%.tex.mk)
-include common.tex.mk
clean:
rm -rf build
rm -f *.tex.mk
rm -f $(DOC:%=%.pdf)
force:
make clean && make
.PHONY: clean force
hdr:head/
beamerpkg
macros
more
intro/
intro
imap.png
stmp.png
imap/
fig:rename
imap
imap-flowchart.png
imap-flowchart-legend.png
fig:pop-interaction
smtp/
fig:ynerant-head
fig:ynerant-body
fig:mail-incorrect
fig:mail-correct
smtp
recu.png
undelivered.png
#! /bin/env python3
# TeXMake
# Build descriptor for LaTeX
import re
import sys
import os
# Error reporting with uniform formatting and colored output
class Err:
fname = ""
line = 0
ALWAYS = 0
WARNING = 1
ERROR = 2
NEVER = 3
fatality = ALWAYS
def in_file(fname):
Err.fname = fname
Err.line = 0
def count_line(text):
Err.line += 1
Err.text = text
def report(*,
kind,
msg,
fatal=True,
):
Err.fatality = max(Err.fatality, Err.ERROR if fatal else Err.WARNING)
print("fatality: {}".format(Err.fatality))
print("In \x1b[36m{}:{}\x1b[0m, '{}'".format(Err.fname, Err.line, Err.text))
print("{}: {}\x1b[0m".format(
"\x1b[1;31mError" if fatal else "\x1b[1;33mWarning", kind
))
print(" {}".format(msg))
print()
def is_filename(s):
for c in s:
if not (
'a' <= c <= 'z' or
'A' <= c <= 'Z' or
c in "-_."
):
return False
return True
class File:
def __init__(self, path):
spath = path.rsplit("/", 1)
dir = spath[0] if len(spath) > 1 else ""
name = spath[-1]
sname = name.rsplit(".", 1)
name = sname[0]
ext = sname[1] if len(sname) > 1 else None
self.dir = dir
self.name = name
self.ext = ext
def with_prefix(self, pre):
f = File("")
f.dir = pre + ("/" if self.dir != "" else "") + self.dir
f.name = self.name
f.ext = self.ext
return f
def with_ext(self, ext):
f = File("")
f.dir = self.dir
f.name = self.name
f.ext = ext
return f
def exists(self):
return os.path.isfile(self.path())
def path(self):
return self.dir + "/" + self.name + "." + (self.ext or "tex")
def __str__(self):
return "File({})".format(self.path())
def try_pdf(self):
if (self.ext or "tex") == "tex":
return self.with_ext("pdf")
else:
return self
class Refs:
def __init__(self, decl=[]):
self.induce = set()
self.depend = set()
for d in decl:
if d == "":
continue
elif len(d) == 1:
Err.report(
kind="Empty Reference",
msg="< or > must be followed by a name",
fatal=False,
)
elif not is_filename(d[1:]):
Err.report(
kind="Invalid Reference",
msg="'{}' contains characters outside of azAZ_-.".format(d[1:]),
fatal=False,
)
elif d[0] == ">":
self.induce.add(d[1:])
elif d[0] == "<":
self.depend.add(d[1:])
else:
Err.report(
kind="Not a Reference",
msg="references must start with > or <",
fatal=False,
)
def union(self, other):
u = Refs()
u.induce = self.induce.union(other.induce)
u.depend = self.depend.union(other.depend)
return u
def __str__(self):
return "<({}) >({})".format(",".join(self.depend), ",".join(self.induce))
def into_graph(refs):
graph = {}
for file in refs:
rs = refs[file]
if len(rs.induce) > 0:
graph[file] = rs.induce
for d in rs.depend:
if d not in graph:
graph[d] = []
graph[d].append(file)
return graph
class Cfg:
def __init__(self):
self.txt = []
self.fig = []
self.bib = []
self.hdr = []
self.tag_stk = ['txt']
self.path_stk = []
self.ref_stk = [Refs()]
self.refs = {}
def trim_comment(line):
i = line.find("#")
if i == -1:
return line
else:
return line[:i]
def push(self, line):
line = Cfg.trim_comment(line)
Err.count_line(line)
item = self.read_path(line)
if item is not None:
(file, tag, refs) = item
self.refs[file] = refs
if tag == 'fig':
self.fig.append(file)
elif tag == 'bib':
self.bib.append(file)
elif tag == 'hdr':
self.hdr.append(file)
elif tag == 'txt':
self.txt.append(file)
else:
Err.report(
kind="Unknown Tag",
msg="'{}' should be in fig,bib,hdr,txt".format(tag),
fatal=False,
)
def read_path(self, line):
# compute indentation depth
depth = 0
while line != "" and line[0] == ' ':
depth += 1
line = line[1:]
while line != "" and line[-1] == ' ':
line = line[:-1]
if line == '':
return None
if depth % 4 != 0:
Err.report(
kind="Invalid Indentation",
msg="current indentation {} is not a multiple of 4 spaces".format(depth),
)
return None
else:
depth = depth // 4
file, *refs = line.split(" ")
refs = Refs(refs)
s = file.split(':')
if len(s) == 1:
tag = None
else:
if len(s) > 2:
Err.report(
kind="Too Many Tags",
msg="':' separates tag, first one overriden",
)
tag = s[-2]
file = s[-1]
if depth > len(self.path_stk):
Err.report(
kind="Too Much Indentation",
msg="indentation is a lot more than previous level",
fatal=False,
)
while depth > len(self.path_stk):
self.path_stk.append("")
self.tag_stk.append(self.tag_stk[-1])
self.ref_stk.append(self.ref_stk[-1])
while len(self.path_stk) > depth:
self.path_stk.pop()
self.tag_stk.pop()
self.ref_stk.pop()
assert len(self.ref_stk) == len(self.tag_stk)
if tag is None:
tag = self.tag_stk[-1]
refs = refs.union(self.ref_stk[-1])
if "/../" in file or file.startswith("../") or file.endswith("/..") or file == "..":
Err.report(
kind="Directory Climbing",
msg="using .. is discouraged",
)
return None
if file == "":
Err.report(
kind="Empty Filename",
msg="use of ./ is preferred to artificially introduce a hierarchy",
fatal=False,
)
self.path_stk.append(file)
self.tag_stk.append(tag)
self.ref_stk.append(refs)
assert len(self.ref_stk) == len(self.tag_stk)
return None
elif file[-1] == '/':
self.path_stk.append(file)
self.tag_stk.append(tag)
self.ref_stk.append(refs)
assert len(self.ref_stk) == len(self.tag_stk)
return None
else:
file = File("".join(self.path_stk) + file)
if not file.with_prefix("src").exists():
Err.report(
kind="Nonexistent File",
msg="file {} was not found".format(file.path()),
fatal=False,
)
return (file, tag, refs)
def print(self, args):
descr = """\
# Autogenerated by TeXMake
TEX_SRC_%name% = \\
%iter%(file.with_prefix("build").path() for file in self.txt + [File("%name%")])
TEX_FIG_%name% = \\
%iter%(file.with_prefix("build").try_pdf().path() for file in self.fig)
BIBLIO_%name% = \\
%iter%(file.with_prefix("build").path() for file in self.bib)
HEADERS_%name% = \\
%iter%(file.with_prefix("build").path() for file in self.hdr)
MAKES_%name% = \\
doc_%name%.tex.mk \\
common.tex.mk \\
Makefile
%name%.pdf: \\
$(TEX_FIG_%name%) \\
$(TEX_SRC_%name%) \\
$(BIBLIO_%name%) \\
$(HEADERS_%name%) \\
$(MAKES_%name%)
#
make compile-%name%
@if [ -z \"$(QUICK)\" ]; then \\
%if% len(self.bib) > 0
cp $(BIBLIO_%name%) build && \\
( cd build && bibtex %name% ) && \\
make compile-%name%; \\
%endif%
make compile-%name%; \\
fi
cp build/%name%.pdf . &>/dev/null
compile-%name%:
make DIR=build FILE=%name%.tex compile
.PHONY: compile-%name%
"""
with open(args.dest, 'w') as f:
isconditional = False
condition = False
for line in descr.split("\n"):
line = line.replace("%name%", args.name).replace(" ", "\t")
if "%if%" in line:
_, test = line.split("%if%", 1)
isconditional = True
conditional = eval(test)
continue
elif "%endif%" in line:
isconditional = False
continue
elif "%else%" in line:
condition = not condition
continue
elif isconditional and condition:
continue
if "%iter%" in line:
_, files = line.split("%iter%", 1)
f.write("\t" + " \\\n\t".join(eval(files)) + "\n")
else:
f.write(line + "\n")
graph = Refs.into_graph(self.refs)
for pre in graph:
post = graph[pre]
if type(pre) == File:
f.write("{}: {}\n".format(
pre.with_prefix("build").path(),
" ".join(p.with_prefix("build").path() for ps in post for p in graph[ps]),
))
f.write("\n")
def print_common():
common = """\
# Autogenerated by TeXMake
TEXFLAGS = --halt-on-error --interaction=nonstopmode
TEXC = pdflatex $(TEXFLAGS)
compile:
cd $(DIR) && $(TEXC) $(FILE) | \\
grep -Ev 'texmf-dist|\.code\.tex|\.dict|^[^(]*\)' | \\
sed '/^[[:space:]]*$$/d'
build/%.tex: src/%.tex
mkdir -p $$(dirname $@)
cp $< $@
# Reroot
ROOT="$$(realpath build)"; \\
HERE="$$(dirname $$(realpath $@))"; \\
echo "ROOT=$${ROOT} HERE=$${HERE}"; \\
sed -Ei \\
-e 's,(\\\\(input|include|includegraphics).*\\{)\\$$\\(ROOT\\),\\1'"$${ROOT}"',g;'\\
-e 's,(\\\\(input|include|includegraphics).*\\{)\\$$\\(HERE\\),\\1'"$${HERE}"',g;'\\
$@
build/%: src/%
mkdir -p $$(dirname $@)
cp $< $@
build/%.pdf: build/%.tex
make DIR=$$(dirname $@) FILE=$$(basename $<) compile
.PHONY: compile
"""
with open("common.tex.mk", 'w') as f:
f.write(common.replace(" ", "\t"))
def print_init():
init = """\
TWICE = true
DOC =
all: $(DOC:%=%.pdf)
doc_%.tex.mk: cfg_%.tmk ../texmake.py
../texmake.py name=$* fail=WARNING
common.tex.mk: ../texmake.py
../texmake.py common
-include $(DOC:%=doc_%.tex.mk)
-include common.tex.mk
clean:
rm -rf build
rm *.tex.mk
rm $(DOC:%=%.pdf)
"""
with open("Makefile", 'w') as f:
f.write(init.replace(" ", "\t"))
class Args:
def __init__(self, args):
self.fail = None
self.name = None
Err.in_file("<cmdline>")
self.action = None
for a in args:
Err.count_line(a)
if "=" in a:
(key, value) = a.split("=", 1)
if key == "fail":
value = value.upper()
if value in ["0", "N", "NEVER"]:
self.fail = Err.NEVER
elif value in ["1", "E", "ERROR"]:
self.fail = Err.ERROR
elif value in ["2", "W", "WARNING"]:
self.fail = Err.WARNING
elif value in ["3", "A", "ALWAYS"]:
self.fail = Err.ALWAYS
else:
Err.report(
kind="Argparse: Unknown Fail Level",
msg="fail level is among NEVER,ERROR,WARNING,ALWAYS",
)
elif key == "name":
self.name = value
if self.action is not None:
Err.report(
kind="Argparse: Multiple Actions",
msg="can only perform one action at a time",
)
self.action = "parse"
else:
Err.report(
kind="Argparse: No Keyword",
msg="in key=value '{}={}', key is not assigned".format(key, value),
)
else:
if a == "help":
print("Help unavailable at the moment")
sys.exit(255)
elif a == "common" or a == "init":
if self.action is not None:
Err.report(
kind="Argparse: Multiple Actions",
msg="can only perform one action at a time",
)
self.action = a
else:
Err.report(
kind="Argparse: Unknown Flag",
msg="'{}' is not recognized".format(a),
)
if Err.fatality >= Err.WARNING:
if Err.fatality == Err.NEVER:
sys.exit(0)
sys.exit(1)
if self.fail is None:
self.fail = Err.ERROR
if self.name is not None:
self.src = f"cfg_{self.name}.tmk"
self.dest = f"doc_{self.name}.tex.mk"
# Read file f (in the texmk format) and return a workable descriptor
def parse_cfg(args):
with open(args.src, 'r') as f:
Err.in_file(args.src)
cfg = Cfg()
for line in f.readlines():
cfg.push(line.rstrip())
if Err.fatality >= Err.WARNING:
return None
if args.fail <= Err.fatality:
return None
else:
return cfg
def main(args):
args = Args(args[1:])
if args.action == "common":
print_common()
return
elif args.action == "init":
print_init()
return
if os.path.isfile(args.dest):
os.remove(args.dest)
elif os.path.isdir(args.dest):
Err.report(
kind="Argparse: Destination is a Directory",
msg="not going to remove '{}', it's probably a mistake".format(args.dest),
)
if args.fail == Err.NEVER:
sys.exit(0)
sys.exit(1)
if not os.path.isfile(args.src):
Err.report(
kind="Argparse: Source not Found",
msg="'{}' cannot be read: it may be a directory or missing".format(args.src),
)
if args.fail == Err.NEVER:
sys.exit(0)
sys.exit(1)
cfg = parse_cfg(args)
if cfg is not None:
cfg.print(args)
elif args.fail <= Err.fatality:
if args.fail == Err.NEVER:
sys.exit(0)
sys.exit(2)
if __name__ == "__main__":
main(sys.argv)
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment