summaryrefslogtreecommitdiff
path: root/Master
diff options
context:
space:
mode:
Diffstat (limited to 'Master')
l---------Master/bin/aarch64-linux/cllualatex1
l---------Master/bin/aarch64-linux/cluttex1
l---------Master/bin/aarch64-linux/clxelatex1
l---------Master/bin/amd64-freebsd/cllualatex1
l---------Master/bin/amd64-freebsd/cluttex1
l---------Master/bin/amd64-freebsd/clxelatex1
l---------Master/bin/amd64-netbsd/cllualatex1
l---------Master/bin/amd64-netbsd/cluttex1
l---------Master/bin/amd64-netbsd/clxelatex1
l---------Master/bin/armhf-linux/cllualatex1
l---------Master/bin/armhf-linux/cluttex1
l---------Master/bin/armhf-linux/clxelatex1
l---------Master/bin/i386-cygwin/cllualatex1
l---------Master/bin/i386-cygwin/cluttex1
l---------Master/bin/i386-cygwin/clxelatex1
l---------Master/bin/i386-freebsd/cllualatex1
l---------Master/bin/i386-freebsd/cluttex1
l---------Master/bin/i386-freebsd/clxelatex1
l---------Master/bin/i386-linux/cllualatex1
l---------Master/bin/i386-linux/cluttex1
l---------Master/bin/i386-linux/clxelatex1
l---------Master/bin/i386-netbsd/cllualatex1
l---------Master/bin/i386-netbsd/cluttex1
l---------Master/bin/i386-netbsd/clxelatex1
l---------Master/bin/i386-solaris/cllualatex1
l---------Master/bin/i386-solaris/cluttex1
l---------Master/bin/i386-solaris/clxelatex1
l---------Master/bin/sparc-solaris/cllualatex1
l---------Master/bin/sparc-solaris/cluttex1
l---------Master/bin/sparc-solaris/clxelatex1
-rwxr-xr-xMaster/bin/win32/cllualatex.bat2598
-rwxr-xr-xMaster/bin/win32/cluttex.bat2598
-rwxr-xr-xMaster/bin/win32/clxelatex.bat2598
l---------Master/bin/x86_64-cygwin/cllualatex1
l---------Master/bin/x86_64-cygwin/cluttex1
l---------Master/bin/x86_64-cygwin/clxelatex1
l---------Master/bin/x86_64-darwin/cllualatex1
l---------Master/bin/x86_64-darwin/cluttex1
l---------Master/bin/x86_64-darwin/clxelatex1
l---------Master/bin/x86_64-darwinlegacy/cllualatex1
l---------Master/bin/x86_64-darwinlegacy/cluttex1
l---------Master/bin/x86_64-darwinlegacy/clxelatex1
l---------Master/bin/x86_64-linux/cllualatex1
l---------Master/bin/x86_64-linux/cluttex1
l---------Master/bin/x86_64-linux/clxelatex1
l---------Master/bin/x86_64-linuxmusl/cllualatex1
l---------Master/bin/x86_64-linuxmusl/cluttex1
l---------Master/bin/x86_64-linuxmusl/clxelatex1
l---------Master/bin/x86_64-solaris/cllualatex1
l---------Master/bin/x86_64-solaris/cluttex1
l---------Master/bin/x86_64-solaris/clxelatex1
-rw-r--r--Master/texmf-dist/doc/support/cluttex/CHANGELOG.md13
-rw-r--r--Master/texmf-dist/doc/support/cluttex/COPYING674
-rw-r--r--Master/texmf-dist/doc/support/cluttex/Makefile37
-rw-r--r--Master/texmf-dist/doc/support/cluttex/README.md122
-rw-r--r--Master/texmf-dist/doc/support/cluttex/build.lua162
-rw-r--r--Master/texmf-dist/doc/support/cluttex/checkglobal.lua69
-rw-r--r--Master/texmf-dist/doc/support/cluttex/example/README.md68
-rw-r--r--Master/texmf-dist/doc/support/cluttex/example/biblatex/foo.bib14
-rw-r--r--Master/texmf-dist/doc/support/cluttex/example/biblatex/main.tex8
-rw-r--r--Master/texmf-dist/doc/support/cluttex/example/biblatex/sub.tex1
-rw-r--r--Master/texmf-dist/doc/support/cluttex/example/bibtex/baz.bib7
-rw-r--r--Master/texmf-dist/doc/support/cluttex/example/bibtex/foo.bib7
-rw-r--r--Master/texmf-dist/doc/support/cluttex/example/bibtex/main.tex7
-rw-r--r--Master/texmf-dist/doc/support/cluttex/example/bibtex/sub.tex1
-rw-r--r--Master/texmf-dist/doc/support/cluttex/example/empty/main.tex3
-rw-r--r--Master/texmf-dist/doc/support/cluttex/example/epstopdf/drawing.eps99
-rw-r--r--Master/texmf-dist/doc/support/cluttex/example/epstopdf/fig/drawing2.eps99
-rw-r--r--Master/texmf-dist/doc/support/cluttex/example/epstopdf/main.tex9
-rw-r--r--Master/texmf-dist/doc/support/cluttex/example/include/main.tex12
-rw-r--r--Master/texmf-dist/doc/support/cluttex/example/include/path/to/file.tex3
-rw-r--r--Master/texmf-dist/doc/support/cluttex/example/include/sub.tex3
-rw-r--r--Master/texmf-dist/doc/support/cluttex/example/include/sub2.tex3
-rw-r--r--Master/texmf-dist/doc/support/cluttex/example/makeindex/main.tex15
-rw-r--r--Master/texmf-dist/doc/support/cluttex/example/minted/file.lua3
-rw-r--r--Master/texmf-dist/doc/support/cluttex/example/minted/main.tex11
-rw-r--r--Master/texmf-dist/doc/support/cluttex/example/simple-ja/main-luatexja.tex6
-rw-r--r--Master/texmf-dist/doc/support/cluttex/example/simple-ja/main-platex.tex5
-rw-r--r--Master/texmf-dist/doc/support/cluttex/example/simple/main.tex4
-rw-r--r--Master/texmf-dist/doc/support/cluttex/src/cluttex.lua517
-rw-r--r--Master/texmf-dist/doc/support/cluttex/src/texrunner/auxfile.lua71
-rw-r--r--Master/texmf-dist/doc/support/cluttex/src/texrunner/fsutil.lua83
-rw-r--r--Master/texmf-dist/doc/support/cluttex/src/texrunner/handleoption.lua416
-rw-r--r--Master/texmf-dist/doc/support/cluttex/src/texrunner/isatty.lua179
-rw-r--r--Master/texmf-dist/doc/support/cluttex/src/texrunner/luatexinit.lua99
-rw-r--r--Master/texmf-dist/doc/support/cluttex/src/texrunner/message.lua135
-rw-r--r--Master/texmf-dist/doc/support/cluttex/src/texrunner/option.lua149
-rw-r--r--Master/texmf-dist/doc/support/cluttex/src/texrunner/pathutil.lua35
-rw-r--r--Master/texmf-dist/doc/support/cluttex/src/texrunner/pathutil_unix.lua214
-rw-r--r--Master/texmf-dist/doc/support/cluttex/src/texrunner/pathutil_windows.lua222
-rw-r--r--Master/texmf-dist/doc/support/cluttex/src/texrunner/recovery.lua86
-rw-r--r--Master/texmf-dist/doc/support/cluttex/src/texrunner/reruncheck.lua250
-rw-r--r--Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil.lua26
-rw-r--r--Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_unix.lua62
-rw-r--r--Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_windows.lua35
-rw-r--r--Master/texmf-dist/doc/support/cluttex/src/texrunner/tex_engine.lua159
-rwxr-xr-xMaster/texmf-dist/scripts/cluttex/cluttex.lua2595
-rwxr-xr-xMaster/tlpkg/bin/tlpkg-ctan-check2
-rwxr-xr-xMaster/tlpkg/libexec/ctan2tds36
-rw-r--r--Master/tlpkg/tlpsrc/cluttex.tlpsrc3
-rw-r--r--Master/tlpkg/tlpsrc/collection-binextra.tlpsrc1
101 files changed, 14674 insertions, 8 deletions
diff --git a/Master/bin/aarch64-linux/cllualatex b/Master/bin/aarch64-linux/cllualatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/aarch64-linux/cllualatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/aarch64-linux/cluttex b/Master/bin/aarch64-linux/cluttex
new file mode 120000
index 00000000000..1061de93aae
--- /dev/null
+++ b/Master/bin/aarch64-linux/cluttex
@@ -0,0 +1 @@
+../../texmf-dist/scripts/cluttex/cluttex.lua \ No newline at end of file
diff --git a/Master/bin/aarch64-linux/clxelatex b/Master/bin/aarch64-linux/clxelatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/aarch64-linux/clxelatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/amd64-freebsd/cllualatex b/Master/bin/amd64-freebsd/cllualatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/amd64-freebsd/cllualatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/amd64-freebsd/cluttex b/Master/bin/amd64-freebsd/cluttex
new file mode 120000
index 00000000000..1061de93aae
--- /dev/null
+++ b/Master/bin/amd64-freebsd/cluttex
@@ -0,0 +1 @@
+../../texmf-dist/scripts/cluttex/cluttex.lua \ No newline at end of file
diff --git a/Master/bin/amd64-freebsd/clxelatex b/Master/bin/amd64-freebsd/clxelatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/amd64-freebsd/clxelatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/amd64-netbsd/cllualatex b/Master/bin/amd64-netbsd/cllualatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/amd64-netbsd/cllualatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/amd64-netbsd/cluttex b/Master/bin/amd64-netbsd/cluttex
new file mode 120000
index 00000000000..1061de93aae
--- /dev/null
+++ b/Master/bin/amd64-netbsd/cluttex
@@ -0,0 +1 @@
+../../texmf-dist/scripts/cluttex/cluttex.lua \ No newline at end of file
diff --git a/Master/bin/amd64-netbsd/clxelatex b/Master/bin/amd64-netbsd/clxelatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/amd64-netbsd/clxelatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/armhf-linux/cllualatex b/Master/bin/armhf-linux/cllualatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/armhf-linux/cllualatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/armhf-linux/cluttex b/Master/bin/armhf-linux/cluttex
new file mode 120000
index 00000000000..1061de93aae
--- /dev/null
+++ b/Master/bin/armhf-linux/cluttex
@@ -0,0 +1 @@
+../../texmf-dist/scripts/cluttex/cluttex.lua \ No newline at end of file
diff --git a/Master/bin/armhf-linux/clxelatex b/Master/bin/armhf-linux/clxelatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/armhf-linux/clxelatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/i386-cygwin/cllualatex b/Master/bin/i386-cygwin/cllualatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/i386-cygwin/cllualatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/i386-cygwin/cluttex b/Master/bin/i386-cygwin/cluttex
new file mode 120000
index 00000000000..1061de93aae
--- /dev/null
+++ b/Master/bin/i386-cygwin/cluttex
@@ -0,0 +1 @@
+../../texmf-dist/scripts/cluttex/cluttex.lua \ No newline at end of file
diff --git a/Master/bin/i386-cygwin/clxelatex b/Master/bin/i386-cygwin/clxelatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/i386-cygwin/clxelatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/i386-freebsd/cllualatex b/Master/bin/i386-freebsd/cllualatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/i386-freebsd/cllualatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/i386-freebsd/cluttex b/Master/bin/i386-freebsd/cluttex
new file mode 120000
index 00000000000..1061de93aae
--- /dev/null
+++ b/Master/bin/i386-freebsd/cluttex
@@ -0,0 +1 @@
+../../texmf-dist/scripts/cluttex/cluttex.lua \ No newline at end of file
diff --git a/Master/bin/i386-freebsd/clxelatex b/Master/bin/i386-freebsd/clxelatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/i386-freebsd/clxelatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/i386-linux/cllualatex b/Master/bin/i386-linux/cllualatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/i386-linux/cllualatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/i386-linux/cluttex b/Master/bin/i386-linux/cluttex
new file mode 120000
index 00000000000..1061de93aae
--- /dev/null
+++ b/Master/bin/i386-linux/cluttex
@@ -0,0 +1 @@
+../../texmf-dist/scripts/cluttex/cluttex.lua \ No newline at end of file
diff --git a/Master/bin/i386-linux/clxelatex b/Master/bin/i386-linux/clxelatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/i386-linux/clxelatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/i386-netbsd/cllualatex b/Master/bin/i386-netbsd/cllualatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/i386-netbsd/cllualatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/i386-netbsd/cluttex b/Master/bin/i386-netbsd/cluttex
new file mode 120000
index 00000000000..1061de93aae
--- /dev/null
+++ b/Master/bin/i386-netbsd/cluttex
@@ -0,0 +1 @@
+../../texmf-dist/scripts/cluttex/cluttex.lua \ No newline at end of file
diff --git a/Master/bin/i386-netbsd/clxelatex b/Master/bin/i386-netbsd/clxelatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/i386-netbsd/clxelatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/i386-solaris/cllualatex b/Master/bin/i386-solaris/cllualatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/i386-solaris/cllualatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/i386-solaris/cluttex b/Master/bin/i386-solaris/cluttex
new file mode 120000
index 00000000000..1061de93aae
--- /dev/null
+++ b/Master/bin/i386-solaris/cluttex
@@ -0,0 +1 @@
+../../texmf-dist/scripts/cluttex/cluttex.lua \ No newline at end of file
diff --git a/Master/bin/i386-solaris/clxelatex b/Master/bin/i386-solaris/clxelatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/i386-solaris/clxelatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/sparc-solaris/cllualatex b/Master/bin/sparc-solaris/cllualatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/sparc-solaris/cllualatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/sparc-solaris/cluttex b/Master/bin/sparc-solaris/cluttex
new file mode 120000
index 00000000000..1061de93aae
--- /dev/null
+++ b/Master/bin/sparc-solaris/cluttex
@@ -0,0 +1 @@
+../../texmf-dist/scripts/cluttex/cluttex.lua \ No newline at end of file
diff --git a/Master/bin/sparc-solaris/clxelatex b/Master/bin/sparc-solaris/clxelatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/sparc-solaris/clxelatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/win32/cllualatex.bat b/Master/bin/win32/cllualatex.bat
new file mode 100755
index 00000000000..6b938411f9d
--- /dev/null
+++ b/Master/bin/win32/cllualatex.bat
@@ -0,0 +1,2598 @@
+::dummy:: --[[
+@texlua "%~f0" %*
+@goto :eof
+]]
+local io, os, string, table, package, require, assert, error, ipairs, type, select, arg = io, os, string, table, package, require, assert, error, ipairs, type, select, arg
+local CLUTTEX_VERBOSITY, CLUTTEX_VERSION
+os.type = os.type or "windows"
+if lfs and not package.loaded['lfs'] then package.loaded['lfs'] = lfs end
+if os.type == "windows" then
+package.preload["texrunner.pathutil"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+-- pathutil module
+
+local assert = assert
+local select = select
+local string = string
+local string_find = string.find
+local string_sub = string.sub
+local string_match = string.match
+local string_gsub = string.gsub
+local filesys = require "lfs"
+
+local function basename(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "[\\/]", i + 1)
+ if j == nil then
+ return string_sub(path, i + 1)
+ elseif j == #path then
+ return string_sub(path, i + 1, -2)
+ end
+ i = j
+ end
+end
+
+
+local function dirname(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "[\\/]", i + 1)
+ if j == nil then
+ if i == 0 then
+ -- No directory portion
+ return "."
+ elseif i == 1 then
+ -- Root
+ return string_sub(path, 1, 1)
+ else
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ end
+ i = j
+ end
+end
+
+
+local function parentdir(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "[\\/]", i + 1)
+ if j == nil then
+ if i == 0 then
+ -- No directory portion
+ return "."
+ elseif i == 1 then
+ -- Root
+ return string_sub(path, 1, 1)
+ else
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ elseif j == #path then
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ i = j
+ end
+end
+
+
+local function trimext(path)
+ return (string_gsub(path, "%.[^\\/%.]*$", ""))
+end
+
+
+local function ext(path)
+ return string_match(path, "%.([^\\/%.]*)$") or ""
+end
+
+
+local function replaceext(path, newext)
+ local newpath, n = string_gsub(path, "%.([^\\/%.]*)$", function() return "." .. newext end)
+ if n == 0 then
+ return newpath .. "." .. newext
+ else
+ return newpath
+ end
+end
+
+
+local function joinpath2(x, y)
+ local xd = x
+ local last = string_sub(x, -1)
+ if last ~= "/" and last ~= "\\" then
+ xd = x .. "\\"
+ end
+ if y == "." then
+ return xd
+ elseif y == ".." then
+ return dirname(x)
+ else
+ if string_match(y, "^%.[\\/]") then
+ return xd .. string_sub(y, 3)
+ else
+ return xd .. y
+ end
+ end
+end
+
+local function joinpath(...)
+ local n = select("#", ...)
+ if n == 2 then
+ return joinpath2(...)
+ elseif n == 0 then
+ return "."
+ elseif n == 1 then
+ return ...
+ else
+ return joinpath(joinpath2(...), select(3, ...))
+ end
+end
+
+
+-- https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
+local function isabspath(path)
+ local init = string_sub(path, 1, 1)
+ return init == "\\" or init == "/" or string_match(path, "^%a:[/\\]")
+end
+
+local function abspath(path, cwd)
+ if isabspath(path) then
+ -- absolute path
+ return path
+ else
+ -- TODO: relative path with a drive letter is not supported
+ cwd = cwd or filesys.currentdir()
+ return joinpath2(cwd, path)
+ end
+end
+
+return {
+ basename = basename,
+ dirname = dirname,
+ parentdir = parentdir,
+ trimext = trimext,
+ ext = ext,
+ replaceext = replaceext,
+ join = joinpath,
+ abspath = abspath,
+}
+end
+else
+package.preload["texrunner.pathutil"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+-- pathutil module for *nix
+
+local assert = assert
+local select = select
+local string = string
+local string_find = string.find
+local string_sub = string.sub
+local string_match = string.match
+local string_gsub = string.gsub
+local filesys = require "lfs"
+
+local function basename(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "/", i + 1, true)
+ if j == nil then
+ return string_sub(path, i + 1)
+ elseif j == #path then
+ return string_sub(path, i + 1, -2)
+ end
+ i = j
+ end
+end
+
+
+local function dirname(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "/", i + 1, true)
+ if j == nil then
+ if i == 0 then
+ -- No directory portion
+ return "."
+ elseif i == 1 then
+ -- Root
+ return "/"
+ else
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ end
+ i = j
+ end
+end
+
+
+local function parentdir(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "/", i + 1, true)
+ if j == nil then
+ if i == 0 then
+ -- No directory portion
+ return "."
+ elseif i == 1 then
+ -- Root
+ return "/"
+ else
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ elseif j == #path then
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ i = j
+ end
+end
+
+
+local function trimext(path)
+ return (string_gsub(path, "%.[^/%.]*$", ""))
+end
+
+
+local function ext(path)
+ return string_match(path, "%.([^/%.]*)$") or ""
+end
+
+
+local function replaceext(path, newext)
+ local newpath, n = string_gsub(path, "%.([^/%.]*)$", function() return "." .. newext end)
+ if n == 0 then
+ return newpath .. "." .. newext
+ else
+ return newpath
+ end
+end
+
+
+local function joinpath2(x, y)
+ local xd = x
+ if string_sub(x, -1) ~= "/" then
+ xd = x .. "/"
+ end
+ if y == "." then
+ return xd
+ elseif y == ".." then
+ return dirname(x)
+ else
+ if string_sub(y, 1, 2) == "./" then
+ return xd .. string_sub(y, 3)
+ else
+ return xd .. y
+ end
+ end
+end
+
+local function joinpath(...)
+ local n = select("#", ...)
+ if n == 2 then
+ return joinpath2(...)
+ elseif n == 0 then
+ return "."
+ elseif n == 1 then
+ return ...
+ else
+ return joinpath(joinpath2(...), select(3, ...))
+ end
+end
+
+
+local function abspath(path, cwd)
+ if string_sub(path, 1, 1) == "/" then
+ -- absolute path
+ return path
+ else
+ cwd = cwd or filesys.currentdir()
+ return joinpath2(cwd, path)
+ end
+end
+
+
+return {
+ basename = basename,
+ dirname = dirname,
+ parentdir = parentdir,
+ trimext = trimext,
+ ext = ext,
+ replaceext = replaceext,
+ join = joinpath,
+ abspath = abspath,
+}
+end
+end
+if os.type == "windows" then
+package.preload["texrunner.shellutil"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local string_gsub = string.gsub
+
+-- s: string
+local function escape(s)
+ return '"' .. string_gsub(string_gsub(s, '(\\*)"', '%1%1\\"'), '(\\+)$', '%1%1') .. '"'
+end
+
+
+return {
+ escape = escape,
+}
+end
+else
+package.preload["texrunner.shellutil"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local assert = assert
+local string_match = string.match
+local table = table
+local table_insert = table.insert
+local table_concat = table.concat
+
+-- s: string
+local function escape(s)
+ local len = #s
+ local result = {}
+ local t,i = string_match(s, "^([^']*)()")
+ assert(t)
+ if t ~= "" then
+ table_insert(result, "'")
+ table_insert(result, t)
+ table_insert(result, "'")
+ end
+ while i < len do
+ t,i = string_match(s, "^('+)()", i)
+ assert(t)
+ table_insert(result, '"')
+ table_insert(result, t)
+ table_insert(result, '"')
+ t,i = string_match(s, "^([^']*)()", i)
+ assert(t)
+ if t ~= "" then
+ table_insert(result, "'")
+ table_insert(result, t)
+ table_insert(result, "'")
+ end
+ end
+ return table_concat(result, "")
+end
+
+
+return {
+ escape = escape,
+}
+end
+end
+package.preload["texrunner.fsutil"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local assert = assert
+local os = os
+local os_execute = os.execute
+local os_remove = os.remove
+local filesys = require "lfs"
+local pathutil = require "texrunner.pathutil"
+local shellutil = require "texrunner.shellutil"
+local escape = shellutil.escape
+
+local copy_command
+if os.type == "windows" then
+ function copy_command(from, to)
+ -- TODO: What if `from` begins with a slash?
+ return "copy " .. escape(from) .. " " .. escape(to) .. " > NUL"
+ end
+else
+ function copy_command(from, to)
+ -- TODO: What if `from` begins with a hypen?
+ return "cp " .. escape(from) .. " " .. escape(to)
+ end
+end
+
+local isfile = filesys.isfile or function(path)
+ return filesys.attributes(path, "mode") == "file"
+end
+
+local isdir = filesys.isdir or function(path)
+ return filesys.attributes(path, "mode") == "directory"
+end
+
+local function mkdir_rec(path)
+ local succ, err = filesys.mkdir(path)
+ if not succ then
+ succ, err = mkdir_rec(pathutil.parentdir(path))
+ if succ then
+ return filesys.mkdir(path)
+ end
+ end
+ return succ, err
+end
+
+local function remove_rec(path)
+ if isdir(path) then
+ for file in filesys.dir(path) do
+ if file ~= "." and file ~= ".." then
+ local succ, err = remove_rec(pathutil.join(path, file))
+ if not succ then
+ return succ, err
+ end
+ end
+ end
+ return filesys.rmdir(path)
+ else
+ return os_remove(path)
+ end
+end
+
+return {
+ copy_command = copy_command,
+ isfile = isfile,
+ isdir = isdir,
+ mkdir_rec = mkdir_rec,
+ remove_rec = remove_rec,
+}
+end
+package.preload["texrunner.option"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+-- options_and_params, i = parseoption(arg, options)
+-- options[i] = {short = "o", long = "option" [, param = true] [, boolean = true] [, allow_single_hyphen = false]}
+-- arg[i], arg[i + 1], ..., arg[#arg] are non-options
+local function parseoption(arg, options)
+ local i = 1
+ local option_and_params = {}
+ while i <= #arg do
+ if arg[i] == "--" then
+ -- Stop handling options
+ i = i + 1
+ break
+ elseif arg[i]:sub(1,2) == "--" then
+ -- Long option
+ local name,param = arg[i]:match("^([^=]+)=(.*)$", 3)
+ name = name or arg[i]:sub(3)
+ local opt = nil
+ for _,o in ipairs(options) do
+ if o.long then
+ if o.long == name then
+ if o.param then
+ if param then
+ -- --option=param
+ else
+ if o.default ~= nil then
+ param = o.default
+ else
+ -- --option param
+ assert(i + 1 <= #arg, "argument missing after " .. arg[i] .. " option")
+ param = arg[i + 1]
+ i = i + 1
+ end
+ end
+ else
+ -- --option
+ param = true
+ end
+ opt = o
+ break
+ elseif o.boolean and name == "no-" .. o.long then
+ -- --no-option
+ opt = o
+ break
+ end
+ end
+ end
+ if opt then
+ table.insert(option_and_params, {opt.long, param})
+ else
+ -- Unknown long option
+ error("unknown long option: " .. arg[i])
+ end
+ elseif arg[i]:sub(1,1) == "-" then
+ local name,param = arg[i]:match("^([^=]+)=(.*)$", 2)
+ name = name or arg[i]:sub(2)
+ local opt = nil
+ for _,o in ipairs(options) do
+ if o.long and o.allow_single_hyphen then
+ if o.long == name then
+ if o.param then
+ if param then
+ -- -option=param
+ else
+ if o.default ~= nil then
+ param = o.default
+ else
+ -- -option param
+ assert(i + 1 <= #arg, "argument missing after " .. arg[i] .. " option")
+ param = arg[i + 1]
+ i = i + 1
+ end
+ end
+ else
+ -- -option
+ param = true
+ end
+ opt = o
+ break
+ elseif o.boolean and name == "no-" .. o.long then
+ -- -no-option
+ opt = o
+ break
+ end
+ elseif o.long and #name >= 2 and (o.long == name or (o.boolean and name == "no-" .. o.long)) then
+ error("You must supply two hyphens (i.e. --" .. name .. ") for long option")
+ end
+ end
+ if opt == nil then
+ -- Short option
+ name = arg[i]:sub(2,2)
+ for _,o in ipairs(options) do
+ if o.short then
+ if o.short == name then
+ if o.param then
+ if #arg[i] > 2 then
+ -- -oparam
+ param = arg[i]:sub(3)
+ else
+ -- -o param
+ assert(i + 1 <= #arg, "argument missing after " .. arg[i] .. " option")
+ param = arg[i + 1]
+ i = i + 1
+ end
+ else
+ -- -o
+ assert(#arg[i] == 2, "combining multiple short options like -abc is not supported")
+ param = true
+ end
+ opt = o
+ break
+ end
+ end
+ end
+ end
+ if opt then
+ table.insert(option_and_params, {opt.long or opt.short, param})
+ else
+ error("unknown short option: " .. arg[i])
+ end
+ else
+ -- arg[i] is not an option
+ break
+ end
+ i = i + 1
+ end
+ return option_and_params, i
+end
+
+return {
+ parseoption = parseoption;
+}
+end
+package.preload["texrunner.tex_engine"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local table = table
+local setmetatable = setmetatable
+local ipairs = ipairs
+
+local shellutil = require "texrunner.shellutil"
+
+--[[
+engine.name: string
+engine.type = "onePass" or "twoPass"
+engine:build_command(inputfile, options)
+ options:
+ halt_on_error: boolean
+ interaction: string
+ file_line_error: boolean
+ synctex: string
+ shell_escape: boolean
+ shell_restricted: boolean
+ jobname: string
+ output_directory: string
+ extraoptions: a list of strings
+ output_format: "pdf" or "dvi"
+ draftmode: boolean (pdfTeX / XeTeX / LuaTeX)
+ fmt: string
+ tex_injection: string
+ lua_initialization_script: string (LuaTeX only)
+engine.executable: string
+engine.supports_pdf_generation: boolean
+engine.dvi_extension: string
+engine.supports_draftmode: boolean
+engine.is_luatex: true or nil
+]]
+
+local engine_meta = {}
+engine_meta.__index = engine_meta
+engine_meta.dvi_extension = "dvi"
+function engine_meta:build_command(inputfile, options)
+ local command = {self.executable, "-recorder"}
+ if options.fmt then
+ table.insert(command, "-fmt=" .. options.fmt)
+ end
+ if options.halt_on_error then
+ table.insert(command, "-halt-on-error")
+ end
+ if options.interaction then
+ table.insert(command, "-interaction=" .. options.interaction)
+ end
+ if options.file_line_error then
+ table.insert(command, "-file-line-error")
+ end
+ if options.synctex then
+ table.insert(command, "-synctex=" .. shellutil.escape(options.synctex))
+ end
+ if options.shell_escape == false then
+ table.insert(command, "-no-shell-escape")
+ elseif options.shell_restricted == true then
+ table.insert(command, "-shell-restricted")
+ elseif options.shell_escape == true then
+ table.insert(command, "-shell-escape")
+ end
+ if options.jobname then
+ table.insert(command, "-jobname=" .. shellutil.escape(options.jobname))
+ end
+ if options.output_directory then
+ table.insert(command, "-output-directory=" .. shellutil.escape(options.output_directory))
+ end
+ if self.handle_additional_options then
+ self:handle_additional_options(command, options)
+ end
+ if options.extraoptions then
+ for _,v in ipairs(options.extraoptions) do
+ table.insert(command, v)
+ end
+ end
+ if type(options.tex_injection) == "string" then
+ table.insert(command, shellutil.escape(options.tex_injection .. "\\input " .. inputfile)) -- TODO: what if filename contains spaces?
+ else
+ table.insert(command, shellutil.escape(inputfile))
+ end
+ return table.concat(command, " ")
+end
+
+local function engine(name, supports_pdf_generation, handle_additional_options)
+ return setmetatable({
+ name = name,
+ executable = name,
+ supports_pdf_generation = supports_pdf_generation,
+ handle_additional_options = handle_additional_options,
+ supports_draftmode = supports_pdf_generation,
+ }, engine_meta)
+end
+
+local function handle_pdftex_options(self, args, options)
+ if options.draftmode then
+ table.insert(args, "-draftmode")
+ elseif options.output_format == "dvi" then
+ table.insert(args, "-output-format=dvi")
+ end
+end
+
+local function handle_xetex_options(self, args, options)
+ if options.output_format == "dvi" or options.draftmode then
+ table.insert(args, "-no-pdf")
+ end
+end
+
+local function handle_luatex_options(self, args, options)
+ if options.lua_initialization_script then
+ table.insert(args, "--lua="..shellutil.escape(options.lua_initialization_script))
+ end
+ handle_pdftex_options(self, args, options)
+end
+
+local function is_luatex(e)
+ e.is_luatex = true
+ return e
+end
+
+local KnownEngines = {
+ ["pdftex"] = engine("pdftex", true, handle_pdftex_options),
+ ["pdflatex"] = engine("pdflatex", true, handle_pdftex_options),
+ ["luatex"] = is_luatex(engine("luatex", true, handle_luatex_options)),
+ ["lualatex"] = is_luatex(engine("lualatex", true, handle_luatex_options)),
+ ["luajittex"] = is_luatex(engine("luajittex", true, handle_luatex_options)),
+ ["xetex"] = engine("xetex", true, handle_xetex_options),
+ ["xelatex"] = engine("xelatex", true, handle_xetex_options),
+ ["tex"] = engine("tex", false),
+ ["etex"] = engine("etex", false),
+ ["latex"] = engine("latex", false),
+ ["ptex"] = engine("ptex", false),
+ ["eptex"] = engine("eptex", false),
+ ["platex"] = engine("platex", false),
+ ["uptex"] = engine("uptex", false),
+ ["euptex"] = engine("euptex", false),
+ ["uplatex"] = engine("uplatex", false),
+}
+
+KnownEngines["xetex"].dvi_extension = "xdv"
+KnownEngines["xelatex"].dvi_extension = "xdv"
+
+return KnownEngines
+end
+package.preload["texrunner.reruncheck"] = function(...)
+--[[
+ Copyright 2016,2018 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local io = io
+local assert = assert
+local filesys = require "lfs"
+local md5 = require "md5"
+local fsutil = require "texrunner.fsutil"
+local pathutil = require "texrunner.pathutil"
+local message = require "texrunner.message"
+
+local function md5sum_file(path)
+ local f = assert(io.open(path, "rb"))
+ local contents = f:read("*a")
+ f:close()
+ return md5.sum(contents)
+end
+
+-- filelist, filemap = parse_recorder_file("jobname.fls", options [, filelist, filemap])
+-- filelist[i] = {path = "...", abspath = "...", kind = "input" or "output" or "auxiliary"}
+local function parse_recorder_file(file, options, filelist, filemap)
+ filelist = filelist or {}
+ filemap = filemap or {}
+ for l in io.lines(file) do
+ local t,path = l:match("^(%w+) (.*)$")
+ if t == "PWD" then
+ -- Ignore
+
+ elseif t == "INPUT" then
+ local abspath = pathutil.abspath(path)
+ local fileinfo = filemap[abspath]
+ if not fileinfo then
+ if fsutil.isfile(path) then
+ local kind = "input"
+ local ext = pathutil.ext(path)
+ if ext == "bbl" then
+ kind = "auxiliary"
+ end
+ fileinfo = {path = path, abspath = abspath, kind = kind}
+ table.insert(filelist, fileinfo)
+ filemap[abspath] = fileinfo
+ else
+ -- Maybe a command execution
+ end
+ else
+ if #path < #fileinfo.path then
+ fileinfo.path = path
+ end
+ if fileinfo.kind == "output" then
+ -- The files listed in both INPUT and OUTPUT are considered to be auxiliary files.
+ fileinfo.kind = "auxiliary"
+ end
+ end
+
+ elseif t == "OUTPUT" then
+ local abspath = pathutil.abspath(path)
+ local fileinfo = filemap[abspath]
+ if not fileinfo then
+ local kind = "output"
+ local ext = pathutil.ext(path)
+ if ext == "out" then
+ -- hyperref bookmarks file
+ kind = "auxiliary"
+ elseif options.makeindex and ext == "idx" then
+ -- Treat .idx files (to be processed by MakeIndex) as auxiliary
+ kind = "auxiliary"
+ -- ...and .ind files
+ elseif ext == "bcf" then -- biber
+ kind = "auxiliary"
+ elseif ext == "glo" then -- makeglossaries
+ kind = "auxiliary"
+ end
+ fileinfo = {path = path, abspath = abspath, kind = kind}
+ table.insert(filelist, fileinfo)
+ filemap[abspath] = fileinfo
+ else
+ if #path < #fileinfo.path then
+ fileinfo.path = path
+ end
+ if fileinfo.kind == "input" then
+ -- The files listed in both INPUT and OUTPUT are considered to be auxiliary files.
+ fileinfo.kind = "auxiliary"
+ end
+ end
+
+ else
+ message.warning("Unrecognized line in recorder file '", file, "': ", l)
+ end
+ end
+ return filelist, filemap
+end
+
+-- auxstatus = collectfileinfo(filelist [, auxstatus])
+local function collectfileinfo(filelist, auxstatus)
+ auxstatus = auxstatus or {}
+ for i,fileinfo in ipairs(filelist) do
+ local path = fileinfo.abspath
+ if fsutil.isfile(path) then
+ local status = auxstatus[path] or {}
+ auxstatus[path] = status
+ if fileinfo.kind == "input" then
+ status.mtime = status.mtime or filesys.attributes(path, "modification")
+ elseif fileinfo.kind == "auxiliary" then
+ status.mtime = status.mtime or filesys.attributes(path, "modification")
+ status.size = status.size or filesys.attributes(path, "size")
+ status.md5sum = status.md5sum or md5sum_file(path)
+ end
+ end
+ end
+ return auxstatus
+end
+
+local function binarytohex(s)
+ return (s:gsub(".", function(c) return string.format("%02x", string.byte(c)) end))
+end
+
+-- should_rerun, newauxstatus = comparefileinfo(auxfiles, auxstatus)
+local function comparefileinfo(filelist, auxstatus)
+ local should_rerun = false
+ local newauxstatus = {}
+ for i,fileinfo in ipairs(filelist) do
+ local path = fileinfo.abspath
+ if fsutil.isfile(path) then
+ if fileinfo.kind == "input" then
+ -- Input file: User might have modified while running TeX.
+ local mtime = filesys.attributes(path, "modification")
+ if auxstatus[path] and auxstatus[path].mtime then
+ if auxstatus[path].mtime < mtime then
+ -- Input file was updated during execution
+ message.info("Input file '", fileinfo.path, "' was modified (by user, or some external commands).")
+ newauxstatus[path] = {mtime = mtime}
+ return true, newauxstatus
+ end
+ else
+ -- New input file
+ end
+
+ elseif fileinfo.kind == "auxiliary" then
+ -- Auxiliary file: Compare file contents.
+ if auxstatus[path] then
+ -- File was touched during execution
+ local really_modified = false
+ local modified_because = nil
+ local size = filesys.attributes(path, "size")
+ if auxstatus[path].size ~= size then
+ really_modified = true
+ if auxstatus[path].size then
+ modified_because = string.format("size: %d -> %d", auxstatus[path].size, size)
+ else
+ modified_because = string.format("size: (N/A) -> %d", size)
+ end
+ newauxstatus[path] = {size = size}
+ else
+ local md5sum = md5sum_file(path)
+ if auxstatus[path].md5sum ~= md5sum then
+ really_modified = true
+ if auxstatus[path].md5sum then
+ modified_because = string.format("md5: %s -> %s", binarytohex(auxstatus[path].md5sum), binarytohex(md5sum))
+ else
+ modified_because = string.format("md5: (N/A) -> %s", binarytohex(md5sum))
+ end
+ end
+ newauxstatus[path] = {size = size, md5sum = md5sum}
+ end
+ if really_modified then
+ message.info("File '", fileinfo.path, "' was modified (", modified_because, ").")
+ should_rerun = true
+ else
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("File '", fileinfo.path, "' unmodified (size and md5sum).")
+ end
+ end
+ else
+ -- New file
+ if path:sub(-4) == ".aux" then
+ local size = filesys.attributes(path, "size")
+ if size == 8 then
+ local auxfile = io.open(path, "rb")
+ local contents = auxfile:read("*a")
+ auxfile:close()
+ if contents == "\\relax \n" then
+ -- The .aux file is new, but it is almost empty
+ else
+ should_rerun = true
+ end
+ newauxstatus[path] = {size = size, md5sum = md5.sum(contents)}
+ else
+ should_rerun = true
+ newauxstatus[path] = {size = size}
+ end
+ else
+ should_rerun = true
+ end
+ if should_rerun then
+ message.info("New auxiliary file '", fileinfo.path, "'.")
+ else
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("Ignoring almost-empty auxiliary file '", fileinfo.path, "'.")
+ end
+ end
+ end
+ if should_rerun then
+ break
+ end
+ end
+ else
+ -- Auxiliary file is not really a file???
+ end
+ end
+ return should_rerun, newauxstatus
+end
+
+-- true if src is newer than dst
+local function comparefiletime(srcpath, dstpath, auxstatus)
+ if not filesys.isfile(dstpath) then
+ return true
+ end
+ local src_info = auxstatus[srcpath]
+ if src_info then
+ local src_mtime = src_info.mtime
+ if src_mtime then
+ local dst_mtime = filesys.attributes(dstpath, "modification")
+ return src_mtime > dst_mtime
+ end
+ end
+ return false
+end
+
+return {
+ parse_recorder_file = parse_recorder_file;
+ collectfileinfo = collectfileinfo;
+ comparefileinfo = comparefileinfo;
+ comparefiletime = comparefiletime;
+}
+end
+package.preload["texrunner.auxfile"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local string_match = string.match
+local pathutil = require "texrunner.pathutil"
+local filesys = require "lfs"
+local fsutil = require "texrunner.fsutil"
+local message = require "texrunner.message"
+
+-- for LaTeX
+local function parse_aux_file(auxfile, outdir, report, seen)
+ report = report or {}
+ seen = seen or {}
+ seen[auxfile] = true
+ for l in io.lines(auxfile) do
+ local subauxfile = string_match(l, "\\@input{(.+)}")
+ if subauxfile then
+ if fsutil.isfile(subauxfile) then
+ parse_aux_file(pathutil.join(outdir, subauxfile), outdir, report, seen)
+ else
+ local dir = pathutil.join(outdir, pathutil.dirname(subauxfile))
+ if not fsutil.isdir(dir) then
+ assert(fsutil.mkdir_rec(dir))
+ report.made_new_directory = true
+ end
+ end
+ end
+ end
+ return report
+end
+
+-- \citation, \bibdata, \bibstyle and \@input
+local function extract_bibtex_from_aux_file(auxfile, outdir, biblines)
+ biblines = biblines or {}
+ for l in io.lines(auxfile) do
+ local name = string_match(l, "\\([%a@]+)")
+ if name == "citation" or name == "bibdata" or name == "bibstyle" then
+ table.insert(biblines, l)
+ if CLUTTEX_VERBOSITY >= 2 then
+ message.info("BibTeX line: ", l)
+ end
+ elseif name == "@input" then
+ local subauxfile = string_match(l, "\\@input{(.+)}")
+ if subauxfile and fsutil.isfile(subauxfile) then
+ extract_bibtex_from_aux_file(pathutil.join(outdir, subauxfile), outdir, biblines)
+ end
+ end
+ end
+ return biblines
+end
+
+return {
+ parse_aux_file = parse_aux_file,
+ extract_bibtex_from_aux_file = extract_bibtex_from_aux_file,
+}
+end
+package.preload["texrunner.luatexinit"] = function(...)
+local function create_initialization_script(filename, options)
+ local initscript = assert(io.open(filename,"w"))
+ if type(options.file_line_error) == "boolean" then
+ initscript:write(string.format("texconfig.file_line_error = %s\n", options.file_line_error))
+ end
+ if type(options.halt_on_error) == "boolean" then
+ initscript:write(string.format("texconfig.halt_on_error = %s\n", options.halt_on_error))
+ end
+ initscript:write([==[
+local print = print
+local io_open = io.open
+local io_write = io.write
+local os_execute = os.execute
+local texio_write = texio.write
+local texio_write_nl = texio.write_nl
+]==])
+
+ -- Packages coded in Lua doesn't follow -output-directory option and doesn't write command to the log file
+ initscript:write(string.format("local output_directory = %q\n", options.output_directory))
+ initscript:write([==[
+local luawritelog
+local function openluawritelog()
+ if not luawritelog then
+ luawritelog = assert(io_open(output_directory .. "/" .. tex.jobname .. ".cluttex-fls", "w"))
+ end
+ return luawritelog
+end
+io.open = function(fname, mode)
+ -- luatexja-ruby
+ if mode == "w" and fname == tex.jobname .. ".ltjruby" then
+ fname = output_directory .. "/" .. fname
+ end
+ if type(mode) == "string" and string.find(mode, "w") ~= nil then
+ -- write mode
+ openluawritelog():write("OUTPUT " .. fname .. "\n")
+ end
+ return io_open(fname, mode)
+end
+os.execute = function(...)
+ texio_write_nl("log", string.format("CLUTTEX_EXEC %s", ...), "")
+ return os_execute(...)
+end
+]==])
+
+ -- Silence some of the TeX output to the terminal.
+ initscript:write([==[
+local function start_file_cb(category, filename)
+ if category == 1 then -- a normal data file, like a TeX source
+ texio_write_nl("log", "("..filename)
+ elseif category == 2 then -- a font map coupling font names to resources
+ texio_write("log", "{"..filename)
+ elseif category == 3 then -- an image file (png, pdf, etc)
+ texio_write("<"..filename)
+ elseif category == 4 then -- an embedded font subset
+ texio_write("<"..filename)
+ elseif category == 5 then -- a fully embedded font
+ texio_write("<<"..filename)
+ else
+ print("start_file: unknown category", category, filename)
+ end
+end
+callback.register("start_file", start_file_cb)
+local function stop_file_cb(category)
+ if category == 1 then
+ texio_write("log", ")")
+ elseif category == 2 then
+ texio_write("log", "}")
+ elseif category == 3 then
+ texio_write(">")
+ elseif category == 4 then
+ texio_write(">")
+ elseif category == 5 then
+ texio_write(">>")
+ else
+ print("stop_file: unknown category", category)
+ end
+end
+callback.register("stop_file", stop_file_cb)
+texio.write = function(...)
+ if select("#",...) == 1 then
+ -- Suppress luaotfload's message (See src/fontloader/runtime/fontload-reference.lua)
+ local s = ...
+ if string.match(s, "^%(using cache: ")
+ or string.match(s, "^%(using write cache: ")
+ or string.match(s, "^%(using read cache: ")
+ or string.match(s, "^%(load luc: ")
+ or string.match(s, "^%(load cache: ") then
+ return texio_write("log", ...)
+ end
+ end
+ return texio_write(...)
+end
+]==])
+ initscript:close()
+end
+
+return {
+ create_initialization_script = create_initialization_script
+}
+end
+package.preload["texrunner.recovery"] = function(...)
+--[[
+ Copyright 2018 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local io = io
+local string = string
+local parse_aux_file = require "texrunner.auxfile".parse_aux_file
+local pathutil = require "texrunner.pathutil"
+local fsutil = require "texrunner.fsutil"
+local shellutil = require "texrunner.shellutil"
+local message = require "texrunner.message"
+
+local function create_missing_directories(args)
+ if string.find(args.execlog, "I can't write on file", 1, true) then
+ -- There is a possibility that there are some subfiles under subdirectories.
+ -- Directories for sub-auxfiles are not created automatically, so we need to provide them.
+ local report = parse_aux_file(args.auxfile, args.options.output_directory)
+ if report.made_new_directory then
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("Created missing directories.")
+ end
+ return true
+ end
+ end
+ return false
+end
+
+local function run_epstopdf(args)
+ local run = false
+ if args.options.shell_escape ~= false then -- (possibly restricted) \write18 enabled
+ for outfile, infile in string.gmatch(args.execlog, "%(epstopdf%)%s*Command: <r?epstopdf %-%-outfile=([%w%-/]+%.pdf) ([%w%-/]+%.eps)>") do
+ local infile_abs = pathutil.abspath(infile, args.original_wd)
+ if fsutil.isfile(infile_abs) then -- input file exists
+ local outfile_abs = pathutil.abspath(outfile, args.options.output_directory)
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("Running epstopdf on ", infile, ".")
+ end
+ local outdir = pathutil.dirname(outfile_abs)
+ if not fsutil.isdir(outdir) then
+ assert(fsutil.mkdir_rec(outdir))
+ end
+ local command = string.format("epstopdf --outfile=%s %s", shellutil.escape(outfile_abs), shellutil.escape(infile_abs))
+ message.exec(command)
+ local success = os.execute(command)
+ if type(success) == "number" then -- Lua 5.1 or LuaTeX
+ success = success == 0
+ end
+ run = run or success
+ end
+ end
+ end
+ return run
+end
+
+local function check_minted(args)
+ return string.find(args.execlog, "Package minted Error: Missing Pygments output; \\inputminted was") ~= nil
+end
+
+local function try_recovery(args)
+ local recovered = false
+ recovered = create_missing_directories(args)
+ recovered = run_epstopdf(args) or recovered
+ recovered = check_minted(args) or recovered
+ return recovered
+end
+
+return {
+ create_missing_directories = create_missing_directories,
+ run_epstopdf = run_epstopdf,
+ try_recovery = try_recovery,
+}
+end
+package.preload["texrunner.handleoption"] = function(...)
+local COPYRIGHT_NOTICE = [[
+Copyright (C) 2016,2018 ARATA Mizuki
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local pathutil = require "texrunner.pathutil"
+local shellutil = require "texrunner.shellutil"
+local parseoption = require "texrunner.option".parseoption
+local KnownEngines = require "texrunner.tex_engine"
+local message = require "texrunner.message"
+
+local function usage(arg)
+ io.write(string.format([[
+ClutTeX: Process TeX files without cluttering your working directory
+
+Usage:
+ %s [options] [--] FILE.tex
+
+Options:
+ -e, --engine=ENGINE Specify which TeX engine to use.
+ ENGINE is one of the following:
+ pdflatex, pdftex,
+ lualatex, luatex, luajittex,
+ xelatex, xetex, latex, etex, tex,
+ platex, eptex, ptex,
+ uplatex, euptex, uptex,
+ -o, --output=FILE The name of output file.
+ [default: JOBNAME.pdf or JOBNAME.dvi]
+ --fresh Clean intermediate files before running TeX.
+ Cannot be used with --output-directory.
+ --max-iterations=N Maximum number of running TeX to resolve
+ cross-references. [default: 3]
+ --start-with-draft Start with draft mode.
+ --[no-]change-directory Change directory before running TeX.
+ --watch Watch input files for change. Requires fswatch
+ program to be installed.
+ --tex-option=OPTION Pass OPTION to TeX as a single option.
+ --tex-options=OPTIONs Pass OPTIONs to TeX as multiple options.
+ --dvipdfmx-option[s]=OPTION[s] Same for dvipdfmx.
+ --makeindex=COMMAND+OPTIONs Command to generate index, such as
+ `makeindex' or `mendex'.
+ --bibtex=COMMAND+OPTIONs Command for BibTeX, such as
+ `bibtex' or `pbibtex'.
+ --biber[=COMMAND+OPTIONs] Command for Biber.
+ --makeglossaries[=COMMAND+OPTIONs] Command for makeglossaries.
+ -h, --help Print this message and exit.
+ -v, --version Print version information and exit.
+ -V, --verbose Be more verbose.
+ --color=WHEN Make ClutTeX's message colorful. WHEN is one of
+ `always', `auto', or `never'. [default: auto]
+ --includeonly=NAMEs Insert '\includeonly{NAMEs}'.
+
+ --[no-]shell-escape
+ --shell-restricted
+ --synctex=NUMBER
+ --fmt=FMTNAME
+ --[no-]file-line-error [default: yes]
+ --[no-]halt-on-error [default: yes]
+ --interaction=STRING [default: nonstopmode]
+ --jobname=STRING
+ --output-directory=DIR [default: somewhere in the temporary directory]
+ --output-format=FORMAT FORMAT is `pdf' or `dvi'. [default: pdf]
+
+%s
+]], arg[0] or 'texlua cluttex.lua', COPYRIGHT_NOTICE))
+end
+
+local option_spec = {
+ -- Options for ClutTeX
+ {
+ short = "e",
+ long = "engine",
+ param = true,
+ },
+ {
+ short = "o",
+ long = "output",
+ param = true,
+ },
+ {
+ long = "fresh",
+ },
+ {
+ long = "max-iterations",
+ param = true,
+ },
+ {
+ long = "start-with-draft",
+ },
+ {
+ long = "change-directory",
+ boolean = true,
+ },
+ {
+ long = "watch",
+ },
+ {
+ short = "h",
+ long = "help",
+ allow_single_hyphen = true,
+ },
+ {
+ short = "v",
+ long = "version",
+ },
+ {
+ short = "V",
+ long = "verbose",
+ },
+ {
+ long = "color",
+ param = true,
+ default = "always",
+ },
+ {
+ long = "includeonly",
+ param = true,
+ },
+ -- Options for TeX
+ {
+ long = "synctex",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "file-line-error",
+ boolean = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "interaction",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "halt-on-error",
+ boolean = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "shell-escape",
+ boolean = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "shell-restricted",
+ allow_single_hyphen = true,
+ },
+ {
+ long = "jobname",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "fmt",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "output-directory",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "output-format",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "tex-option",
+ param = true,
+ },
+ {
+ long = "tex-options",
+ param = true,
+ },
+ {
+ long = "dvipdfmx-option",
+ param = true,
+ },
+ {
+ long = "dvipdfmx-options",
+ param = true,
+ },
+ {
+ long = "makeindex",
+ param = true,
+ },
+ {
+ long = "bibtex",
+ param = true,
+ },
+ {
+ long = "biber",
+ param = true,
+ default = "biber",
+ },
+ {
+ long = "makeglossaries",
+ param = true,
+ default = "makeglossaries",
+ },
+}
+
+-- Default values for options
+local function set_default_values(options)
+ if options.max_iterations == nil then
+ options.max_iterations = 3
+ end
+
+ if options.interaction == nil then
+ options.interaction = "nonstopmode"
+ end
+
+ if options.file_line_error == nil then
+ options.file_line_error = true
+ end
+
+ if options.halt_on_error == nil then
+ options.halt_on_error = true
+ end
+end
+
+-- inputfile, engine, options = handle_cluttex_options(arg)
+local function handle_cluttex_options(arg)
+ -- Parse options
+ local option_and_params, non_option_index = parseoption(arg, option_spec)
+
+ -- Handle options
+ local options = {
+ tex_extraoptions = {},
+ dvipdfmx_extraoptions = {},
+ }
+ CLUTTEX_VERBOSITY = 0
+ for _,option in ipairs(option_and_params) do
+ local name = option[1]
+ local param = option[2]
+
+ if name == "engine" then
+ assert(options.engine == nil, "multiple --engine options")
+ options.engine = param
+
+ elseif name == "output" then
+ assert(options.output == nil, "multiple --output options")
+ options.output = param
+
+ elseif name == "fresh" then
+ assert(options.fresh == nil, "multiple --fresh options")
+ options.fresh = true
+
+ elseif name == "max-iterations" then
+ assert(options.max_iterations == nil, "multiple --max-iterations options")
+ options.max_iterations = assert(tonumber(param), "invalid value for --max-iterations option")
+ assert(options.max_iterations >= 1, "invalid value for --max-iterations option")
+
+ elseif name == "start-with-draft" then
+ assert(options.start_with_draft == nil, "multiple --start-with-draft options")
+ options.start_with_draft = true
+
+ elseif name == "watch" then
+ assert(options.watch == nil, "multiple --watch options")
+ options.watch = true
+
+ elseif name == "help" then
+ usage(arg)
+ os.exit(0)
+
+ elseif name == "version" then
+ io.stderr:write("cluttex ",CLUTTEX_VERSION,"\n")
+ os.exit(0)
+
+ elseif name == "verbose" then
+ CLUTTEX_VERBOSITY = CLUTTEX_VERBOSITY + 1
+
+ elseif name == "color" then
+ assert(options.color == nil, "multiple --collor options")
+ options.color = param
+ message.set_colors(options.color)
+
+ elseif name == "change-directory" then
+ assert(options.change_directory == nil, "multiple --change-directory options")
+ options.change_directory = param
+
+ elseif name == "includeonly" then
+ assert(options.includeonly == nil, "multiple --includeonly options")
+ options.includeonly = param
+
+ -- Options for TeX
+ elseif name == "synctex" then
+ assert(options.synctex == nil, "multiple --synctex options")
+ options.synctex = param
+
+ elseif name == "file-line-error" then
+ options.file_line_error = param
+
+ elseif name == "interaction" then
+ assert(options.interaction == nil, "multiple --interaction options")
+ assert(param == "batchmode" or param == "nonstopmode" or param == "scrollmode" or param == "errorstopmode", "invalid argument for --interaction")
+ options.interaction = param
+
+ elseif name == "halt-on-error" then
+ options.halt_on_error = param
+
+ elseif name == "shell-escape" then
+ assert(options.shell_escape == nil and options.shell_restricted == nil, "multiple --(no-)shell-escape or --shell-restricted options")
+ options.shell_escape = param
+
+ elseif name == "shell-restricted" then
+ assert(options.shell_escape == nil and options.shell_restricted == nil, "multiple --(no-)shell-escape or --shell-restricted options")
+ options.shell_restricted = true
+
+ elseif name == "jobname" then
+ assert(options.jobname == nil, "multiple --jobname options")
+ options.jobname = param
+
+ elseif name == "fmt" then
+ assert(options.fmt == nil, "multiple --fmt options")
+ options.fmt = param
+
+ elseif name == "output-directory" then
+ assert(options.output_directory == nil, "multiple --output-directory options")
+ options.output_directory = param
+
+ elseif name == "output-format" then
+ assert(options.output_format == nil, "multiple --output-format options")
+ assert(param == "pdf" or param == "dvi", "invalid argument for --output-format")
+ options.output_format = param
+
+ elseif name == "tex-option" then
+ table.insert(options.tex_extraoptions, shellutil.escape(param))
+
+ elseif name == "tex-options" then
+ table.insert(options.tex_extraoptions, param)
+
+ elseif name == "dvipdfmx-option" then
+ table.insert(options.dvipdfmx_extraoptions, shellutil.escape(param))
+
+ elseif name == "dvipdfmx-options" then
+ table.insert(options.dvipdfmx_extraoptions, param)
+
+ elseif name == "makeindex" then
+ assert(options.makeindex == nil, "multiple --makeindex options")
+ options.makeindex = param
+
+ elseif name == "bibtex" then
+ assert(options.bibtex == nil, "multiple --bibtex options")
+ assert(options.biber == nil, "multiple --bibtex/--biber options")
+ options.bibtex = param
+
+ elseif name == "biber" then
+ assert(options.biber == nil, "multiple --biber options")
+ assert(options.bibtex == nil, "multiple --bibtex/--biber options")
+ options.biber = param
+
+ elseif name == "makeglossaries" then
+ assert(options.makeglossaries == nil, "multiple --makeglossaries options")
+ options.makeglossaries = param
+
+ end
+ end
+
+ if options.color == nil then
+ message.set_colors("auto")
+ end
+
+ -- Handle non-options (i.e. input file)
+ if non_option_index > #arg then
+ -- No input file given
+ usage(arg)
+ os.exit(1)
+ elseif non_option_index < #arg then
+ message.error("Multiple input files are not supported.")
+ os.exit(1)
+ end
+ local inputfile = arg[non_option_index]
+
+ -- If run as 'cllualatex', then the default engine is lualatex
+ if options.engine == nil and type(arg[0]) == "string" then
+ local basename = pathutil.trimext(pathutil.basename(arg[0]))
+ local engine_part = string.match(basename, "^cl(%w+)$")
+ if engine_part and KnownEngines[engine_part] then
+ options.engine = engine_part
+ end
+ end
+
+ if options.engine == nil then
+ message.error("Engine not specified.")
+ os.exit(1)
+ end
+ local engine = KnownEngines[options.engine]
+ if not engine then
+ message.error("Unknown engine name '", options.engine, "'.")
+ os.exit(1)
+ end
+
+ set_default_values(options)
+
+ return inputfile, engine, options
+end
+
+return {
+ usage = usage,
+ handle_cluttex_options = handle_cluttex_options,
+}
+end
+package.preload["texrunner.isatty"] = function(...)
+--[[
+ Copyright 2018 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+if os.type == "unix" then
+ -- Try luaposix
+ local succ, M = pcall(function()
+ local isatty = require "posix.unistd".isatty
+ local fileno = require "posix.stdio".fileno
+ return {
+ isatty = function(file)
+ return isatty(fileno(file)) == 1
+ end,
+ }
+ end)
+ if succ then
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: isatty found via luaposix\n")
+ end
+ return M
+ else
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: luaposix not found: ", M, "\n")
+ end
+ end
+
+ -- Try LuaJIT-like FFI
+ local succ, M = pcall(function()
+ local ffi = require "ffi"
+ ffi.cdef[[
+int isatty(int fd);
+int fileno(void *stream);
+]]
+ local isatty = assert(ffi.C.isatty, "isatty not found")
+ local fileno = assert(ffi.C.fileno, "fileno not found")
+ return {
+ isatty = function(file)
+ -- LuaJIT converts Lua's file handles into FILE* (void*)
+ return isatty(fileno(file)) ~= 0
+ end
+ }
+ end)
+ if succ then
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: isatty found via FFI (Unix)\n")
+ end
+ return M
+ else
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: FFI (Unix) not found: ", M, "\n")
+ end
+ end
+
+else
+ -- Try LuaJIT
+ -- TODO: Try to detect MinTTY using GetFileInformationByHandleEx
+ local succ, M = pcall(function()
+ local ffi = require "ffi"
+ local bitlib = assert(bit32 or bit, "Neither bit32 (Lua 5.2) nor bit (LuaJIT) found") -- Lua 5.2 or LuaJIT
+ ffi.cdef[[
+int _isatty(int fd);
+int _fileno(void *stream);
+void *_get_osfhandle(int fd); // should return intptr_t
+typedef int BOOL;
+typedef uint32_t DWORD;
+typedef int FILE_INFO_BY_HANDLE_CLASS; // ???
+typedef struct _FILE_NAME_INFO {
+DWORD FileNameLength;
+uint16_t FileName[?];
+} FILE_NAME_INFO;
+DWORD GetFileType(void *hFile);
+BOOL GetFileInformationByHandleEx(void *hFile, FILE_INFO_BY_HANDLE_CLASS fic, void *fileinfo, DWORD dwBufferSize);
+BOOL GetConsoleMode(void *hConsoleHandle, DWORD* lpMode);
+BOOL SetConsoleMode(void *hConsoleHandle, DWORD dwMode);
+]]
+ local isatty = assert(ffi.C._isatty, "_isatty not found")
+ local fileno = assert(ffi.C._fileno, "_fileno not found")
+ local get_osfhandle = assert(ffi.C._get_osfhandle, "_get_osfhandle not found")
+ local GetFileType = assert(ffi.C.GetFileType, "GetFileType not found")
+ local GetFileInformationByHandleEx = assert(ffi.C.GetFileInformationByHandleEx, "GetFileInformationByHandleEx not found")
+ local GetConsoleMode = assert(ffi.C.GetConsoleMode, "GetConsoleMode not found")
+ local SetConsoleMode = assert(ffi.C.SetConsoleMode, "SetConsoleMode not found")
+ local function wide_to_narrow(array, length)
+ local t = {}
+ for i = 0, length - 1 do
+ table.insert(t, string.char(math.min(array[i], 0xff)))
+ end
+ return table.concat(t, "")
+ end
+ local function is_mintty(fd)
+ local handle = get_osfhandle(fd)
+ local filetype = GetFileType(handle)
+ if filetype ~= 0x0003 then -- not FILE_TYPE_PIPE (0x0003)
+ -- mintty must be a pipe
+ if CLUTTEX_VERBOSITY >= 4 then
+ io.stderr:write("ClutTeX: not a pipe\n")
+ end
+ return false
+ end
+ local nameinfo = ffi.new("FILE_NAME_INFO", 32768)
+ local FileNameInfo = 2 -- : FILE_INFO_BY_HANDLE_CLASS
+ if GetFileInformationByHandleEx(handle, FileNameInfo, nameinfo, ffi.sizeof("FILE_NAME_INFO", 32768)) ~= 0 then
+ local filename = wide_to_narrow(nameinfo.FileName, math.floor(nameinfo.FileNameLength / 2))
+ -- \(cygwin|msys)-<hex digits>-pty<N>-(from|to)-master
+ if CLUTTEX_VERBOSITY >= 4 then
+ io.stderr:write("ClutTeX: GetFileInformationByHandleEx returned ", filename, "\n")
+ end
+ local a, b = string.match(filename, "^\\(%w+)%-%x+%-pty%d+%-(%w+)%-master$")
+ if (a == "cygwin" or a == "msys") and (b == "from" or b == "to") then
+ return true
+ end
+ else
+ if CLUTTEX_VERBOSITY >= 4 then
+ io.stderr:write("ClutTeX: GetFileInformationByHandleEx failed\n")
+ end
+ end
+ return false
+ end
+ return {
+ isatty = function(file)
+ -- LuaJIT converts Lua's file handles into FILE* (void*)
+ local fd = fileno(file)
+ return isatty(fd) ~= 0 or is_mintty(fd)
+ end,
+ enable_console_colors = function(file)
+ local fd = fileno(file)
+ if isatty(fd) ~= 0 then
+ local handle = get_osfhandle(fd)
+ local modePtr = ffi.new("DWORD[1]")
+ local result = GetConsoleMode(handle, modePtr)
+ if result ~= 0 then
+ local ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
+ result = SetConsoleMode(handle, bitlib.bor(modePtr[0], ENABLE_VIRTUAL_TERMINAL_PROCESSING))
+ if result == 0 then
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: SetConsoleMode failed\n")
+ end
+ end
+ else
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: GetConsoleMode failed\n")
+ end
+ end
+ end
+ end,
+ }
+ end)
+ if succ then
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: isatty found via FFI (Windows)\n")
+ end
+ return M
+ else
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: FFI (Windows) not found: ", M, "\n")
+ end
+ end
+end
+
+return {
+ isatty = function(file)
+ return false
+ end,
+}
+end
+package.preload["texrunner.message"] = function(...)
+--[[
+ Copyright 2018 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local use_colors = false
+
+local function set_colors(mode)
+ local M
+ if mode == "always" then
+ use_colors = true
+ M = require "texrunner.isatty"
+ if M.enable_console_colors then
+ M.enable_console_colors(io.stderr)
+ end
+ elseif mode == "never" then
+ use_colors = false
+ elseif mode == "auto" then
+ M = require "texrunner.isatty"
+ use_colors = M.isatty(io.stderr)
+ else
+ error "The value of --color option must be one of 'auto', 'always', or 'never'."
+ end
+ if use_colors and M.enable_console_colors then
+ M.enable_console_colors(io.stderr)
+ end
+end
+
+-- ESCAPE: hex 1B = dec 27 = oct 33
+
+local CMD = {
+ reset = "\027[0m",
+ underline = "\027[4m",
+ fg_black = "\027[30m",
+ fg_red = "\027[31m",
+ fg_green = "\027[32m",
+ fg_yellow = "\027[33m",
+ fg_blue = "\027[34m",
+ fg_magenta = "\027[35m",
+ fg_cyan = "\027[36m",
+ fg_white = "\027[37m",
+ fg_reset = "\027[39m",
+ bg_black = "\027[40m",
+ bg_red = "\027[41m",
+ bg_green = "\027[42m",
+ bg_yellow = "\027[43m",
+ bg_blue = "\027[44m",
+ bg_magenta = "\027[45m",
+ bg_cyan = "\027[46m",
+ bg_white = "\027[47m",
+ bg_reset = "\027[49m",
+ fg_x_black = "\027[90m",
+ fg_x_red = "\027[91m",
+ fg_x_green = "\027[92m",
+ fg_x_yellow = "\027[93m",
+ fg_x_blue = "\027[94m",
+ fg_x_magenta = "\027[95m",
+ fg_x_cyan = "\027[96m",
+ fg_x_white = "\027[97m",
+ bg_x_black = "\027[100m",
+ bg_x_red = "\027[101m",
+ bg_x_green = "\027[102m",
+ bg_x_yellow = "\027[103m",
+ bg_x_blue = "\027[104m",
+ bg_x_magenta = "\027[105m",
+ bg_x_cyan = "\027[106m",
+ bg_x_white = "\027[107m",
+}
+
+local function exec_msg(commandline)
+ if use_colors then
+ io.stderr:write(CMD.fg_x_white, CMD.bg_red, "[EXEC]", CMD.reset, " ", CMD.fg_red, commandline, CMD.reset, "\n")
+ else
+ io.stderr:write("[EXEC] ", commandline, "\n")
+ end
+end
+
+local function error_msg(...)
+ local message = table.concat({...}, "")
+ if use_colors then
+ io.stderr:write(CMD.fg_x_white, CMD.bg_red, "[ERROR]", CMD.reset, " ", CMD.fg_red, message, CMD.reset, "\n")
+ else
+ io.stderr:write("[ERROR] ", message, "\n")
+ end
+end
+
+local function warn_msg(...)
+ local message = table.concat({...}, "")
+ if use_colors then
+ io.stderr:write(CMD.fg_x_white, CMD.bg_red, "[WARN]", CMD.reset, " ", CMD.fg_blue, message, CMD.reset, "\n")
+ else
+ io.stderr:write("[WARN] ", message, "\n")
+ end
+end
+
+local function diag_msg(...)
+ local message = table.concat({...}, "")
+ if use_colors then
+ io.stderr:write(CMD.fg_x_white, CMD.bg_red, "[DIAG]", CMD.reset, " ", CMD.fg_blue, message, CMD.reset, "\n")
+ else
+ io.stderr:write("[DIAG] ", message, "\n")
+ end
+end
+
+local function info_msg(...)
+ local message = table.concat({...}, "")
+ if use_colors then
+ io.stderr:write(CMD.fg_x_white, CMD.bg_red, "[INFO]", CMD.reset, " ", CMD.fg_magenta, message, CMD.reset, "\n")
+ else
+ io.stderr:write("[INFO] ", message, "\n")
+ end
+end
+
+return {
+ set_colors = set_colors,
+ exec = exec_msg,
+ error = error_msg,
+ warn = warn_msg,
+ diag = diag_msg,
+ info = info_msg,
+}
+end
+--[[
+ Copyright 2016,2018 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+CLUTTEX_VERSION = "v0.1"
+
+-- Standard libraries
+local coroutine = coroutine
+local tostring = tostring
+
+-- External libraries (included in texlua)
+local filesys = require "lfs"
+local md5 = require "md5"
+-- local kpse = require "kpse"
+
+-- My own modules
+local pathutil = require "texrunner.pathutil"
+local fsutil = require "texrunner.fsutil"
+local shellutil = require "texrunner.shellutil"
+local reruncheck = require "texrunner.reruncheck"
+local luatexinit = require "texrunner.luatexinit"
+local recoverylib = require "texrunner.recovery"
+local message = require "texrunner.message"
+local extract_bibtex_from_aux_file = require "texrunner.auxfile".extract_bibtex_from_aux_file
+local handle_cluttex_options = require "texrunner.handleoption".handle_cluttex_options
+
+-- arguments: input file name, jobname, etc...
+local function genOutputDirectory(...)
+ -- The name of the temporary directory is based on the path of input file.
+ local message = table.concat({...}, "\0")
+ local hash = md5.sumhexa(message)
+ local tmpdir = os.getenv("TMPDIR") or os.getenv("TMP") or os.getenv("TEMP")
+ if tmpdir == nil then
+ local home = os.getenv("HOME") or os.getenv("USERPROFILE") or error("environment variable 'TMPDIR' not set!")
+ tmpdir = pathutil.join(home, ".latex-build-temp")
+ end
+ return pathutil.join(tmpdir, 'latex-build-' .. hash)
+end
+
+local inputfile, engine, options = handle_cluttex_options(arg)
+
+local jobname = options.jobname or pathutil.basename(pathutil.trimext(inputfile))
+assert(jobname ~= "", "jobname cannot be empty")
+
+if options.output_format == nil then
+ options.output_format = "pdf"
+end
+local output_extension
+if options.output_format == "dvi" then
+ output_extension = engine.dvi_extension or "dvi"
+else
+ output_extension = "pdf"
+end
+
+if options.output == nil then
+ options.output = jobname .. "." .. output_extension
+end
+
+-- Prepare output directory
+if options.output_directory == nil then
+ local inputfile_abs = pathutil.abspath(inputfile)
+ options.output_directory = genOutputDirectory(inputfile_abs, jobname, options.engine)
+
+ if not fsutil.isdir(options.output_directory) then
+ assert(fsutil.mkdir_rec(options.output_directory))
+
+ elseif options.fresh then
+ -- The output directory exists and --fresh is given:
+ -- Remove all files in the output directory
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("Cleaning '", options.output_directory, "'...")
+ end
+ assert(fsutil.remove_rec(options.output_directory))
+ assert(filesys.mkdir(options.output_directory))
+ end
+
+elseif options.fresh then
+ message.error("--fresh and --output-directory cannot be used together.")
+ os.exit(1)
+end
+
+local pathsep = ":"
+if os.type == "windows" then
+ pathsep = ";"
+end
+
+local original_wd = filesys.currentdir()
+if options.change_directory then
+ local TEXINPUTS = os.getenv("TEXINPUTS") or ""
+ filesys.chdir(options.output_directory)
+ options.output = pathutil.abspath(options.output, original_wd)
+ os.setenv("TEXINPUTS", original_wd .. pathsep .. TEXINPUTS)
+end
+if options.bibtex or options.biber then
+ local BIBINPUTS = os.getenv("BIBINPUTS") or ""
+ options.output = pathutil.abspath(options.output, original_wd)
+ os.setenv("BIBINPUTS", original_wd .. pathsep .. BIBINPUTS)
+end
+
+-- Set `max_print_line' environment variable if not already set.
+if os.getenv("max_print_line") == nil then
+ os.setenv("max_print_line", "65536")
+end
+-- TODO: error_line, half_error_line
+--[[
+ According to texmf.cnf:
+ 45 < error_line < 255,
+ 30 < half_error_line < error_line - 15,
+ 60 <= max_print_line.
+]]
+
+local function path_in_output_directory(ext)
+ return pathutil.join(options.output_directory, jobname .. "." .. ext)
+end
+
+local recorderfile = path_in_output_directory("fls")
+local recorderfile2 = path_in_output_directory("cluttex-fls")
+
+local tex_options = {
+ interaction = options.interaction,
+ file_line_error = options.file_line_error,
+ halt_on_error = options.halt_on_error,
+ synctex = options.synctex,
+ output_directory = options.output_directory,
+ shell_escape = options.shell_escape,
+ shell_restricted = options.shell_restricted,
+ jobname = options.jobname,
+ fmt = options.fmt,
+ extraoptions = options.tex_extraoptions,
+}
+if options.output_format ~= "pdf" and engine.supports_pdf_generation then
+ tex_options.output_format = options.output_format
+end
+
+-- Setup LuaTeX initialization script
+if engine.is_luatex then
+ local initscriptfile = path_in_output_directory("cluttexinit.lua")
+ luatexinit.create_initialization_script(initscriptfile, tex_options)
+ tex_options.lua_initialization_script = initscriptfile
+end
+
+-- Run TeX command (*tex, *latex)
+-- should_rerun, newauxstatus = single_run([auxstatus])
+-- This function should be run in a coroutine.
+local function single_run(auxstatus, iteration)
+ local minted = false
+ local bibtex_aux_hash = nil
+ local mainauxfile = path_in_output_directory("aux")
+ if fsutil.isfile(recorderfile) then
+ -- Recorder file already exists
+ local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options)
+ if engine.is_luatex and fsutil.isfile(recorderfile2) then
+ filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap)
+ end
+ auxstatus = reruncheck.collectfileinfo(filelist, auxstatus)
+ for _,fileinfo in ipairs(filelist) do
+ if string.match(fileinfo.path, "minted/minted%.sty$") then
+ minted = true
+ break
+ end
+ end
+ if options.bibtex then
+ local biblines = extract_bibtex_from_aux_file(mainauxfile, options.output_directory)
+ if #biblines > 0 then
+ bibtex_aux_hash = md5.sum(table.concat(biblines, "\n"))
+ end
+ end
+ else
+ -- This is the first execution
+ if auxstatus ~= nil then
+ message.error("Recorder file was not generated during the execution!")
+ os.exit(1)
+ end
+ auxstatus = {}
+ end
+ --local timestamp = os.time()
+
+ if options.includeonly then
+ tex_options.tex_injection = string.format("%s\\includeonly{%s}", tex_options.tex_injection or "", options.includeonly)
+ end
+
+ if minted and not (tex_options.tex_injection and string.find(tex_options.tex_injection,"minted") == nil) then
+ tex_options.tex_injection = string.format("%s\\PassOptionsToPackage{outputdir=%s}{minted}", tex_options.tex_injection or "", options.output_directory)
+ end
+
+ local current_tex_options, lightweight_mode = tex_options, false
+ if iteration == 1 and options.start_with_draft then
+ current_tex_options = {}
+ for k,v in pairs(tex_options) do
+ current_tex_options[k] = v
+ end
+ if engine.supports_draftmode then
+ current_tex_options.draftmode = true
+ options.start_with_draft = false
+ end
+ current_tex_options.interaction = "batchmode"
+ lightweight_mode = true
+ else
+ current_tex_options.draftmode = false
+ end
+
+ local command = engine:build_command(inputfile, current_tex_options)
+
+ local execlog -- the contents of .log file
+
+ local recovered = false
+ local function recover()
+ -- Check log file
+ if not execlog then
+ local logfile = assert(io.open(path_in_output_directory("log")))
+ execlog = logfile:read("*a")
+ logfile:close()
+ end
+ recovered = recoverylib.try_recovery{
+ execlog = execlog,
+ auxfile = path_in_output_directory("aux"),
+ options = options,
+ original_wd = original_wd,
+ }
+ return recovered
+ end
+ coroutine.yield(command, recover) -- Execute the command
+ if recovered then
+ return true, {}
+ end
+
+ local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options)
+ if engine.is_luatex and fsutil.isfile(recorderfile2) then
+ filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap)
+ end
+
+ if not execlog then
+ local logfile = assert(io.open(path_in_output_directory("log")))
+ execlog = logfile:read("*a")
+ logfile:close()
+ end
+
+ if options.makeindex then
+ -- Look for .idx files and run MakeIndex
+ for _,file in ipairs(filelist) do
+ if pathutil.ext(file.path) == "idx" then
+ -- Run makeindex if the .idx file is new or updated
+ local idxfileinfo = {path = file.path, abspath = file.abspath, kind = "auxiliary"}
+ local output_ind = pathutil.replaceext(file.abspath, "ind")
+ if reruncheck.comparefileinfo({idxfileinfo}, auxstatus) or reruncheck.comparefiletime(file.abspath, output_ind, auxstatus) then
+ local idx_dir = pathutil.dirname(file.abspath)
+ local makeindex_command = {
+ "cd", shellutil.escape(idx_dir), "&&",
+ options.makeindex, -- Do not escape options.makeindex to allow additional options
+ "-o", pathutil.basename(output_ind),
+ pathutil.basename(file.abspath)
+ }
+ coroutine.yield(table.concat(makeindex_command, " "))
+ table.insert(filelist, {path = output_ind, abspath = output_ind, kind = "auxiliary"})
+ else
+ local succ, err = filesys.touch(output_ind)
+ if not succ then
+ message.warn("Failed to touch " .. output_ind .. " (" .. err .. ")")
+ end
+ end
+ end
+ end
+ else
+ -- Check log file
+ if string.find(execlog, "No file [^\n]+%.ind%.") then
+ message.diag("You may want to use --makeindex option.")
+ end
+ end
+
+ if options.makeglossaries then
+ -- Look for .glo files and run makeglossaries
+ for _,file in ipairs(filelist) do
+ if pathutil.ext(file.path) == "glo" then
+ -- Run makeglossaries if the .glo file is new or updated
+ local glofileinfo = {path = file.path, abspath = file.abspath, kind = "auxiliary"}
+ local output_gls = pathutil.replaceext(file.abspath, "gls")
+ if reruncheck.comparefileinfo({glofileinfo}, auxstatus) or reruncheck.comparefiletime(file.abspath, output_gls, auxstatus) then
+ local makeglossaries_command = {
+ options.makeglossaries,
+ "-d", shellutil.escape(options.output_directory),
+ pathutil.trimext(pathutil.basename(file.path))
+ }
+ coroutine.yield(table.concat(makeglossaries_command, " "))
+ table.insert(filelist, {path = output_gls, abspath = output_gls, kind = "auxiliary"})
+ else
+ local succ, err = filesys.touch(output_gls)
+ if not succ then
+ message.warn("Failed to touch " .. output_ind .. " (" .. err .. ")")
+ end
+ end
+ end
+ end
+ else
+ -- Check log file
+ if string.find(execlog, "No file [^\n]+%.gls%.") then
+ message.diag("You may want to use --makeglossaries option.")
+ end
+ end
+
+ if options.bibtex then
+ local biblines2 = extract_bibtex_from_aux_file(mainauxfile, options.output_directory)
+ local bibtex_aux_hash2
+ if #biblines2 > 0 then
+ bibtex_aux_hash2 = md5.sum(table.concat(biblines2, "\n"))
+ end
+ local output_bbl = path_in_output_directory("bbl")
+ if bibtex_aux_hash ~= bibtex_aux_hash2 or reruncheck.comparefiletime(mainauxfile, output_bbl, auxstatus) then
+ -- The input for BibTeX command has changed...
+ local bibtex_command = {
+ "cd", shellutil.escape(options.output_directory), "&&",
+ options.bibtex,
+ pathutil.basename(mainauxfile)
+ }
+ coroutine.yield(table.concat(bibtex_command, " "))
+ else
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("No need to run BibTeX.")
+ end
+ local succ, err = filesys.touch(output_bbl)
+ if not succ then
+ message.warn("Failed to touch " .. output_bbl .. " (" .. err .. ")")
+ end
+ end
+ elseif options.biber then
+ for _,file in ipairs(filelist) do
+ if pathutil.ext(file.path) == "bcf" then
+ -- Run biber if the .bcf file is new or updated
+ local bcffileinfo = {path = file.path, abspath = file.abspath, kind = "auxiliary"}
+ local output_bbl = pathutil.replaceext(file.abspath, "bbl")
+ if reruncheck.comparefileinfo({bcffileinfo}, auxstatus) or reruncheck.comparefiletime(file.abspath, output_bbl, auxstatus) then
+ local bbl_dir = pathutil.dirname(file.abspath)
+ local biber_command = {
+ options.biber, -- Do not escape options.biber to allow additional options
+ "--output-directory", shellutil.escape(options.output_directory),
+ pathutil.basename(file.abspath)
+ }
+ coroutine.yield(table.concat(biber_command, " "))
+ table.insert(filelist, {path = output_bbl, abspath = output_bbl, kind = "auxiliary"})
+ else
+ local succ, err = filesys.touch(output_bbl)
+ if not succ then
+ message.warn("Failed to touch " .. output_bbl .. " (" .. err .. ")")
+ end
+ end
+ end
+ end
+ else
+ -- Check log file
+ if string.find(execlog, "No file [^\n]+%.bbl%.") then
+ message.diag("You may want to use --bibtex or --biber option.")
+ end
+ end
+
+ if string.find(execlog, "No pages of output.") then
+ return "No pages of output."
+ end
+
+ local should_rerun, auxstatus = reruncheck.comparefileinfo(filelist, auxstatus)
+ return should_rerun or lightweight_mode, auxstatus
+end
+
+-- Run (La)TeX (possibly multiple times) and produce a PDF file.
+-- This function should be run in a coroutine.
+local function do_typeset_c()
+ local iteration = 0
+ local should_rerun, auxstatus
+ repeat
+ iteration = iteration + 1
+ should_rerun, auxstatus = single_run(auxstatus, iteration)
+ if should_rerun == "No pages of output." then
+ message.warn("No pages of output.")
+ return
+ end
+ until not should_rerun or iteration >= options.max_iterations
+
+ if should_rerun then
+ message.warn("LaTeX should be run once more.")
+ end
+
+ -- Successful
+ if options.output_format == "dvi" or engine.supports_pdf_generation then
+ -- Output file (DVI/PDF) is generated in the output directory
+ local outfile = path_in_output_directory(output_extension)
+ local oncopyerror
+ if os.type == "windows" then
+ oncopyerror = function()
+ message.error("Failed to copy file. Some applications may be locking the ", string.upper(options.output_format), " file.")
+ return false
+ end
+ end
+ coroutine.yield(fsutil.copy_command(outfile, options.output), oncopyerror)
+ if #options.dvipdfmx_extraoptions > 0 then
+ message.warn("--dvipdfmx-option[s] are ignored.")
+ end
+
+ else
+ -- DVI file is generated, but PDF file is wanted
+ local dvifile = path_in_output_directory("dvi")
+ local dvipdfmx_command = {"dvipdfmx", "-o", shellutil.escape(options.output)}
+ for _,v in ipairs(options.dvipdfmx_extraoptions) do
+ table.insert(dvipdfmx_command, v)
+ end
+ table.insert(dvipdfmx_command, shellutil.escape(dvifile))
+ coroutine.yield(table.concat(dvipdfmx_command, " "))
+ end
+
+ -- Copy SyncTeX file if necessary
+ if options.output_format == "pdf" then
+ local synctex = tonumber(options.synctex or "0")
+ local synctex_ext = nil
+ if synctex > 0 then
+ -- Compressed SyncTeX file (.synctex.gz)
+ synctex_ext = "synctex.gz"
+ elseif synctex < 0 then
+ -- Uncompressed SyncTeX file (.synctex)
+ synctex_ext = "synctex"
+ end
+ if synctex_ext then
+ coroutine.yield(fsutil.copy_command(path_in_output_directory(synctex_ext), pathutil.replaceext(options.output, synctex_ext)))
+ end
+ end
+end
+
+local function do_typeset()
+ -- Execute the command string yielded by do_typeset_c
+ for command, recover in coroutine.wrap(do_typeset_c) do
+ message.exec(command)
+ local success, termination, status_or_signal = os.execute(command)
+ if type(success) == "number" then -- Lua 5.1 or LuaTeX
+ local code = success
+ success = code == 0
+ termination = nil
+ status_or_signal = code
+ end
+ if not success and not (recover and recover()) then
+ if termination == "exit" then
+ message.error("Command exited abnormally: exit status ", tostring(status_or_signal))
+ elseif termination == "signal" then
+ message.error("Command exited abnormally: signal ", tostring(status_or_signal))
+ else
+ message.error("Command exited abnormally: ", tostring(status_or_signal))
+ end
+ return false, termination, status_or_signal
+ end
+ end
+ -- Successful
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("Command exited successfully")
+ end
+ return true
+end
+
+if options.watch then
+ -- Watch mode
+ local success, status = do_typeset()
+ local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options)
+ if engine.is_luatex and fsutil.isfile(recorderfile2) then
+ filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap)
+ end
+ local input_files_to_watch = {}
+ for _,fileinfo in ipairs(filelist) do
+ if fileinfo.kind == "input" then
+ table.insert(input_files_to_watch, fileinfo.abspath)
+ end
+ end
+ local fswatch_command = {"fswatch", "--event=Updated", "--"}
+ for _,path in ipairs(input_files_to_watch) do
+ table.insert(fswatch_command, shellutil.escape(path))
+ end
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.exec(table.concat(fswatch_command, " "))
+ end
+ local fswatch = assert(io.popen(table.concat(fswatch_command, " "), "r"))
+ for l in fswatch:lines() do
+ local found = false
+ for _,path in ipairs(input_files_to_watch) do
+ if l == path then
+ found = true
+ break
+ end
+ end
+ if found then
+ local success, status = do_typeset()
+ if not success then
+ -- Not successful
+ end
+ end
+ end
+
+else
+ -- Not in watch mode
+ local success, status = do_typeset()
+ if not success then
+ os.exit(1)
+ end
+end
diff --git a/Master/bin/win32/cluttex.bat b/Master/bin/win32/cluttex.bat
new file mode 100755
index 00000000000..6b938411f9d
--- /dev/null
+++ b/Master/bin/win32/cluttex.bat
@@ -0,0 +1,2598 @@
+::dummy:: --[[
+@texlua "%~f0" %*
+@goto :eof
+]]
+local io, os, string, table, package, require, assert, error, ipairs, type, select, arg = io, os, string, table, package, require, assert, error, ipairs, type, select, arg
+local CLUTTEX_VERBOSITY, CLUTTEX_VERSION
+os.type = os.type or "windows"
+if lfs and not package.loaded['lfs'] then package.loaded['lfs'] = lfs end
+if os.type == "windows" then
+package.preload["texrunner.pathutil"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+-- pathutil module
+
+local assert = assert
+local select = select
+local string = string
+local string_find = string.find
+local string_sub = string.sub
+local string_match = string.match
+local string_gsub = string.gsub
+local filesys = require "lfs"
+
+local function basename(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "[\\/]", i + 1)
+ if j == nil then
+ return string_sub(path, i + 1)
+ elseif j == #path then
+ return string_sub(path, i + 1, -2)
+ end
+ i = j
+ end
+end
+
+
+local function dirname(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "[\\/]", i + 1)
+ if j == nil then
+ if i == 0 then
+ -- No directory portion
+ return "."
+ elseif i == 1 then
+ -- Root
+ return string_sub(path, 1, 1)
+ else
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ end
+ i = j
+ end
+end
+
+
+local function parentdir(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "[\\/]", i + 1)
+ if j == nil then
+ if i == 0 then
+ -- No directory portion
+ return "."
+ elseif i == 1 then
+ -- Root
+ return string_sub(path, 1, 1)
+ else
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ elseif j == #path then
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ i = j
+ end
+end
+
+
+local function trimext(path)
+ return (string_gsub(path, "%.[^\\/%.]*$", ""))
+end
+
+
+local function ext(path)
+ return string_match(path, "%.([^\\/%.]*)$") or ""
+end
+
+
+local function replaceext(path, newext)
+ local newpath, n = string_gsub(path, "%.([^\\/%.]*)$", function() return "." .. newext end)
+ if n == 0 then
+ return newpath .. "." .. newext
+ else
+ return newpath
+ end
+end
+
+
+local function joinpath2(x, y)
+ local xd = x
+ local last = string_sub(x, -1)
+ if last ~= "/" and last ~= "\\" then
+ xd = x .. "\\"
+ end
+ if y == "." then
+ return xd
+ elseif y == ".." then
+ return dirname(x)
+ else
+ if string_match(y, "^%.[\\/]") then
+ return xd .. string_sub(y, 3)
+ else
+ return xd .. y
+ end
+ end
+end
+
+local function joinpath(...)
+ local n = select("#", ...)
+ if n == 2 then
+ return joinpath2(...)
+ elseif n == 0 then
+ return "."
+ elseif n == 1 then
+ return ...
+ else
+ return joinpath(joinpath2(...), select(3, ...))
+ end
+end
+
+
+-- https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
+local function isabspath(path)
+ local init = string_sub(path, 1, 1)
+ return init == "\\" or init == "/" or string_match(path, "^%a:[/\\]")
+end
+
+local function abspath(path, cwd)
+ if isabspath(path) then
+ -- absolute path
+ return path
+ else
+ -- TODO: relative path with a drive letter is not supported
+ cwd = cwd or filesys.currentdir()
+ return joinpath2(cwd, path)
+ end
+end
+
+return {
+ basename = basename,
+ dirname = dirname,
+ parentdir = parentdir,
+ trimext = trimext,
+ ext = ext,
+ replaceext = replaceext,
+ join = joinpath,
+ abspath = abspath,
+}
+end
+else
+package.preload["texrunner.pathutil"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+-- pathutil module for *nix
+
+local assert = assert
+local select = select
+local string = string
+local string_find = string.find
+local string_sub = string.sub
+local string_match = string.match
+local string_gsub = string.gsub
+local filesys = require "lfs"
+
+local function basename(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "/", i + 1, true)
+ if j == nil then
+ return string_sub(path, i + 1)
+ elseif j == #path then
+ return string_sub(path, i + 1, -2)
+ end
+ i = j
+ end
+end
+
+
+local function dirname(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "/", i + 1, true)
+ if j == nil then
+ if i == 0 then
+ -- No directory portion
+ return "."
+ elseif i == 1 then
+ -- Root
+ return "/"
+ else
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ end
+ i = j
+ end
+end
+
+
+local function parentdir(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "/", i + 1, true)
+ if j == nil then
+ if i == 0 then
+ -- No directory portion
+ return "."
+ elseif i == 1 then
+ -- Root
+ return "/"
+ else
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ elseif j == #path then
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ i = j
+ end
+end
+
+
+local function trimext(path)
+ return (string_gsub(path, "%.[^/%.]*$", ""))
+end
+
+
+local function ext(path)
+ return string_match(path, "%.([^/%.]*)$") or ""
+end
+
+
+local function replaceext(path, newext)
+ local newpath, n = string_gsub(path, "%.([^/%.]*)$", function() return "." .. newext end)
+ if n == 0 then
+ return newpath .. "." .. newext
+ else
+ return newpath
+ end
+end
+
+
+local function joinpath2(x, y)
+ local xd = x
+ if string_sub(x, -1) ~= "/" then
+ xd = x .. "/"
+ end
+ if y == "." then
+ return xd
+ elseif y == ".." then
+ return dirname(x)
+ else
+ if string_sub(y, 1, 2) == "./" then
+ return xd .. string_sub(y, 3)
+ else
+ return xd .. y
+ end
+ end
+end
+
+local function joinpath(...)
+ local n = select("#", ...)
+ if n == 2 then
+ return joinpath2(...)
+ elseif n == 0 then
+ return "."
+ elseif n == 1 then
+ return ...
+ else
+ return joinpath(joinpath2(...), select(3, ...))
+ end
+end
+
+
+local function abspath(path, cwd)
+ if string_sub(path, 1, 1) == "/" then
+ -- absolute path
+ return path
+ else
+ cwd = cwd or filesys.currentdir()
+ return joinpath2(cwd, path)
+ end
+end
+
+
+return {
+ basename = basename,
+ dirname = dirname,
+ parentdir = parentdir,
+ trimext = trimext,
+ ext = ext,
+ replaceext = replaceext,
+ join = joinpath,
+ abspath = abspath,
+}
+end
+end
+if os.type == "windows" then
+package.preload["texrunner.shellutil"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local string_gsub = string.gsub
+
+-- s: string
+local function escape(s)
+ return '"' .. string_gsub(string_gsub(s, '(\\*)"', '%1%1\\"'), '(\\+)$', '%1%1') .. '"'
+end
+
+
+return {
+ escape = escape,
+}
+end
+else
+package.preload["texrunner.shellutil"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local assert = assert
+local string_match = string.match
+local table = table
+local table_insert = table.insert
+local table_concat = table.concat
+
+-- s: string
+local function escape(s)
+ local len = #s
+ local result = {}
+ local t,i = string_match(s, "^([^']*)()")
+ assert(t)
+ if t ~= "" then
+ table_insert(result, "'")
+ table_insert(result, t)
+ table_insert(result, "'")
+ end
+ while i < len do
+ t,i = string_match(s, "^('+)()", i)
+ assert(t)
+ table_insert(result, '"')
+ table_insert(result, t)
+ table_insert(result, '"')
+ t,i = string_match(s, "^([^']*)()", i)
+ assert(t)
+ if t ~= "" then
+ table_insert(result, "'")
+ table_insert(result, t)
+ table_insert(result, "'")
+ end
+ end
+ return table_concat(result, "")
+end
+
+
+return {
+ escape = escape,
+}
+end
+end
+package.preload["texrunner.fsutil"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local assert = assert
+local os = os
+local os_execute = os.execute
+local os_remove = os.remove
+local filesys = require "lfs"
+local pathutil = require "texrunner.pathutil"
+local shellutil = require "texrunner.shellutil"
+local escape = shellutil.escape
+
+local copy_command
+if os.type == "windows" then
+ function copy_command(from, to)
+ -- TODO: What if `from` begins with a slash?
+ return "copy " .. escape(from) .. " " .. escape(to) .. " > NUL"
+ end
+else
+ function copy_command(from, to)
+ -- TODO: What if `from` begins with a hypen?
+ return "cp " .. escape(from) .. " " .. escape(to)
+ end
+end
+
+local isfile = filesys.isfile or function(path)
+ return filesys.attributes(path, "mode") == "file"
+end
+
+local isdir = filesys.isdir or function(path)
+ return filesys.attributes(path, "mode") == "directory"
+end
+
+local function mkdir_rec(path)
+ local succ, err = filesys.mkdir(path)
+ if not succ then
+ succ, err = mkdir_rec(pathutil.parentdir(path))
+ if succ then
+ return filesys.mkdir(path)
+ end
+ end
+ return succ, err
+end
+
+local function remove_rec(path)
+ if isdir(path) then
+ for file in filesys.dir(path) do
+ if file ~= "." and file ~= ".." then
+ local succ, err = remove_rec(pathutil.join(path, file))
+ if not succ then
+ return succ, err
+ end
+ end
+ end
+ return filesys.rmdir(path)
+ else
+ return os_remove(path)
+ end
+end
+
+return {
+ copy_command = copy_command,
+ isfile = isfile,
+ isdir = isdir,
+ mkdir_rec = mkdir_rec,
+ remove_rec = remove_rec,
+}
+end
+package.preload["texrunner.option"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+-- options_and_params, i = parseoption(arg, options)
+-- options[i] = {short = "o", long = "option" [, param = true] [, boolean = true] [, allow_single_hyphen = false]}
+-- arg[i], arg[i + 1], ..., arg[#arg] are non-options
+local function parseoption(arg, options)
+ local i = 1
+ local option_and_params = {}
+ while i <= #arg do
+ if arg[i] == "--" then
+ -- Stop handling options
+ i = i + 1
+ break
+ elseif arg[i]:sub(1,2) == "--" then
+ -- Long option
+ local name,param = arg[i]:match("^([^=]+)=(.*)$", 3)
+ name = name or arg[i]:sub(3)
+ local opt = nil
+ for _,o in ipairs(options) do
+ if o.long then
+ if o.long == name then
+ if o.param then
+ if param then
+ -- --option=param
+ else
+ if o.default ~= nil then
+ param = o.default
+ else
+ -- --option param
+ assert(i + 1 <= #arg, "argument missing after " .. arg[i] .. " option")
+ param = arg[i + 1]
+ i = i + 1
+ end
+ end
+ else
+ -- --option
+ param = true
+ end
+ opt = o
+ break
+ elseif o.boolean and name == "no-" .. o.long then
+ -- --no-option
+ opt = o
+ break
+ end
+ end
+ end
+ if opt then
+ table.insert(option_and_params, {opt.long, param})
+ else
+ -- Unknown long option
+ error("unknown long option: " .. arg[i])
+ end
+ elseif arg[i]:sub(1,1) == "-" then
+ local name,param = arg[i]:match("^([^=]+)=(.*)$", 2)
+ name = name or arg[i]:sub(2)
+ local opt = nil
+ for _,o in ipairs(options) do
+ if o.long and o.allow_single_hyphen then
+ if o.long == name then
+ if o.param then
+ if param then
+ -- -option=param
+ else
+ if o.default ~= nil then
+ param = o.default
+ else
+ -- -option param
+ assert(i + 1 <= #arg, "argument missing after " .. arg[i] .. " option")
+ param = arg[i + 1]
+ i = i + 1
+ end
+ end
+ else
+ -- -option
+ param = true
+ end
+ opt = o
+ break
+ elseif o.boolean and name == "no-" .. o.long then
+ -- -no-option
+ opt = o
+ break
+ end
+ elseif o.long and #name >= 2 and (o.long == name or (o.boolean and name == "no-" .. o.long)) then
+ error("You must supply two hyphens (i.e. --" .. name .. ") for long option")
+ end
+ end
+ if opt == nil then
+ -- Short option
+ name = arg[i]:sub(2,2)
+ for _,o in ipairs(options) do
+ if o.short then
+ if o.short == name then
+ if o.param then
+ if #arg[i] > 2 then
+ -- -oparam
+ param = arg[i]:sub(3)
+ else
+ -- -o param
+ assert(i + 1 <= #arg, "argument missing after " .. arg[i] .. " option")
+ param = arg[i + 1]
+ i = i + 1
+ end
+ else
+ -- -o
+ assert(#arg[i] == 2, "combining multiple short options like -abc is not supported")
+ param = true
+ end
+ opt = o
+ break
+ end
+ end
+ end
+ end
+ if opt then
+ table.insert(option_and_params, {opt.long or opt.short, param})
+ else
+ error("unknown short option: " .. arg[i])
+ end
+ else
+ -- arg[i] is not an option
+ break
+ end
+ i = i + 1
+ end
+ return option_and_params, i
+end
+
+return {
+ parseoption = parseoption;
+}
+end
+package.preload["texrunner.tex_engine"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local table = table
+local setmetatable = setmetatable
+local ipairs = ipairs
+
+local shellutil = require "texrunner.shellutil"
+
+--[[
+engine.name: string
+engine.type = "onePass" or "twoPass"
+engine:build_command(inputfile, options)
+ options:
+ halt_on_error: boolean
+ interaction: string
+ file_line_error: boolean
+ synctex: string
+ shell_escape: boolean
+ shell_restricted: boolean
+ jobname: string
+ output_directory: string
+ extraoptions: a list of strings
+ output_format: "pdf" or "dvi"
+ draftmode: boolean (pdfTeX / XeTeX / LuaTeX)
+ fmt: string
+ tex_injection: string
+ lua_initialization_script: string (LuaTeX only)
+engine.executable: string
+engine.supports_pdf_generation: boolean
+engine.dvi_extension: string
+engine.supports_draftmode: boolean
+engine.is_luatex: true or nil
+]]
+
+local engine_meta = {}
+engine_meta.__index = engine_meta
+engine_meta.dvi_extension = "dvi"
+function engine_meta:build_command(inputfile, options)
+ local command = {self.executable, "-recorder"}
+ if options.fmt then
+ table.insert(command, "-fmt=" .. options.fmt)
+ end
+ if options.halt_on_error then
+ table.insert(command, "-halt-on-error")
+ end
+ if options.interaction then
+ table.insert(command, "-interaction=" .. options.interaction)
+ end
+ if options.file_line_error then
+ table.insert(command, "-file-line-error")
+ end
+ if options.synctex then
+ table.insert(command, "-synctex=" .. shellutil.escape(options.synctex))
+ end
+ if options.shell_escape == false then
+ table.insert(command, "-no-shell-escape")
+ elseif options.shell_restricted == true then
+ table.insert(command, "-shell-restricted")
+ elseif options.shell_escape == true then
+ table.insert(command, "-shell-escape")
+ end
+ if options.jobname then
+ table.insert(command, "-jobname=" .. shellutil.escape(options.jobname))
+ end
+ if options.output_directory then
+ table.insert(command, "-output-directory=" .. shellutil.escape(options.output_directory))
+ end
+ if self.handle_additional_options then
+ self:handle_additional_options(command, options)
+ end
+ if options.extraoptions then
+ for _,v in ipairs(options.extraoptions) do
+ table.insert(command, v)
+ end
+ end
+ if type(options.tex_injection) == "string" then
+ table.insert(command, shellutil.escape(options.tex_injection .. "\\input " .. inputfile)) -- TODO: what if filename contains spaces?
+ else
+ table.insert(command, shellutil.escape(inputfile))
+ end
+ return table.concat(command, " ")
+end
+
+local function engine(name, supports_pdf_generation, handle_additional_options)
+ return setmetatable({
+ name = name,
+ executable = name,
+ supports_pdf_generation = supports_pdf_generation,
+ handle_additional_options = handle_additional_options,
+ supports_draftmode = supports_pdf_generation,
+ }, engine_meta)
+end
+
+local function handle_pdftex_options(self, args, options)
+ if options.draftmode then
+ table.insert(args, "-draftmode")
+ elseif options.output_format == "dvi" then
+ table.insert(args, "-output-format=dvi")
+ end
+end
+
+local function handle_xetex_options(self, args, options)
+ if options.output_format == "dvi" or options.draftmode then
+ table.insert(args, "-no-pdf")
+ end
+end
+
+local function handle_luatex_options(self, args, options)
+ if options.lua_initialization_script then
+ table.insert(args, "--lua="..shellutil.escape(options.lua_initialization_script))
+ end
+ handle_pdftex_options(self, args, options)
+end
+
+local function is_luatex(e)
+ e.is_luatex = true
+ return e
+end
+
+local KnownEngines = {
+ ["pdftex"] = engine("pdftex", true, handle_pdftex_options),
+ ["pdflatex"] = engine("pdflatex", true, handle_pdftex_options),
+ ["luatex"] = is_luatex(engine("luatex", true, handle_luatex_options)),
+ ["lualatex"] = is_luatex(engine("lualatex", true, handle_luatex_options)),
+ ["luajittex"] = is_luatex(engine("luajittex", true, handle_luatex_options)),
+ ["xetex"] = engine("xetex", true, handle_xetex_options),
+ ["xelatex"] = engine("xelatex", true, handle_xetex_options),
+ ["tex"] = engine("tex", false),
+ ["etex"] = engine("etex", false),
+ ["latex"] = engine("latex", false),
+ ["ptex"] = engine("ptex", false),
+ ["eptex"] = engine("eptex", false),
+ ["platex"] = engine("platex", false),
+ ["uptex"] = engine("uptex", false),
+ ["euptex"] = engine("euptex", false),
+ ["uplatex"] = engine("uplatex", false),
+}
+
+KnownEngines["xetex"].dvi_extension = "xdv"
+KnownEngines["xelatex"].dvi_extension = "xdv"
+
+return KnownEngines
+end
+package.preload["texrunner.reruncheck"] = function(...)
+--[[
+ Copyright 2016,2018 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local io = io
+local assert = assert
+local filesys = require "lfs"
+local md5 = require "md5"
+local fsutil = require "texrunner.fsutil"
+local pathutil = require "texrunner.pathutil"
+local message = require "texrunner.message"
+
+local function md5sum_file(path)
+ local f = assert(io.open(path, "rb"))
+ local contents = f:read("*a")
+ f:close()
+ return md5.sum(contents)
+end
+
+-- filelist, filemap = parse_recorder_file("jobname.fls", options [, filelist, filemap])
+-- filelist[i] = {path = "...", abspath = "...", kind = "input" or "output" or "auxiliary"}
+local function parse_recorder_file(file, options, filelist, filemap)
+ filelist = filelist or {}
+ filemap = filemap or {}
+ for l in io.lines(file) do
+ local t,path = l:match("^(%w+) (.*)$")
+ if t == "PWD" then
+ -- Ignore
+
+ elseif t == "INPUT" then
+ local abspath = pathutil.abspath(path)
+ local fileinfo = filemap[abspath]
+ if not fileinfo then
+ if fsutil.isfile(path) then
+ local kind = "input"
+ local ext = pathutil.ext(path)
+ if ext == "bbl" then
+ kind = "auxiliary"
+ end
+ fileinfo = {path = path, abspath = abspath, kind = kind}
+ table.insert(filelist, fileinfo)
+ filemap[abspath] = fileinfo
+ else
+ -- Maybe a command execution
+ end
+ else
+ if #path < #fileinfo.path then
+ fileinfo.path = path
+ end
+ if fileinfo.kind == "output" then
+ -- The files listed in both INPUT and OUTPUT are considered to be auxiliary files.
+ fileinfo.kind = "auxiliary"
+ end
+ end
+
+ elseif t == "OUTPUT" then
+ local abspath = pathutil.abspath(path)
+ local fileinfo = filemap[abspath]
+ if not fileinfo then
+ local kind = "output"
+ local ext = pathutil.ext(path)
+ if ext == "out" then
+ -- hyperref bookmarks file
+ kind = "auxiliary"
+ elseif options.makeindex and ext == "idx" then
+ -- Treat .idx files (to be processed by MakeIndex) as auxiliary
+ kind = "auxiliary"
+ -- ...and .ind files
+ elseif ext == "bcf" then -- biber
+ kind = "auxiliary"
+ elseif ext == "glo" then -- makeglossaries
+ kind = "auxiliary"
+ end
+ fileinfo = {path = path, abspath = abspath, kind = kind}
+ table.insert(filelist, fileinfo)
+ filemap[abspath] = fileinfo
+ else
+ if #path < #fileinfo.path then
+ fileinfo.path = path
+ end
+ if fileinfo.kind == "input" then
+ -- The files listed in both INPUT and OUTPUT are considered to be auxiliary files.
+ fileinfo.kind = "auxiliary"
+ end
+ end
+
+ else
+ message.warning("Unrecognized line in recorder file '", file, "': ", l)
+ end
+ end
+ return filelist, filemap
+end
+
+-- auxstatus = collectfileinfo(filelist [, auxstatus])
+local function collectfileinfo(filelist, auxstatus)
+ auxstatus = auxstatus or {}
+ for i,fileinfo in ipairs(filelist) do
+ local path = fileinfo.abspath
+ if fsutil.isfile(path) then
+ local status = auxstatus[path] or {}
+ auxstatus[path] = status
+ if fileinfo.kind == "input" then
+ status.mtime = status.mtime or filesys.attributes(path, "modification")
+ elseif fileinfo.kind == "auxiliary" then
+ status.mtime = status.mtime or filesys.attributes(path, "modification")
+ status.size = status.size or filesys.attributes(path, "size")
+ status.md5sum = status.md5sum or md5sum_file(path)
+ end
+ end
+ end
+ return auxstatus
+end
+
+local function binarytohex(s)
+ return (s:gsub(".", function(c) return string.format("%02x", string.byte(c)) end))
+end
+
+-- should_rerun, newauxstatus = comparefileinfo(auxfiles, auxstatus)
+local function comparefileinfo(filelist, auxstatus)
+ local should_rerun = false
+ local newauxstatus = {}
+ for i,fileinfo in ipairs(filelist) do
+ local path = fileinfo.abspath
+ if fsutil.isfile(path) then
+ if fileinfo.kind == "input" then
+ -- Input file: User might have modified while running TeX.
+ local mtime = filesys.attributes(path, "modification")
+ if auxstatus[path] and auxstatus[path].mtime then
+ if auxstatus[path].mtime < mtime then
+ -- Input file was updated during execution
+ message.info("Input file '", fileinfo.path, "' was modified (by user, or some external commands).")
+ newauxstatus[path] = {mtime = mtime}
+ return true, newauxstatus
+ end
+ else
+ -- New input file
+ end
+
+ elseif fileinfo.kind == "auxiliary" then
+ -- Auxiliary file: Compare file contents.
+ if auxstatus[path] then
+ -- File was touched during execution
+ local really_modified = false
+ local modified_because = nil
+ local size = filesys.attributes(path, "size")
+ if auxstatus[path].size ~= size then
+ really_modified = true
+ if auxstatus[path].size then
+ modified_because = string.format("size: %d -> %d", auxstatus[path].size, size)
+ else
+ modified_because = string.format("size: (N/A) -> %d", size)
+ end
+ newauxstatus[path] = {size = size}
+ else
+ local md5sum = md5sum_file(path)
+ if auxstatus[path].md5sum ~= md5sum then
+ really_modified = true
+ if auxstatus[path].md5sum then
+ modified_because = string.format("md5: %s -> %s", binarytohex(auxstatus[path].md5sum), binarytohex(md5sum))
+ else
+ modified_because = string.format("md5: (N/A) -> %s", binarytohex(md5sum))
+ end
+ end
+ newauxstatus[path] = {size = size, md5sum = md5sum}
+ end
+ if really_modified then
+ message.info("File '", fileinfo.path, "' was modified (", modified_because, ").")
+ should_rerun = true
+ else
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("File '", fileinfo.path, "' unmodified (size and md5sum).")
+ end
+ end
+ else
+ -- New file
+ if path:sub(-4) == ".aux" then
+ local size = filesys.attributes(path, "size")
+ if size == 8 then
+ local auxfile = io.open(path, "rb")
+ local contents = auxfile:read("*a")
+ auxfile:close()
+ if contents == "\\relax \n" then
+ -- The .aux file is new, but it is almost empty
+ else
+ should_rerun = true
+ end
+ newauxstatus[path] = {size = size, md5sum = md5.sum(contents)}
+ else
+ should_rerun = true
+ newauxstatus[path] = {size = size}
+ end
+ else
+ should_rerun = true
+ end
+ if should_rerun then
+ message.info("New auxiliary file '", fileinfo.path, "'.")
+ else
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("Ignoring almost-empty auxiliary file '", fileinfo.path, "'.")
+ end
+ end
+ end
+ if should_rerun then
+ break
+ end
+ end
+ else
+ -- Auxiliary file is not really a file???
+ end
+ end
+ return should_rerun, newauxstatus
+end
+
+-- true if src is newer than dst
+local function comparefiletime(srcpath, dstpath, auxstatus)
+ if not filesys.isfile(dstpath) then
+ return true
+ end
+ local src_info = auxstatus[srcpath]
+ if src_info then
+ local src_mtime = src_info.mtime
+ if src_mtime then
+ local dst_mtime = filesys.attributes(dstpath, "modification")
+ return src_mtime > dst_mtime
+ end
+ end
+ return false
+end
+
+return {
+ parse_recorder_file = parse_recorder_file;
+ collectfileinfo = collectfileinfo;
+ comparefileinfo = comparefileinfo;
+ comparefiletime = comparefiletime;
+}
+end
+package.preload["texrunner.auxfile"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local string_match = string.match
+local pathutil = require "texrunner.pathutil"
+local filesys = require "lfs"
+local fsutil = require "texrunner.fsutil"
+local message = require "texrunner.message"
+
+-- for LaTeX
+local function parse_aux_file(auxfile, outdir, report, seen)
+ report = report or {}
+ seen = seen or {}
+ seen[auxfile] = true
+ for l in io.lines(auxfile) do
+ local subauxfile = string_match(l, "\\@input{(.+)}")
+ if subauxfile then
+ if fsutil.isfile(subauxfile) then
+ parse_aux_file(pathutil.join(outdir, subauxfile), outdir, report, seen)
+ else
+ local dir = pathutil.join(outdir, pathutil.dirname(subauxfile))
+ if not fsutil.isdir(dir) then
+ assert(fsutil.mkdir_rec(dir))
+ report.made_new_directory = true
+ end
+ end
+ end
+ end
+ return report
+end
+
+-- \citation, \bibdata, \bibstyle and \@input
+local function extract_bibtex_from_aux_file(auxfile, outdir, biblines)
+ biblines = biblines or {}
+ for l in io.lines(auxfile) do
+ local name = string_match(l, "\\([%a@]+)")
+ if name == "citation" or name == "bibdata" or name == "bibstyle" then
+ table.insert(biblines, l)
+ if CLUTTEX_VERBOSITY >= 2 then
+ message.info("BibTeX line: ", l)
+ end
+ elseif name == "@input" then
+ local subauxfile = string_match(l, "\\@input{(.+)}")
+ if subauxfile and fsutil.isfile(subauxfile) then
+ extract_bibtex_from_aux_file(pathutil.join(outdir, subauxfile), outdir, biblines)
+ end
+ end
+ end
+ return biblines
+end
+
+return {
+ parse_aux_file = parse_aux_file,
+ extract_bibtex_from_aux_file = extract_bibtex_from_aux_file,
+}
+end
+package.preload["texrunner.luatexinit"] = function(...)
+local function create_initialization_script(filename, options)
+ local initscript = assert(io.open(filename,"w"))
+ if type(options.file_line_error) == "boolean" then
+ initscript:write(string.format("texconfig.file_line_error = %s\n", options.file_line_error))
+ end
+ if type(options.halt_on_error) == "boolean" then
+ initscript:write(string.format("texconfig.halt_on_error = %s\n", options.halt_on_error))
+ end
+ initscript:write([==[
+local print = print
+local io_open = io.open
+local io_write = io.write
+local os_execute = os.execute
+local texio_write = texio.write
+local texio_write_nl = texio.write_nl
+]==])
+
+ -- Packages coded in Lua doesn't follow -output-directory option and doesn't write command to the log file
+ initscript:write(string.format("local output_directory = %q\n", options.output_directory))
+ initscript:write([==[
+local luawritelog
+local function openluawritelog()
+ if not luawritelog then
+ luawritelog = assert(io_open(output_directory .. "/" .. tex.jobname .. ".cluttex-fls", "w"))
+ end
+ return luawritelog
+end
+io.open = function(fname, mode)
+ -- luatexja-ruby
+ if mode == "w" and fname == tex.jobname .. ".ltjruby" then
+ fname = output_directory .. "/" .. fname
+ end
+ if type(mode) == "string" and string.find(mode, "w") ~= nil then
+ -- write mode
+ openluawritelog():write("OUTPUT " .. fname .. "\n")
+ end
+ return io_open(fname, mode)
+end
+os.execute = function(...)
+ texio_write_nl("log", string.format("CLUTTEX_EXEC %s", ...), "")
+ return os_execute(...)
+end
+]==])
+
+ -- Silence some of the TeX output to the terminal.
+ initscript:write([==[
+local function start_file_cb(category, filename)
+ if category == 1 then -- a normal data file, like a TeX source
+ texio_write_nl("log", "("..filename)
+ elseif category == 2 then -- a font map coupling font names to resources
+ texio_write("log", "{"..filename)
+ elseif category == 3 then -- an image file (png, pdf, etc)
+ texio_write("<"..filename)
+ elseif category == 4 then -- an embedded font subset
+ texio_write("<"..filename)
+ elseif category == 5 then -- a fully embedded font
+ texio_write("<<"..filename)
+ else
+ print("start_file: unknown category", category, filename)
+ end
+end
+callback.register("start_file", start_file_cb)
+local function stop_file_cb(category)
+ if category == 1 then
+ texio_write("log", ")")
+ elseif category == 2 then
+ texio_write("log", "}")
+ elseif category == 3 then
+ texio_write(">")
+ elseif category == 4 then
+ texio_write(">")
+ elseif category == 5 then
+ texio_write(">>")
+ else
+ print("stop_file: unknown category", category)
+ end
+end
+callback.register("stop_file", stop_file_cb)
+texio.write = function(...)
+ if select("#",...) == 1 then
+ -- Suppress luaotfload's message (See src/fontloader/runtime/fontload-reference.lua)
+ local s = ...
+ if string.match(s, "^%(using cache: ")
+ or string.match(s, "^%(using write cache: ")
+ or string.match(s, "^%(using read cache: ")
+ or string.match(s, "^%(load luc: ")
+ or string.match(s, "^%(load cache: ") then
+ return texio_write("log", ...)
+ end
+ end
+ return texio_write(...)
+end
+]==])
+ initscript:close()
+end
+
+return {
+ create_initialization_script = create_initialization_script
+}
+end
+package.preload["texrunner.recovery"] = function(...)
+--[[
+ Copyright 2018 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local io = io
+local string = string
+local parse_aux_file = require "texrunner.auxfile".parse_aux_file
+local pathutil = require "texrunner.pathutil"
+local fsutil = require "texrunner.fsutil"
+local shellutil = require "texrunner.shellutil"
+local message = require "texrunner.message"
+
+local function create_missing_directories(args)
+ if string.find(args.execlog, "I can't write on file", 1, true) then
+ -- There is a possibility that there are some subfiles under subdirectories.
+ -- Directories for sub-auxfiles are not created automatically, so we need to provide them.
+ local report = parse_aux_file(args.auxfile, args.options.output_directory)
+ if report.made_new_directory then
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("Created missing directories.")
+ end
+ return true
+ end
+ end
+ return false
+end
+
+local function run_epstopdf(args)
+ local run = false
+ if args.options.shell_escape ~= false then -- (possibly restricted) \write18 enabled
+ for outfile, infile in string.gmatch(args.execlog, "%(epstopdf%)%s*Command: <r?epstopdf %-%-outfile=([%w%-/]+%.pdf) ([%w%-/]+%.eps)>") do
+ local infile_abs = pathutil.abspath(infile, args.original_wd)
+ if fsutil.isfile(infile_abs) then -- input file exists
+ local outfile_abs = pathutil.abspath(outfile, args.options.output_directory)
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("Running epstopdf on ", infile, ".")
+ end
+ local outdir = pathutil.dirname(outfile_abs)
+ if not fsutil.isdir(outdir) then
+ assert(fsutil.mkdir_rec(outdir))
+ end
+ local command = string.format("epstopdf --outfile=%s %s", shellutil.escape(outfile_abs), shellutil.escape(infile_abs))
+ message.exec(command)
+ local success = os.execute(command)
+ if type(success) == "number" then -- Lua 5.1 or LuaTeX
+ success = success == 0
+ end
+ run = run or success
+ end
+ end
+ end
+ return run
+end
+
+local function check_minted(args)
+ return string.find(args.execlog, "Package minted Error: Missing Pygments output; \\inputminted was") ~= nil
+end
+
+local function try_recovery(args)
+ local recovered = false
+ recovered = create_missing_directories(args)
+ recovered = run_epstopdf(args) or recovered
+ recovered = check_minted(args) or recovered
+ return recovered
+end
+
+return {
+ create_missing_directories = create_missing_directories,
+ run_epstopdf = run_epstopdf,
+ try_recovery = try_recovery,
+}
+end
+package.preload["texrunner.handleoption"] = function(...)
+local COPYRIGHT_NOTICE = [[
+Copyright (C) 2016,2018 ARATA Mizuki
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local pathutil = require "texrunner.pathutil"
+local shellutil = require "texrunner.shellutil"
+local parseoption = require "texrunner.option".parseoption
+local KnownEngines = require "texrunner.tex_engine"
+local message = require "texrunner.message"
+
+local function usage(arg)
+ io.write(string.format([[
+ClutTeX: Process TeX files without cluttering your working directory
+
+Usage:
+ %s [options] [--] FILE.tex
+
+Options:
+ -e, --engine=ENGINE Specify which TeX engine to use.
+ ENGINE is one of the following:
+ pdflatex, pdftex,
+ lualatex, luatex, luajittex,
+ xelatex, xetex, latex, etex, tex,
+ platex, eptex, ptex,
+ uplatex, euptex, uptex,
+ -o, --output=FILE The name of output file.
+ [default: JOBNAME.pdf or JOBNAME.dvi]
+ --fresh Clean intermediate files before running TeX.
+ Cannot be used with --output-directory.
+ --max-iterations=N Maximum number of running TeX to resolve
+ cross-references. [default: 3]
+ --start-with-draft Start with draft mode.
+ --[no-]change-directory Change directory before running TeX.
+ --watch Watch input files for change. Requires fswatch
+ program to be installed.
+ --tex-option=OPTION Pass OPTION to TeX as a single option.
+ --tex-options=OPTIONs Pass OPTIONs to TeX as multiple options.
+ --dvipdfmx-option[s]=OPTION[s] Same for dvipdfmx.
+ --makeindex=COMMAND+OPTIONs Command to generate index, such as
+ `makeindex' or `mendex'.
+ --bibtex=COMMAND+OPTIONs Command for BibTeX, such as
+ `bibtex' or `pbibtex'.
+ --biber[=COMMAND+OPTIONs] Command for Biber.
+ --makeglossaries[=COMMAND+OPTIONs] Command for makeglossaries.
+ -h, --help Print this message and exit.
+ -v, --version Print version information and exit.
+ -V, --verbose Be more verbose.
+ --color=WHEN Make ClutTeX's message colorful. WHEN is one of
+ `always', `auto', or `never'. [default: auto]
+ --includeonly=NAMEs Insert '\includeonly{NAMEs}'.
+
+ --[no-]shell-escape
+ --shell-restricted
+ --synctex=NUMBER
+ --fmt=FMTNAME
+ --[no-]file-line-error [default: yes]
+ --[no-]halt-on-error [default: yes]
+ --interaction=STRING [default: nonstopmode]
+ --jobname=STRING
+ --output-directory=DIR [default: somewhere in the temporary directory]
+ --output-format=FORMAT FORMAT is `pdf' or `dvi'. [default: pdf]
+
+%s
+]], arg[0] or 'texlua cluttex.lua', COPYRIGHT_NOTICE))
+end
+
+local option_spec = {
+ -- Options for ClutTeX
+ {
+ short = "e",
+ long = "engine",
+ param = true,
+ },
+ {
+ short = "o",
+ long = "output",
+ param = true,
+ },
+ {
+ long = "fresh",
+ },
+ {
+ long = "max-iterations",
+ param = true,
+ },
+ {
+ long = "start-with-draft",
+ },
+ {
+ long = "change-directory",
+ boolean = true,
+ },
+ {
+ long = "watch",
+ },
+ {
+ short = "h",
+ long = "help",
+ allow_single_hyphen = true,
+ },
+ {
+ short = "v",
+ long = "version",
+ },
+ {
+ short = "V",
+ long = "verbose",
+ },
+ {
+ long = "color",
+ param = true,
+ default = "always",
+ },
+ {
+ long = "includeonly",
+ param = true,
+ },
+ -- Options for TeX
+ {
+ long = "synctex",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "file-line-error",
+ boolean = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "interaction",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "halt-on-error",
+ boolean = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "shell-escape",
+ boolean = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "shell-restricted",
+ allow_single_hyphen = true,
+ },
+ {
+ long = "jobname",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "fmt",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "output-directory",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "output-format",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "tex-option",
+ param = true,
+ },
+ {
+ long = "tex-options",
+ param = true,
+ },
+ {
+ long = "dvipdfmx-option",
+ param = true,
+ },
+ {
+ long = "dvipdfmx-options",
+ param = true,
+ },
+ {
+ long = "makeindex",
+ param = true,
+ },
+ {
+ long = "bibtex",
+ param = true,
+ },
+ {
+ long = "biber",
+ param = true,
+ default = "biber",
+ },
+ {
+ long = "makeglossaries",
+ param = true,
+ default = "makeglossaries",
+ },
+}
+
+-- Default values for options
+local function set_default_values(options)
+ if options.max_iterations == nil then
+ options.max_iterations = 3
+ end
+
+ if options.interaction == nil then
+ options.interaction = "nonstopmode"
+ end
+
+ if options.file_line_error == nil then
+ options.file_line_error = true
+ end
+
+ if options.halt_on_error == nil then
+ options.halt_on_error = true
+ end
+end
+
+-- inputfile, engine, options = handle_cluttex_options(arg)
+local function handle_cluttex_options(arg)
+ -- Parse options
+ local option_and_params, non_option_index = parseoption(arg, option_spec)
+
+ -- Handle options
+ local options = {
+ tex_extraoptions = {},
+ dvipdfmx_extraoptions = {},
+ }
+ CLUTTEX_VERBOSITY = 0
+ for _,option in ipairs(option_and_params) do
+ local name = option[1]
+ local param = option[2]
+
+ if name == "engine" then
+ assert(options.engine == nil, "multiple --engine options")
+ options.engine = param
+
+ elseif name == "output" then
+ assert(options.output == nil, "multiple --output options")
+ options.output = param
+
+ elseif name == "fresh" then
+ assert(options.fresh == nil, "multiple --fresh options")
+ options.fresh = true
+
+ elseif name == "max-iterations" then
+ assert(options.max_iterations == nil, "multiple --max-iterations options")
+ options.max_iterations = assert(tonumber(param), "invalid value for --max-iterations option")
+ assert(options.max_iterations >= 1, "invalid value for --max-iterations option")
+
+ elseif name == "start-with-draft" then
+ assert(options.start_with_draft == nil, "multiple --start-with-draft options")
+ options.start_with_draft = true
+
+ elseif name == "watch" then
+ assert(options.watch == nil, "multiple --watch options")
+ options.watch = true
+
+ elseif name == "help" then
+ usage(arg)
+ os.exit(0)
+
+ elseif name == "version" then
+ io.stderr:write("cluttex ",CLUTTEX_VERSION,"\n")
+ os.exit(0)
+
+ elseif name == "verbose" then
+ CLUTTEX_VERBOSITY = CLUTTEX_VERBOSITY + 1
+
+ elseif name == "color" then
+ assert(options.color == nil, "multiple --collor options")
+ options.color = param
+ message.set_colors(options.color)
+
+ elseif name == "change-directory" then
+ assert(options.change_directory == nil, "multiple --change-directory options")
+ options.change_directory = param
+
+ elseif name == "includeonly" then
+ assert(options.includeonly == nil, "multiple --includeonly options")
+ options.includeonly = param
+
+ -- Options for TeX
+ elseif name == "synctex" then
+ assert(options.synctex == nil, "multiple --synctex options")
+ options.synctex = param
+
+ elseif name == "file-line-error" then
+ options.file_line_error = param
+
+ elseif name == "interaction" then
+ assert(options.interaction == nil, "multiple --interaction options")
+ assert(param == "batchmode" or param == "nonstopmode" or param == "scrollmode" or param == "errorstopmode", "invalid argument for --interaction")
+ options.interaction = param
+
+ elseif name == "halt-on-error" then
+ options.halt_on_error = param
+
+ elseif name == "shell-escape" then
+ assert(options.shell_escape == nil and options.shell_restricted == nil, "multiple --(no-)shell-escape or --shell-restricted options")
+ options.shell_escape = param
+
+ elseif name == "shell-restricted" then
+ assert(options.shell_escape == nil and options.shell_restricted == nil, "multiple --(no-)shell-escape or --shell-restricted options")
+ options.shell_restricted = true
+
+ elseif name == "jobname" then
+ assert(options.jobname == nil, "multiple --jobname options")
+ options.jobname = param
+
+ elseif name == "fmt" then
+ assert(options.fmt == nil, "multiple --fmt options")
+ options.fmt = param
+
+ elseif name == "output-directory" then
+ assert(options.output_directory == nil, "multiple --output-directory options")
+ options.output_directory = param
+
+ elseif name == "output-format" then
+ assert(options.output_format == nil, "multiple --output-format options")
+ assert(param == "pdf" or param == "dvi", "invalid argument for --output-format")
+ options.output_format = param
+
+ elseif name == "tex-option" then
+ table.insert(options.tex_extraoptions, shellutil.escape(param))
+
+ elseif name == "tex-options" then
+ table.insert(options.tex_extraoptions, param)
+
+ elseif name == "dvipdfmx-option" then
+ table.insert(options.dvipdfmx_extraoptions, shellutil.escape(param))
+
+ elseif name == "dvipdfmx-options" then
+ table.insert(options.dvipdfmx_extraoptions, param)
+
+ elseif name == "makeindex" then
+ assert(options.makeindex == nil, "multiple --makeindex options")
+ options.makeindex = param
+
+ elseif name == "bibtex" then
+ assert(options.bibtex == nil, "multiple --bibtex options")
+ assert(options.biber == nil, "multiple --bibtex/--biber options")
+ options.bibtex = param
+
+ elseif name == "biber" then
+ assert(options.biber == nil, "multiple --biber options")
+ assert(options.bibtex == nil, "multiple --bibtex/--biber options")
+ options.biber = param
+
+ elseif name == "makeglossaries" then
+ assert(options.makeglossaries == nil, "multiple --makeglossaries options")
+ options.makeglossaries = param
+
+ end
+ end
+
+ if options.color == nil then
+ message.set_colors("auto")
+ end
+
+ -- Handle non-options (i.e. input file)
+ if non_option_index > #arg then
+ -- No input file given
+ usage(arg)
+ os.exit(1)
+ elseif non_option_index < #arg then
+ message.error("Multiple input files are not supported.")
+ os.exit(1)
+ end
+ local inputfile = arg[non_option_index]
+
+ -- If run as 'cllualatex', then the default engine is lualatex
+ if options.engine == nil and type(arg[0]) == "string" then
+ local basename = pathutil.trimext(pathutil.basename(arg[0]))
+ local engine_part = string.match(basename, "^cl(%w+)$")
+ if engine_part and KnownEngines[engine_part] then
+ options.engine = engine_part
+ end
+ end
+
+ if options.engine == nil then
+ message.error("Engine not specified.")
+ os.exit(1)
+ end
+ local engine = KnownEngines[options.engine]
+ if not engine then
+ message.error("Unknown engine name '", options.engine, "'.")
+ os.exit(1)
+ end
+
+ set_default_values(options)
+
+ return inputfile, engine, options
+end
+
+return {
+ usage = usage,
+ handle_cluttex_options = handle_cluttex_options,
+}
+end
+package.preload["texrunner.isatty"] = function(...)
+--[[
+ Copyright 2018 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+if os.type == "unix" then
+ -- Try luaposix
+ local succ, M = pcall(function()
+ local isatty = require "posix.unistd".isatty
+ local fileno = require "posix.stdio".fileno
+ return {
+ isatty = function(file)
+ return isatty(fileno(file)) == 1
+ end,
+ }
+ end)
+ if succ then
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: isatty found via luaposix\n")
+ end
+ return M
+ else
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: luaposix not found: ", M, "\n")
+ end
+ end
+
+ -- Try LuaJIT-like FFI
+ local succ, M = pcall(function()
+ local ffi = require "ffi"
+ ffi.cdef[[
+int isatty(int fd);
+int fileno(void *stream);
+]]
+ local isatty = assert(ffi.C.isatty, "isatty not found")
+ local fileno = assert(ffi.C.fileno, "fileno not found")
+ return {
+ isatty = function(file)
+ -- LuaJIT converts Lua's file handles into FILE* (void*)
+ return isatty(fileno(file)) ~= 0
+ end
+ }
+ end)
+ if succ then
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: isatty found via FFI (Unix)\n")
+ end
+ return M
+ else
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: FFI (Unix) not found: ", M, "\n")
+ end
+ end
+
+else
+ -- Try LuaJIT
+ -- TODO: Try to detect MinTTY using GetFileInformationByHandleEx
+ local succ, M = pcall(function()
+ local ffi = require "ffi"
+ local bitlib = assert(bit32 or bit, "Neither bit32 (Lua 5.2) nor bit (LuaJIT) found") -- Lua 5.2 or LuaJIT
+ ffi.cdef[[
+int _isatty(int fd);
+int _fileno(void *stream);
+void *_get_osfhandle(int fd); // should return intptr_t
+typedef int BOOL;
+typedef uint32_t DWORD;
+typedef int FILE_INFO_BY_HANDLE_CLASS; // ???
+typedef struct _FILE_NAME_INFO {
+DWORD FileNameLength;
+uint16_t FileName[?];
+} FILE_NAME_INFO;
+DWORD GetFileType(void *hFile);
+BOOL GetFileInformationByHandleEx(void *hFile, FILE_INFO_BY_HANDLE_CLASS fic, void *fileinfo, DWORD dwBufferSize);
+BOOL GetConsoleMode(void *hConsoleHandle, DWORD* lpMode);
+BOOL SetConsoleMode(void *hConsoleHandle, DWORD dwMode);
+]]
+ local isatty = assert(ffi.C._isatty, "_isatty not found")
+ local fileno = assert(ffi.C._fileno, "_fileno not found")
+ local get_osfhandle = assert(ffi.C._get_osfhandle, "_get_osfhandle not found")
+ local GetFileType = assert(ffi.C.GetFileType, "GetFileType not found")
+ local GetFileInformationByHandleEx = assert(ffi.C.GetFileInformationByHandleEx, "GetFileInformationByHandleEx not found")
+ local GetConsoleMode = assert(ffi.C.GetConsoleMode, "GetConsoleMode not found")
+ local SetConsoleMode = assert(ffi.C.SetConsoleMode, "SetConsoleMode not found")
+ local function wide_to_narrow(array, length)
+ local t = {}
+ for i = 0, length - 1 do
+ table.insert(t, string.char(math.min(array[i], 0xff)))
+ end
+ return table.concat(t, "")
+ end
+ local function is_mintty(fd)
+ local handle = get_osfhandle(fd)
+ local filetype = GetFileType(handle)
+ if filetype ~= 0x0003 then -- not FILE_TYPE_PIPE (0x0003)
+ -- mintty must be a pipe
+ if CLUTTEX_VERBOSITY >= 4 then
+ io.stderr:write("ClutTeX: not a pipe\n")
+ end
+ return false
+ end
+ local nameinfo = ffi.new("FILE_NAME_INFO", 32768)
+ local FileNameInfo = 2 -- : FILE_INFO_BY_HANDLE_CLASS
+ if GetFileInformationByHandleEx(handle, FileNameInfo, nameinfo, ffi.sizeof("FILE_NAME_INFO", 32768)) ~= 0 then
+ local filename = wide_to_narrow(nameinfo.FileName, math.floor(nameinfo.FileNameLength / 2))
+ -- \(cygwin|msys)-<hex digits>-pty<N>-(from|to)-master
+ if CLUTTEX_VERBOSITY >= 4 then
+ io.stderr:write("ClutTeX: GetFileInformationByHandleEx returned ", filename, "\n")
+ end
+ local a, b = string.match(filename, "^\\(%w+)%-%x+%-pty%d+%-(%w+)%-master$")
+ if (a == "cygwin" or a == "msys") and (b == "from" or b == "to") then
+ return true
+ end
+ else
+ if CLUTTEX_VERBOSITY >= 4 then
+ io.stderr:write("ClutTeX: GetFileInformationByHandleEx failed\n")
+ end
+ end
+ return false
+ end
+ return {
+ isatty = function(file)
+ -- LuaJIT converts Lua's file handles into FILE* (void*)
+ local fd = fileno(file)
+ return isatty(fd) ~= 0 or is_mintty(fd)
+ end,
+ enable_console_colors = function(file)
+ local fd = fileno(file)
+ if isatty(fd) ~= 0 then
+ local handle = get_osfhandle(fd)
+ local modePtr = ffi.new("DWORD[1]")
+ local result = GetConsoleMode(handle, modePtr)
+ if result ~= 0 then
+ local ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
+ result = SetConsoleMode(handle, bitlib.bor(modePtr[0], ENABLE_VIRTUAL_TERMINAL_PROCESSING))
+ if result == 0 then
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: SetConsoleMode failed\n")
+ end
+ end
+ else
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: GetConsoleMode failed\n")
+ end
+ end
+ end
+ end,
+ }
+ end)
+ if succ then
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: isatty found via FFI (Windows)\n")
+ end
+ return M
+ else
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: FFI (Windows) not found: ", M, "\n")
+ end
+ end
+end
+
+return {
+ isatty = function(file)
+ return false
+ end,
+}
+end
+package.preload["texrunner.message"] = function(...)
+--[[
+ Copyright 2018 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local use_colors = false
+
+local function set_colors(mode)
+ local M
+ if mode == "always" then
+ use_colors = true
+ M = require "texrunner.isatty"
+ if M.enable_console_colors then
+ M.enable_console_colors(io.stderr)
+ end
+ elseif mode == "never" then
+ use_colors = false
+ elseif mode == "auto" then
+ M = require "texrunner.isatty"
+ use_colors = M.isatty(io.stderr)
+ else
+ error "The value of --color option must be one of 'auto', 'always', or 'never'."
+ end
+ if use_colors and M.enable_console_colors then
+ M.enable_console_colors(io.stderr)
+ end
+end
+
+-- ESCAPE: hex 1B = dec 27 = oct 33
+
+local CMD = {
+ reset = "\027[0m",
+ underline = "\027[4m",
+ fg_black = "\027[30m",
+ fg_red = "\027[31m",
+ fg_green = "\027[32m",
+ fg_yellow = "\027[33m",
+ fg_blue = "\027[34m",
+ fg_magenta = "\027[35m",
+ fg_cyan = "\027[36m",
+ fg_white = "\027[37m",
+ fg_reset = "\027[39m",
+ bg_black = "\027[40m",
+ bg_red = "\027[41m",
+ bg_green = "\027[42m",
+ bg_yellow = "\027[43m",
+ bg_blue = "\027[44m",
+ bg_magenta = "\027[45m",
+ bg_cyan = "\027[46m",
+ bg_white = "\027[47m",
+ bg_reset = "\027[49m",
+ fg_x_black = "\027[90m",
+ fg_x_red = "\027[91m",
+ fg_x_green = "\027[92m",
+ fg_x_yellow = "\027[93m",
+ fg_x_blue = "\027[94m",
+ fg_x_magenta = "\027[95m",
+ fg_x_cyan = "\027[96m",
+ fg_x_white = "\027[97m",
+ bg_x_black = "\027[100m",
+ bg_x_red = "\027[101m",
+ bg_x_green = "\027[102m",
+ bg_x_yellow = "\027[103m",
+ bg_x_blue = "\027[104m",
+ bg_x_magenta = "\027[105m",
+ bg_x_cyan = "\027[106m",
+ bg_x_white = "\027[107m",
+}
+
+local function exec_msg(commandline)
+ if use_colors then
+ io.stderr:write(CMD.fg_x_white, CMD.bg_red, "[EXEC]", CMD.reset, " ", CMD.fg_red, commandline, CMD.reset, "\n")
+ else
+ io.stderr:write("[EXEC] ", commandline, "\n")
+ end
+end
+
+local function error_msg(...)
+ local message = table.concat({...}, "")
+ if use_colors then
+ io.stderr:write(CMD.fg_x_white, CMD.bg_red, "[ERROR]", CMD.reset, " ", CMD.fg_red, message, CMD.reset, "\n")
+ else
+ io.stderr:write("[ERROR] ", message, "\n")
+ end
+end
+
+local function warn_msg(...)
+ local message = table.concat({...}, "")
+ if use_colors then
+ io.stderr:write(CMD.fg_x_white, CMD.bg_red, "[WARN]", CMD.reset, " ", CMD.fg_blue, message, CMD.reset, "\n")
+ else
+ io.stderr:write("[WARN] ", message, "\n")
+ end
+end
+
+local function diag_msg(...)
+ local message = table.concat({...}, "")
+ if use_colors then
+ io.stderr:write(CMD.fg_x_white, CMD.bg_red, "[DIAG]", CMD.reset, " ", CMD.fg_blue, message, CMD.reset, "\n")
+ else
+ io.stderr:write("[DIAG] ", message, "\n")
+ end
+end
+
+local function info_msg(...)
+ local message = table.concat({...}, "")
+ if use_colors then
+ io.stderr:write(CMD.fg_x_white, CMD.bg_red, "[INFO]", CMD.reset, " ", CMD.fg_magenta, message, CMD.reset, "\n")
+ else
+ io.stderr:write("[INFO] ", message, "\n")
+ end
+end
+
+return {
+ set_colors = set_colors,
+ exec = exec_msg,
+ error = error_msg,
+ warn = warn_msg,
+ diag = diag_msg,
+ info = info_msg,
+}
+end
+--[[
+ Copyright 2016,2018 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+CLUTTEX_VERSION = "v0.1"
+
+-- Standard libraries
+local coroutine = coroutine
+local tostring = tostring
+
+-- External libraries (included in texlua)
+local filesys = require "lfs"
+local md5 = require "md5"
+-- local kpse = require "kpse"
+
+-- My own modules
+local pathutil = require "texrunner.pathutil"
+local fsutil = require "texrunner.fsutil"
+local shellutil = require "texrunner.shellutil"
+local reruncheck = require "texrunner.reruncheck"
+local luatexinit = require "texrunner.luatexinit"
+local recoverylib = require "texrunner.recovery"
+local message = require "texrunner.message"
+local extract_bibtex_from_aux_file = require "texrunner.auxfile".extract_bibtex_from_aux_file
+local handle_cluttex_options = require "texrunner.handleoption".handle_cluttex_options
+
+-- arguments: input file name, jobname, etc...
+local function genOutputDirectory(...)
+ -- The name of the temporary directory is based on the path of input file.
+ local message = table.concat({...}, "\0")
+ local hash = md5.sumhexa(message)
+ local tmpdir = os.getenv("TMPDIR") or os.getenv("TMP") or os.getenv("TEMP")
+ if tmpdir == nil then
+ local home = os.getenv("HOME") or os.getenv("USERPROFILE") or error("environment variable 'TMPDIR' not set!")
+ tmpdir = pathutil.join(home, ".latex-build-temp")
+ end
+ return pathutil.join(tmpdir, 'latex-build-' .. hash)
+end
+
+local inputfile, engine, options = handle_cluttex_options(arg)
+
+local jobname = options.jobname or pathutil.basename(pathutil.trimext(inputfile))
+assert(jobname ~= "", "jobname cannot be empty")
+
+if options.output_format == nil then
+ options.output_format = "pdf"
+end
+local output_extension
+if options.output_format == "dvi" then
+ output_extension = engine.dvi_extension or "dvi"
+else
+ output_extension = "pdf"
+end
+
+if options.output == nil then
+ options.output = jobname .. "." .. output_extension
+end
+
+-- Prepare output directory
+if options.output_directory == nil then
+ local inputfile_abs = pathutil.abspath(inputfile)
+ options.output_directory = genOutputDirectory(inputfile_abs, jobname, options.engine)
+
+ if not fsutil.isdir(options.output_directory) then
+ assert(fsutil.mkdir_rec(options.output_directory))
+
+ elseif options.fresh then
+ -- The output directory exists and --fresh is given:
+ -- Remove all files in the output directory
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("Cleaning '", options.output_directory, "'...")
+ end
+ assert(fsutil.remove_rec(options.output_directory))
+ assert(filesys.mkdir(options.output_directory))
+ end
+
+elseif options.fresh then
+ message.error("--fresh and --output-directory cannot be used together.")
+ os.exit(1)
+end
+
+local pathsep = ":"
+if os.type == "windows" then
+ pathsep = ";"
+end
+
+local original_wd = filesys.currentdir()
+if options.change_directory then
+ local TEXINPUTS = os.getenv("TEXINPUTS") or ""
+ filesys.chdir(options.output_directory)
+ options.output = pathutil.abspath(options.output, original_wd)
+ os.setenv("TEXINPUTS", original_wd .. pathsep .. TEXINPUTS)
+end
+if options.bibtex or options.biber then
+ local BIBINPUTS = os.getenv("BIBINPUTS") or ""
+ options.output = pathutil.abspath(options.output, original_wd)
+ os.setenv("BIBINPUTS", original_wd .. pathsep .. BIBINPUTS)
+end
+
+-- Set `max_print_line' environment variable if not already set.
+if os.getenv("max_print_line") == nil then
+ os.setenv("max_print_line", "65536")
+end
+-- TODO: error_line, half_error_line
+--[[
+ According to texmf.cnf:
+ 45 < error_line < 255,
+ 30 < half_error_line < error_line - 15,
+ 60 <= max_print_line.
+]]
+
+local function path_in_output_directory(ext)
+ return pathutil.join(options.output_directory, jobname .. "." .. ext)
+end
+
+local recorderfile = path_in_output_directory("fls")
+local recorderfile2 = path_in_output_directory("cluttex-fls")
+
+local tex_options = {
+ interaction = options.interaction,
+ file_line_error = options.file_line_error,
+ halt_on_error = options.halt_on_error,
+ synctex = options.synctex,
+ output_directory = options.output_directory,
+ shell_escape = options.shell_escape,
+ shell_restricted = options.shell_restricted,
+ jobname = options.jobname,
+ fmt = options.fmt,
+ extraoptions = options.tex_extraoptions,
+}
+if options.output_format ~= "pdf" and engine.supports_pdf_generation then
+ tex_options.output_format = options.output_format
+end
+
+-- Setup LuaTeX initialization script
+if engine.is_luatex then
+ local initscriptfile = path_in_output_directory("cluttexinit.lua")
+ luatexinit.create_initialization_script(initscriptfile, tex_options)
+ tex_options.lua_initialization_script = initscriptfile
+end
+
+-- Run TeX command (*tex, *latex)
+-- should_rerun, newauxstatus = single_run([auxstatus])
+-- This function should be run in a coroutine.
+local function single_run(auxstatus, iteration)
+ local minted = false
+ local bibtex_aux_hash = nil
+ local mainauxfile = path_in_output_directory("aux")
+ if fsutil.isfile(recorderfile) then
+ -- Recorder file already exists
+ local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options)
+ if engine.is_luatex and fsutil.isfile(recorderfile2) then
+ filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap)
+ end
+ auxstatus = reruncheck.collectfileinfo(filelist, auxstatus)
+ for _,fileinfo in ipairs(filelist) do
+ if string.match(fileinfo.path, "minted/minted%.sty$") then
+ minted = true
+ break
+ end
+ end
+ if options.bibtex then
+ local biblines = extract_bibtex_from_aux_file(mainauxfile, options.output_directory)
+ if #biblines > 0 then
+ bibtex_aux_hash = md5.sum(table.concat(biblines, "\n"))
+ end
+ end
+ else
+ -- This is the first execution
+ if auxstatus ~= nil then
+ message.error("Recorder file was not generated during the execution!")
+ os.exit(1)
+ end
+ auxstatus = {}
+ end
+ --local timestamp = os.time()
+
+ if options.includeonly then
+ tex_options.tex_injection = string.format("%s\\includeonly{%s}", tex_options.tex_injection or "", options.includeonly)
+ end
+
+ if minted and not (tex_options.tex_injection and string.find(tex_options.tex_injection,"minted") == nil) then
+ tex_options.tex_injection = string.format("%s\\PassOptionsToPackage{outputdir=%s}{minted}", tex_options.tex_injection or "", options.output_directory)
+ end
+
+ local current_tex_options, lightweight_mode = tex_options, false
+ if iteration == 1 and options.start_with_draft then
+ current_tex_options = {}
+ for k,v in pairs(tex_options) do
+ current_tex_options[k] = v
+ end
+ if engine.supports_draftmode then
+ current_tex_options.draftmode = true
+ options.start_with_draft = false
+ end
+ current_tex_options.interaction = "batchmode"
+ lightweight_mode = true
+ else
+ current_tex_options.draftmode = false
+ end
+
+ local command = engine:build_command(inputfile, current_tex_options)
+
+ local execlog -- the contents of .log file
+
+ local recovered = false
+ local function recover()
+ -- Check log file
+ if not execlog then
+ local logfile = assert(io.open(path_in_output_directory("log")))
+ execlog = logfile:read("*a")
+ logfile:close()
+ end
+ recovered = recoverylib.try_recovery{
+ execlog = execlog,
+ auxfile = path_in_output_directory("aux"),
+ options = options,
+ original_wd = original_wd,
+ }
+ return recovered
+ end
+ coroutine.yield(command, recover) -- Execute the command
+ if recovered then
+ return true, {}
+ end
+
+ local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options)
+ if engine.is_luatex and fsutil.isfile(recorderfile2) then
+ filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap)
+ end
+
+ if not execlog then
+ local logfile = assert(io.open(path_in_output_directory("log")))
+ execlog = logfile:read("*a")
+ logfile:close()
+ end
+
+ if options.makeindex then
+ -- Look for .idx files and run MakeIndex
+ for _,file in ipairs(filelist) do
+ if pathutil.ext(file.path) == "idx" then
+ -- Run makeindex if the .idx file is new or updated
+ local idxfileinfo = {path = file.path, abspath = file.abspath, kind = "auxiliary"}
+ local output_ind = pathutil.replaceext(file.abspath, "ind")
+ if reruncheck.comparefileinfo({idxfileinfo}, auxstatus) or reruncheck.comparefiletime(file.abspath, output_ind, auxstatus) then
+ local idx_dir = pathutil.dirname(file.abspath)
+ local makeindex_command = {
+ "cd", shellutil.escape(idx_dir), "&&",
+ options.makeindex, -- Do not escape options.makeindex to allow additional options
+ "-o", pathutil.basename(output_ind),
+ pathutil.basename(file.abspath)
+ }
+ coroutine.yield(table.concat(makeindex_command, " "))
+ table.insert(filelist, {path = output_ind, abspath = output_ind, kind = "auxiliary"})
+ else
+ local succ, err = filesys.touch(output_ind)
+ if not succ then
+ message.warn("Failed to touch " .. output_ind .. " (" .. err .. ")")
+ end
+ end
+ end
+ end
+ else
+ -- Check log file
+ if string.find(execlog, "No file [^\n]+%.ind%.") then
+ message.diag("You may want to use --makeindex option.")
+ end
+ end
+
+ if options.makeglossaries then
+ -- Look for .glo files and run makeglossaries
+ for _,file in ipairs(filelist) do
+ if pathutil.ext(file.path) == "glo" then
+ -- Run makeglossaries if the .glo file is new or updated
+ local glofileinfo = {path = file.path, abspath = file.abspath, kind = "auxiliary"}
+ local output_gls = pathutil.replaceext(file.abspath, "gls")
+ if reruncheck.comparefileinfo({glofileinfo}, auxstatus) or reruncheck.comparefiletime(file.abspath, output_gls, auxstatus) then
+ local makeglossaries_command = {
+ options.makeglossaries,
+ "-d", shellutil.escape(options.output_directory),
+ pathutil.trimext(pathutil.basename(file.path))
+ }
+ coroutine.yield(table.concat(makeglossaries_command, " "))
+ table.insert(filelist, {path = output_gls, abspath = output_gls, kind = "auxiliary"})
+ else
+ local succ, err = filesys.touch(output_gls)
+ if not succ then
+ message.warn("Failed to touch " .. output_ind .. " (" .. err .. ")")
+ end
+ end
+ end
+ end
+ else
+ -- Check log file
+ if string.find(execlog, "No file [^\n]+%.gls%.") then
+ message.diag("You may want to use --makeglossaries option.")
+ end
+ end
+
+ if options.bibtex then
+ local biblines2 = extract_bibtex_from_aux_file(mainauxfile, options.output_directory)
+ local bibtex_aux_hash2
+ if #biblines2 > 0 then
+ bibtex_aux_hash2 = md5.sum(table.concat(biblines2, "\n"))
+ end
+ local output_bbl = path_in_output_directory("bbl")
+ if bibtex_aux_hash ~= bibtex_aux_hash2 or reruncheck.comparefiletime(mainauxfile, output_bbl, auxstatus) then
+ -- The input for BibTeX command has changed...
+ local bibtex_command = {
+ "cd", shellutil.escape(options.output_directory), "&&",
+ options.bibtex,
+ pathutil.basename(mainauxfile)
+ }
+ coroutine.yield(table.concat(bibtex_command, " "))
+ else
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("No need to run BibTeX.")
+ end
+ local succ, err = filesys.touch(output_bbl)
+ if not succ then
+ message.warn("Failed to touch " .. output_bbl .. " (" .. err .. ")")
+ end
+ end
+ elseif options.biber then
+ for _,file in ipairs(filelist) do
+ if pathutil.ext(file.path) == "bcf" then
+ -- Run biber if the .bcf file is new or updated
+ local bcffileinfo = {path = file.path, abspath = file.abspath, kind = "auxiliary"}
+ local output_bbl = pathutil.replaceext(file.abspath, "bbl")
+ if reruncheck.comparefileinfo({bcffileinfo}, auxstatus) or reruncheck.comparefiletime(file.abspath, output_bbl, auxstatus) then
+ local bbl_dir = pathutil.dirname(file.abspath)
+ local biber_command = {
+ options.biber, -- Do not escape options.biber to allow additional options
+ "--output-directory", shellutil.escape(options.output_directory),
+ pathutil.basename(file.abspath)
+ }
+ coroutine.yield(table.concat(biber_command, " "))
+ table.insert(filelist, {path = output_bbl, abspath = output_bbl, kind = "auxiliary"})
+ else
+ local succ, err = filesys.touch(output_bbl)
+ if not succ then
+ message.warn("Failed to touch " .. output_bbl .. " (" .. err .. ")")
+ end
+ end
+ end
+ end
+ else
+ -- Check log file
+ if string.find(execlog, "No file [^\n]+%.bbl%.") then
+ message.diag("You may want to use --bibtex or --biber option.")
+ end
+ end
+
+ if string.find(execlog, "No pages of output.") then
+ return "No pages of output."
+ end
+
+ local should_rerun, auxstatus = reruncheck.comparefileinfo(filelist, auxstatus)
+ return should_rerun or lightweight_mode, auxstatus
+end
+
+-- Run (La)TeX (possibly multiple times) and produce a PDF file.
+-- This function should be run in a coroutine.
+local function do_typeset_c()
+ local iteration = 0
+ local should_rerun, auxstatus
+ repeat
+ iteration = iteration + 1
+ should_rerun, auxstatus = single_run(auxstatus, iteration)
+ if should_rerun == "No pages of output." then
+ message.warn("No pages of output.")
+ return
+ end
+ until not should_rerun or iteration >= options.max_iterations
+
+ if should_rerun then
+ message.warn("LaTeX should be run once more.")
+ end
+
+ -- Successful
+ if options.output_format == "dvi" or engine.supports_pdf_generation then
+ -- Output file (DVI/PDF) is generated in the output directory
+ local outfile = path_in_output_directory(output_extension)
+ local oncopyerror
+ if os.type == "windows" then
+ oncopyerror = function()
+ message.error("Failed to copy file. Some applications may be locking the ", string.upper(options.output_format), " file.")
+ return false
+ end
+ end
+ coroutine.yield(fsutil.copy_command(outfile, options.output), oncopyerror)
+ if #options.dvipdfmx_extraoptions > 0 then
+ message.warn("--dvipdfmx-option[s] are ignored.")
+ end
+
+ else
+ -- DVI file is generated, but PDF file is wanted
+ local dvifile = path_in_output_directory("dvi")
+ local dvipdfmx_command = {"dvipdfmx", "-o", shellutil.escape(options.output)}
+ for _,v in ipairs(options.dvipdfmx_extraoptions) do
+ table.insert(dvipdfmx_command, v)
+ end
+ table.insert(dvipdfmx_command, shellutil.escape(dvifile))
+ coroutine.yield(table.concat(dvipdfmx_command, " "))
+ end
+
+ -- Copy SyncTeX file if necessary
+ if options.output_format == "pdf" then
+ local synctex = tonumber(options.synctex or "0")
+ local synctex_ext = nil
+ if synctex > 0 then
+ -- Compressed SyncTeX file (.synctex.gz)
+ synctex_ext = "synctex.gz"
+ elseif synctex < 0 then
+ -- Uncompressed SyncTeX file (.synctex)
+ synctex_ext = "synctex"
+ end
+ if synctex_ext then
+ coroutine.yield(fsutil.copy_command(path_in_output_directory(synctex_ext), pathutil.replaceext(options.output, synctex_ext)))
+ end
+ end
+end
+
+local function do_typeset()
+ -- Execute the command string yielded by do_typeset_c
+ for command, recover in coroutine.wrap(do_typeset_c) do
+ message.exec(command)
+ local success, termination, status_or_signal = os.execute(command)
+ if type(success) == "number" then -- Lua 5.1 or LuaTeX
+ local code = success
+ success = code == 0
+ termination = nil
+ status_or_signal = code
+ end
+ if not success and not (recover and recover()) then
+ if termination == "exit" then
+ message.error("Command exited abnormally: exit status ", tostring(status_or_signal))
+ elseif termination == "signal" then
+ message.error("Command exited abnormally: signal ", tostring(status_or_signal))
+ else
+ message.error("Command exited abnormally: ", tostring(status_or_signal))
+ end
+ return false, termination, status_or_signal
+ end
+ end
+ -- Successful
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("Command exited successfully")
+ end
+ return true
+end
+
+if options.watch then
+ -- Watch mode
+ local success, status = do_typeset()
+ local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options)
+ if engine.is_luatex and fsutil.isfile(recorderfile2) then
+ filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap)
+ end
+ local input_files_to_watch = {}
+ for _,fileinfo in ipairs(filelist) do
+ if fileinfo.kind == "input" then
+ table.insert(input_files_to_watch, fileinfo.abspath)
+ end
+ end
+ local fswatch_command = {"fswatch", "--event=Updated", "--"}
+ for _,path in ipairs(input_files_to_watch) do
+ table.insert(fswatch_command, shellutil.escape(path))
+ end
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.exec(table.concat(fswatch_command, " "))
+ end
+ local fswatch = assert(io.popen(table.concat(fswatch_command, " "), "r"))
+ for l in fswatch:lines() do
+ local found = false
+ for _,path in ipairs(input_files_to_watch) do
+ if l == path then
+ found = true
+ break
+ end
+ end
+ if found then
+ local success, status = do_typeset()
+ if not success then
+ -- Not successful
+ end
+ end
+ end
+
+else
+ -- Not in watch mode
+ local success, status = do_typeset()
+ if not success then
+ os.exit(1)
+ end
+end
diff --git a/Master/bin/win32/clxelatex.bat b/Master/bin/win32/clxelatex.bat
new file mode 100755
index 00000000000..6b938411f9d
--- /dev/null
+++ b/Master/bin/win32/clxelatex.bat
@@ -0,0 +1,2598 @@
+::dummy:: --[[
+@texlua "%~f0" %*
+@goto :eof
+]]
+local io, os, string, table, package, require, assert, error, ipairs, type, select, arg = io, os, string, table, package, require, assert, error, ipairs, type, select, arg
+local CLUTTEX_VERBOSITY, CLUTTEX_VERSION
+os.type = os.type or "windows"
+if lfs and not package.loaded['lfs'] then package.loaded['lfs'] = lfs end
+if os.type == "windows" then
+package.preload["texrunner.pathutil"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+-- pathutil module
+
+local assert = assert
+local select = select
+local string = string
+local string_find = string.find
+local string_sub = string.sub
+local string_match = string.match
+local string_gsub = string.gsub
+local filesys = require "lfs"
+
+local function basename(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "[\\/]", i + 1)
+ if j == nil then
+ return string_sub(path, i + 1)
+ elseif j == #path then
+ return string_sub(path, i + 1, -2)
+ end
+ i = j
+ end
+end
+
+
+local function dirname(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "[\\/]", i + 1)
+ if j == nil then
+ if i == 0 then
+ -- No directory portion
+ return "."
+ elseif i == 1 then
+ -- Root
+ return string_sub(path, 1, 1)
+ else
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ end
+ i = j
+ end
+end
+
+
+local function parentdir(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "[\\/]", i + 1)
+ if j == nil then
+ if i == 0 then
+ -- No directory portion
+ return "."
+ elseif i == 1 then
+ -- Root
+ return string_sub(path, 1, 1)
+ else
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ elseif j == #path then
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ i = j
+ end
+end
+
+
+local function trimext(path)
+ return (string_gsub(path, "%.[^\\/%.]*$", ""))
+end
+
+
+local function ext(path)
+ return string_match(path, "%.([^\\/%.]*)$") or ""
+end
+
+
+local function replaceext(path, newext)
+ local newpath, n = string_gsub(path, "%.([^\\/%.]*)$", function() return "." .. newext end)
+ if n == 0 then
+ return newpath .. "." .. newext
+ else
+ return newpath
+ end
+end
+
+
+local function joinpath2(x, y)
+ local xd = x
+ local last = string_sub(x, -1)
+ if last ~= "/" and last ~= "\\" then
+ xd = x .. "\\"
+ end
+ if y == "." then
+ return xd
+ elseif y == ".." then
+ return dirname(x)
+ else
+ if string_match(y, "^%.[\\/]") then
+ return xd .. string_sub(y, 3)
+ else
+ return xd .. y
+ end
+ end
+end
+
+local function joinpath(...)
+ local n = select("#", ...)
+ if n == 2 then
+ return joinpath2(...)
+ elseif n == 0 then
+ return "."
+ elseif n == 1 then
+ return ...
+ else
+ return joinpath(joinpath2(...), select(3, ...))
+ end
+end
+
+
+-- https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
+local function isabspath(path)
+ local init = string_sub(path, 1, 1)
+ return init == "\\" or init == "/" or string_match(path, "^%a:[/\\]")
+end
+
+local function abspath(path, cwd)
+ if isabspath(path) then
+ -- absolute path
+ return path
+ else
+ -- TODO: relative path with a drive letter is not supported
+ cwd = cwd or filesys.currentdir()
+ return joinpath2(cwd, path)
+ end
+end
+
+return {
+ basename = basename,
+ dirname = dirname,
+ parentdir = parentdir,
+ trimext = trimext,
+ ext = ext,
+ replaceext = replaceext,
+ join = joinpath,
+ abspath = abspath,
+}
+end
+else
+package.preload["texrunner.pathutil"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+-- pathutil module for *nix
+
+local assert = assert
+local select = select
+local string = string
+local string_find = string.find
+local string_sub = string.sub
+local string_match = string.match
+local string_gsub = string.gsub
+local filesys = require "lfs"
+
+local function basename(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "/", i + 1, true)
+ if j == nil then
+ return string_sub(path, i + 1)
+ elseif j == #path then
+ return string_sub(path, i + 1, -2)
+ end
+ i = j
+ end
+end
+
+
+local function dirname(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "/", i + 1, true)
+ if j == nil then
+ if i == 0 then
+ -- No directory portion
+ return "."
+ elseif i == 1 then
+ -- Root
+ return "/"
+ else
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ end
+ i = j
+ end
+end
+
+
+local function parentdir(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "/", i + 1, true)
+ if j == nil then
+ if i == 0 then
+ -- No directory portion
+ return "."
+ elseif i == 1 then
+ -- Root
+ return "/"
+ else
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ elseif j == #path then
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ i = j
+ end
+end
+
+
+local function trimext(path)
+ return (string_gsub(path, "%.[^/%.]*$", ""))
+end
+
+
+local function ext(path)
+ return string_match(path, "%.([^/%.]*)$") or ""
+end
+
+
+local function replaceext(path, newext)
+ local newpath, n = string_gsub(path, "%.([^/%.]*)$", function() return "." .. newext end)
+ if n == 0 then
+ return newpath .. "." .. newext
+ else
+ return newpath
+ end
+end
+
+
+local function joinpath2(x, y)
+ local xd = x
+ if string_sub(x, -1) ~= "/" then
+ xd = x .. "/"
+ end
+ if y == "." then
+ return xd
+ elseif y == ".." then
+ return dirname(x)
+ else
+ if string_sub(y, 1, 2) == "./" then
+ return xd .. string_sub(y, 3)
+ else
+ return xd .. y
+ end
+ end
+end
+
+local function joinpath(...)
+ local n = select("#", ...)
+ if n == 2 then
+ return joinpath2(...)
+ elseif n == 0 then
+ return "."
+ elseif n == 1 then
+ return ...
+ else
+ return joinpath(joinpath2(...), select(3, ...))
+ end
+end
+
+
+local function abspath(path, cwd)
+ if string_sub(path, 1, 1) == "/" then
+ -- absolute path
+ return path
+ else
+ cwd = cwd or filesys.currentdir()
+ return joinpath2(cwd, path)
+ end
+end
+
+
+return {
+ basename = basename,
+ dirname = dirname,
+ parentdir = parentdir,
+ trimext = trimext,
+ ext = ext,
+ replaceext = replaceext,
+ join = joinpath,
+ abspath = abspath,
+}
+end
+end
+if os.type == "windows" then
+package.preload["texrunner.shellutil"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local string_gsub = string.gsub
+
+-- s: string
+local function escape(s)
+ return '"' .. string_gsub(string_gsub(s, '(\\*)"', '%1%1\\"'), '(\\+)$', '%1%1') .. '"'
+end
+
+
+return {
+ escape = escape,
+}
+end
+else
+package.preload["texrunner.shellutil"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local assert = assert
+local string_match = string.match
+local table = table
+local table_insert = table.insert
+local table_concat = table.concat
+
+-- s: string
+local function escape(s)
+ local len = #s
+ local result = {}
+ local t,i = string_match(s, "^([^']*)()")
+ assert(t)
+ if t ~= "" then
+ table_insert(result, "'")
+ table_insert(result, t)
+ table_insert(result, "'")
+ end
+ while i < len do
+ t,i = string_match(s, "^('+)()", i)
+ assert(t)
+ table_insert(result, '"')
+ table_insert(result, t)
+ table_insert(result, '"')
+ t,i = string_match(s, "^([^']*)()", i)
+ assert(t)
+ if t ~= "" then
+ table_insert(result, "'")
+ table_insert(result, t)
+ table_insert(result, "'")
+ end
+ end
+ return table_concat(result, "")
+end
+
+
+return {
+ escape = escape,
+}
+end
+end
+package.preload["texrunner.fsutil"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local assert = assert
+local os = os
+local os_execute = os.execute
+local os_remove = os.remove
+local filesys = require "lfs"
+local pathutil = require "texrunner.pathutil"
+local shellutil = require "texrunner.shellutil"
+local escape = shellutil.escape
+
+local copy_command
+if os.type == "windows" then
+ function copy_command(from, to)
+ -- TODO: What if `from` begins with a slash?
+ return "copy " .. escape(from) .. " " .. escape(to) .. " > NUL"
+ end
+else
+ function copy_command(from, to)
+ -- TODO: What if `from` begins with a hypen?
+ return "cp " .. escape(from) .. " " .. escape(to)
+ end
+end
+
+local isfile = filesys.isfile or function(path)
+ return filesys.attributes(path, "mode") == "file"
+end
+
+local isdir = filesys.isdir or function(path)
+ return filesys.attributes(path, "mode") == "directory"
+end
+
+local function mkdir_rec(path)
+ local succ, err = filesys.mkdir(path)
+ if not succ then
+ succ, err = mkdir_rec(pathutil.parentdir(path))
+ if succ then
+ return filesys.mkdir(path)
+ end
+ end
+ return succ, err
+end
+
+local function remove_rec(path)
+ if isdir(path) then
+ for file in filesys.dir(path) do
+ if file ~= "." and file ~= ".." then
+ local succ, err = remove_rec(pathutil.join(path, file))
+ if not succ then
+ return succ, err
+ end
+ end
+ end
+ return filesys.rmdir(path)
+ else
+ return os_remove(path)
+ end
+end
+
+return {
+ copy_command = copy_command,
+ isfile = isfile,
+ isdir = isdir,
+ mkdir_rec = mkdir_rec,
+ remove_rec = remove_rec,
+}
+end
+package.preload["texrunner.option"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+-- options_and_params, i = parseoption(arg, options)
+-- options[i] = {short = "o", long = "option" [, param = true] [, boolean = true] [, allow_single_hyphen = false]}
+-- arg[i], arg[i + 1], ..., arg[#arg] are non-options
+local function parseoption(arg, options)
+ local i = 1
+ local option_and_params = {}
+ while i <= #arg do
+ if arg[i] == "--" then
+ -- Stop handling options
+ i = i + 1
+ break
+ elseif arg[i]:sub(1,2) == "--" then
+ -- Long option
+ local name,param = arg[i]:match("^([^=]+)=(.*)$", 3)
+ name = name or arg[i]:sub(3)
+ local opt = nil
+ for _,o in ipairs(options) do
+ if o.long then
+ if o.long == name then
+ if o.param then
+ if param then
+ -- --option=param
+ else
+ if o.default ~= nil then
+ param = o.default
+ else
+ -- --option param
+ assert(i + 1 <= #arg, "argument missing after " .. arg[i] .. " option")
+ param = arg[i + 1]
+ i = i + 1
+ end
+ end
+ else
+ -- --option
+ param = true
+ end
+ opt = o
+ break
+ elseif o.boolean and name == "no-" .. o.long then
+ -- --no-option
+ opt = o
+ break
+ end
+ end
+ end
+ if opt then
+ table.insert(option_and_params, {opt.long, param})
+ else
+ -- Unknown long option
+ error("unknown long option: " .. arg[i])
+ end
+ elseif arg[i]:sub(1,1) == "-" then
+ local name,param = arg[i]:match("^([^=]+)=(.*)$", 2)
+ name = name or arg[i]:sub(2)
+ local opt = nil
+ for _,o in ipairs(options) do
+ if o.long and o.allow_single_hyphen then
+ if o.long == name then
+ if o.param then
+ if param then
+ -- -option=param
+ else
+ if o.default ~= nil then
+ param = o.default
+ else
+ -- -option param
+ assert(i + 1 <= #arg, "argument missing after " .. arg[i] .. " option")
+ param = arg[i + 1]
+ i = i + 1
+ end
+ end
+ else
+ -- -option
+ param = true
+ end
+ opt = o
+ break
+ elseif o.boolean and name == "no-" .. o.long then
+ -- -no-option
+ opt = o
+ break
+ end
+ elseif o.long and #name >= 2 and (o.long == name or (o.boolean and name == "no-" .. o.long)) then
+ error("You must supply two hyphens (i.e. --" .. name .. ") for long option")
+ end
+ end
+ if opt == nil then
+ -- Short option
+ name = arg[i]:sub(2,2)
+ for _,o in ipairs(options) do
+ if o.short then
+ if o.short == name then
+ if o.param then
+ if #arg[i] > 2 then
+ -- -oparam
+ param = arg[i]:sub(3)
+ else
+ -- -o param
+ assert(i + 1 <= #arg, "argument missing after " .. arg[i] .. " option")
+ param = arg[i + 1]
+ i = i + 1
+ end
+ else
+ -- -o
+ assert(#arg[i] == 2, "combining multiple short options like -abc is not supported")
+ param = true
+ end
+ opt = o
+ break
+ end
+ end
+ end
+ end
+ if opt then
+ table.insert(option_and_params, {opt.long or opt.short, param})
+ else
+ error("unknown short option: " .. arg[i])
+ end
+ else
+ -- arg[i] is not an option
+ break
+ end
+ i = i + 1
+ end
+ return option_and_params, i
+end
+
+return {
+ parseoption = parseoption;
+}
+end
+package.preload["texrunner.tex_engine"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local table = table
+local setmetatable = setmetatable
+local ipairs = ipairs
+
+local shellutil = require "texrunner.shellutil"
+
+--[[
+engine.name: string
+engine.type = "onePass" or "twoPass"
+engine:build_command(inputfile, options)
+ options:
+ halt_on_error: boolean
+ interaction: string
+ file_line_error: boolean
+ synctex: string
+ shell_escape: boolean
+ shell_restricted: boolean
+ jobname: string
+ output_directory: string
+ extraoptions: a list of strings
+ output_format: "pdf" or "dvi"
+ draftmode: boolean (pdfTeX / XeTeX / LuaTeX)
+ fmt: string
+ tex_injection: string
+ lua_initialization_script: string (LuaTeX only)
+engine.executable: string
+engine.supports_pdf_generation: boolean
+engine.dvi_extension: string
+engine.supports_draftmode: boolean
+engine.is_luatex: true or nil
+]]
+
+local engine_meta = {}
+engine_meta.__index = engine_meta
+engine_meta.dvi_extension = "dvi"
+function engine_meta:build_command(inputfile, options)
+ local command = {self.executable, "-recorder"}
+ if options.fmt then
+ table.insert(command, "-fmt=" .. options.fmt)
+ end
+ if options.halt_on_error then
+ table.insert(command, "-halt-on-error")
+ end
+ if options.interaction then
+ table.insert(command, "-interaction=" .. options.interaction)
+ end
+ if options.file_line_error then
+ table.insert(command, "-file-line-error")
+ end
+ if options.synctex then
+ table.insert(command, "-synctex=" .. shellutil.escape(options.synctex))
+ end
+ if options.shell_escape == false then
+ table.insert(command, "-no-shell-escape")
+ elseif options.shell_restricted == true then
+ table.insert(command, "-shell-restricted")
+ elseif options.shell_escape == true then
+ table.insert(command, "-shell-escape")
+ end
+ if options.jobname then
+ table.insert(command, "-jobname=" .. shellutil.escape(options.jobname))
+ end
+ if options.output_directory then
+ table.insert(command, "-output-directory=" .. shellutil.escape(options.output_directory))
+ end
+ if self.handle_additional_options then
+ self:handle_additional_options(command, options)
+ end
+ if options.extraoptions then
+ for _,v in ipairs(options.extraoptions) do
+ table.insert(command, v)
+ end
+ end
+ if type(options.tex_injection) == "string" then
+ table.insert(command, shellutil.escape(options.tex_injection .. "\\input " .. inputfile)) -- TODO: what if filename contains spaces?
+ else
+ table.insert(command, shellutil.escape(inputfile))
+ end
+ return table.concat(command, " ")
+end
+
+local function engine(name, supports_pdf_generation, handle_additional_options)
+ return setmetatable({
+ name = name,
+ executable = name,
+ supports_pdf_generation = supports_pdf_generation,
+ handle_additional_options = handle_additional_options,
+ supports_draftmode = supports_pdf_generation,
+ }, engine_meta)
+end
+
+local function handle_pdftex_options(self, args, options)
+ if options.draftmode then
+ table.insert(args, "-draftmode")
+ elseif options.output_format == "dvi" then
+ table.insert(args, "-output-format=dvi")
+ end
+end
+
+local function handle_xetex_options(self, args, options)
+ if options.output_format == "dvi" or options.draftmode then
+ table.insert(args, "-no-pdf")
+ end
+end
+
+local function handle_luatex_options(self, args, options)
+ if options.lua_initialization_script then
+ table.insert(args, "--lua="..shellutil.escape(options.lua_initialization_script))
+ end
+ handle_pdftex_options(self, args, options)
+end
+
+local function is_luatex(e)
+ e.is_luatex = true
+ return e
+end
+
+local KnownEngines = {
+ ["pdftex"] = engine("pdftex", true, handle_pdftex_options),
+ ["pdflatex"] = engine("pdflatex", true, handle_pdftex_options),
+ ["luatex"] = is_luatex(engine("luatex", true, handle_luatex_options)),
+ ["lualatex"] = is_luatex(engine("lualatex", true, handle_luatex_options)),
+ ["luajittex"] = is_luatex(engine("luajittex", true, handle_luatex_options)),
+ ["xetex"] = engine("xetex", true, handle_xetex_options),
+ ["xelatex"] = engine("xelatex", true, handle_xetex_options),
+ ["tex"] = engine("tex", false),
+ ["etex"] = engine("etex", false),
+ ["latex"] = engine("latex", false),
+ ["ptex"] = engine("ptex", false),
+ ["eptex"] = engine("eptex", false),
+ ["platex"] = engine("platex", false),
+ ["uptex"] = engine("uptex", false),
+ ["euptex"] = engine("euptex", false),
+ ["uplatex"] = engine("uplatex", false),
+}
+
+KnownEngines["xetex"].dvi_extension = "xdv"
+KnownEngines["xelatex"].dvi_extension = "xdv"
+
+return KnownEngines
+end
+package.preload["texrunner.reruncheck"] = function(...)
+--[[
+ Copyright 2016,2018 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local io = io
+local assert = assert
+local filesys = require "lfs"
+local md5 = require "md5"
+local fsutil = require "texrunner.fsutil"
+local pathutil = require "texrunner.pathutil"
+local message = require "texrunner.message"
+
+local function md5sum_file(path)
+ local f = assert(io.open(path, "rb"))
+ local contents = f:read("*a")
+ f:close()
+ return md5.sum(contents)
+end
+
+-- filelist, filemap = parse_recorder_file("jobname.fls", options [, filelist, filemap])
+-- filelist[i] = {path = "...", abspath = "...", kind = "input" or "output" or "auxiliary"}
+local function parse_recorder_file(file, options, filelist, filemap)
+ filelist = filelist or {}
+ filemap = filemap or {}
+ for l in io.lines(file) do
+ local t,path = l:match("^(%w+) (.*)$")
+ if t == "PWD" then
+ -- Ignore
+
+ elseif t == "INPUT" then
+ local abspath = pathutil.abspath(path)
+ local fileinfo = filemap[abspath]
+ if not fileinfo then
+ if fsutil.isfile(path) then
+ local kind = "input"
+ local ext = pathutil.ext(path)
+ if ext == "bbl" then
+ kind = "auxiliary"
+ end
+ fileinfo = {path = path, abspath = abspath, kind = kind}
+ table.insert(filelist, fileinfo)
+ filemap[abspath] = fileinfo
+ else
+ -- Maybe a command execution
+ end
+ else
+ if #path < #fileinfo.path then
+ fileinfo.path = path
+ end
+ if fileinfo.kind == "output" then
+ -- The files listed in both INPUT and OUTPUT are considered to be auxiliary files.
+ fileinfo.kind = "auxiliary"
+ end
+ end
+
+ elseif t == "OUTPUT" then
+ local abspath = pathutil.abspath(path)
+ local fileinfo = filemap[abspath]
+ if not fileinfo then
+ local kind = "output"
+ local ext = pathutil.ext(path)
+ if ext == "out" then
+ -- hyperref bookmarks file
+ kind = "auxiliary"
+ elseif options.makeindex and ext == "idx" then
+ -- Treat .idx files (to be processed by MakeIndex) as auxiliary
+ kind = "auxiliary"
+ -- ...and .ind files
+ elseif ext == "bcf" then -- biber
+ kind = "auxiliary"
+ elseif ext == "glo" then -- makeglossaries
+ kind = "auxiliary"
+ end
+ fileinfo = {path = path, abspath = abspath, kind = kind}
+ table.insert(filelist, fileinfo)
+ filemap[abspath] = fileinfo
+ else
+ if #path < #fileinfo.path then
+ fileinfo.path = path
+ end
+ if fileinfo.kind == "input" then
+ -- The files listed in both INPUT and OUTPUT are considered to be auxiliary files.
+ fileinfo.kind = "auxiliary"
+ end
+ end
+
+ else
+ message.warning("Unrecognized line in recorder file '", file, "': ", l)
+ end
+ end
+ return filelist, filemap
+end
+
+-- auxstatus = collectfileinfo(filelist [, auxstatus])
+local function collectfileinfo(filelist, auxstatus)
+ auxstatus = auxstatus or {}
+ for i,fileinfo in ipairs(filelist) do
+ local path = fileinfo.abspath
+ if fsutil.isfile(path) then
+ local status = auxstatus[path] or {}
+ auxstatus[path] = status
+ if fileinfo.kind == "input" then
+ status.mtime = status.mtime or filesys.attributes(path, "modification")
+ elseif fileinfo.kind == "auxiliary" then
+ status.mtime = status.mtime or filesys.attributes(path, "modification")
+ status.size = status.size or filesys.attributes(path, "size")
+ status.md5sum = status.md5sum or md5sum_file(path)
+ end
+ end
+ end
+ return auxstatus
+end
+
+local function binarytohex(s)
+ return (s:gsub(".", function(c) return string.format("%02x", string.byte(c)) end))
+end
+
+-- should_rerun, newauxstatus = comparefileinfo(auxfiles, auxstatus)
+local function comparefileinfo(filelist, auxstatus)
+ local should_rerun = false
+ local newauxstatus = {}
+ for i,fileinfo in ipairs(filelist) do
+ local path = fileinfo.abspath
+ if fsutil.isfile(path) then
+ if fileinfo.kind == "input" then
+ -- Input file: User might have modified while running TeX.
+ local mtime = filesys.attributes(path, "modification")
+ if auxstatus[path] and auxstatus[path].mtime then
+ if auxstatus[path].mtime < mtime then
+ -- Input file was updated during execution
+ message.info("Input file '", fileinfo.path, "' was modified (by user, or some external commands).")
+ newauxstatus[path] = {mtime = mtime}
+ return true, newauxstatus
+ end
+ else
+ -- New input file
+ end
+
+ elseif fileinfo.kind == "auxiliary" then
+ -- Auxiliary file: Compare file contents.
+ if auxstatus[path] then
+ -- File was touched during execution
+ local really_modified = false
+ local modified_because = nil
+ local size = filesys.attributes(path, "size")
+ if auxstatus[path].size ~= size then
+ really_modified = true
+ if auxstatus[path].size then
+ modified_because = string.format("size: %d -> %d", auxstatus[path].size, size)
+ else
+ modified_because = string.format("size: (N/A) -> %d", size)
+ end
+ newauxstatus[path] = {size = size}
+ else
+ local md5sum = md5sum_file(path)
+ if auxstatus[path].md5sum ~= md5sum then
+ really_modified = true
+ if auxstatus[path].md5sum then
+ modified_because = string.format("md5: %s -> %s", binarytohex(auxstatus[path].md5sum), binarytohex(md5sum))
+ else
+ modified_because = string.format("md5: (N/A) -> %s", binarytohex(md5sum))
+ end
+ end
+ newauxstatus[path] = {size = size, md5sum = md5sum}
+ end
+ if really_modified then
+ message.info("File '", fileinfo.path, "' was modified (", modified_because, ").")
+ should_rerun = true
+ else
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("File '", fileinfo.path, "' unmodified (size and md5sum).")
+ end
+ end
+ else
+ -- New file
+ if path:sub(-4) == ".aux" then
+ local size = filesys.attributes(path, "size")
+ if size == 8 then
+ local auxfile = io.open(path, "rb")
+ local contents = auxfile:read("*a")
+ auxfile:close()
+ if contents == "\\relax \n" then
+ -- The .aux file is new, but it is almost empty
+ else
+ should_rerun = true
+ end
+ newauxstatus[path] = {size = size, md5sum = md5.sum(contents)}
+ else
+ should_rerun = true
+ newauxstatus[path] = {size = size}
+ end
+ else
+ should_rerun = true
+ end
+ if should_rerun then
+ message.info("New auxiliary file '", fileinfo.path, "'.")
+ else
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("Ignoring almost-empty auxiliary file '", fileinfo.path, "'.")
+ end
+ end
+ end
+ if should_rerun then
+ break
+ end
+ end
+ else
+ -- Auxiliary file is not really a file???
+ end
+ end
+ return should_rerun, newauxstatus
+end
+
+-- true if src is newer than dst
+local function comparefiletime(srcpath, dstpath, auxstatus)
+ if not filesys.isfile(dstpath) then
+ return true
+ end
+ local src_info = auxstatus[srcpath]
+ if src_info then
+ local src_mtime = src_info.mtime
+ if src_mtime then
+ local dst_mtime = filesys.attributes(dstpath, "modification")
+ return src_mtime > dst_mtime
+ end
+ end
+ return false
+end
+
+return {
+ parse_recorder_file = parse_recorder_file;
+ collectfileinfo = collectfileinfo;
+ comparefileinfo = comparefileinfo;
+ comparefiletime = comparefiletime;
+}
+end
+package.preload["texrunner.auxfile"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local string_match = string.match
+local pathutil = require "texrunner.pathutil"
+local filesys = require "lfs"
+local fsutil = require "texrunner.fsutil"
+local message = require "texrunner.message"
+
+-- for LaTeX
+local function parse_aux_file(auxfile, outdir, report, seen)
+ report = report or {}
+ seen = seen or {}
+ seen[auxfile] = true
+ for l in io.lines(auxfile) do
+ local subauxfile = string_match(l, "\\@input{(.+)}")
+ if subauxfile then
+ if fsutil.isfile(subauxfile) then
+ parse_aux_file(pathutil.join(outdir, subauxfile), outdir, report, seen)
+ else
+ local dir = pathutil.join(outdir, pathutil.dirname(subauxfile))
+ if not fsutil.isdir(dir) then
+ assert(fsutil.mkdir_rec(dir))
+ report.made_new_directory = true
+ end
+ end
+ end
+ end
+ return report
+end
+
+-- \citation, \bibdata, \bibstyle and \@input
+local function extract_bibtex_from_aux_file(auxfile, outdir, biblines)
+ biblines = biblines or {}
+ for l in io.lines(auxfile) do
+ local name = string_match(l, "\\([%a@]+)")
+ if name == "citation" or name == "bibdata" or name == "bibstyle" then
+ table.insert(biblines, l)
+ if CLUTTEX_VERBOSITY >= 2 then
+ message.info("BibTeX line: ", l)
+ end
+ elseif name == "@input" then
+ local subauxfile = string_match(l, "\\@input{(.+)}")
+ if subauxfile and fsutil.isfile(subauxfile) then
+ extract_bibtex_from_aux_file(pathutil.join(outdir, subauxfile), outdir, biblines)
+ end
+ end
+ end
+ return biblines
+end
+
+return {
+ parse_aux_file = parse_aux_file,
+ extract_bibtex_from_aux_file = extract_bibtex_from_aux_file,
+}
+end
+package.preload["texrunner.luatexinit"] = function(...)
+local function create_initialization_script(filename, options)
+ local initscript = assert(io.open(filename,"w"))
+ if type(options.file_line_error) == "boolean" then
+ initscript:write(string.format("texconfig.file_line_error = %s\n", options.file_line_error))
+ end
+ if type(options.halt_on_error) == "boolean" then
+ initscript:write(string.format("texconfig.halt_on_error = %s\n", options.halt_on_error))
+ end
+ initscript:write([==[
+local print = print
+local io_open = io.open
+local io_write = io.write
+local os_execute = os.execute
+local texio_write = texio.write
+local texio_write_nl = texio.write_nl
+]==])
+
+ -- Packages coded in Lua doesn't follow -output-directory option and doesn't write command to the log file
+ initscript:write(string.format("local output_directory = %q\n", options.output_directory))
+ initscript:write([==[
+local luawritelog
+local function openluawritelog()
+ if not luawritelog then
+ luawritelog = assert(io_open(output_directory .. "/" .. tex.jobname .. ".cluttex-fls", "w"))
+ end
+ return luawritelog
+end
+io.open = function(fname, mode)
+ -- luatexja-ruby
+ if mode == "w" and fname == tex.jobname .. ".ltjruby" then
+ fname = output_directory .. "/" .. fname
+ end
+ if type(mode) == "string" and string.find(mode, "w") ~= nil then
+ -- write mode
+ openluawritelog():write("OUTPUT " .. fname .. "\n")
+ end
+ return io_open(fname, mode)
+end
+os.execute = function(...)
+ texio_write_nl("log", string.format("CLUTTEX_EXEC %s", ...), "")
+ return os_execute(...)
+end
+]==])
+
+ -- Silence some of the TeX output to the terminal.
+ initscript:write([==[
+local function start_file_cb(category, filename)
+ if category == 1 then -- a normal data file, like a TeX source
+ texio_write_nl("log", "("..filename)
+ elseif category == 2 then -- a font map coupling font names to resources
+ texio_write("log", "{"..filename)
+ elseif category == 3 then -- an image file (png, pdf, etc)
+ texio_write("<"..filename)
+ elseif category == 4 then -- an embedded font subset
+ texio_write("<"..filename)
+ elseif category == 5 then -- a fully embedded font
+ texio_write("<<"..filename)
+ else
+ print("start_file: unknown category", category, filename)
+ end
+end
+callback.register("start_file", start_file_cb)
+local function stop_file_cb(category)
+ if category == 1 then
+ texio_write("log", ")")
+ elseif category == 2 then
+ texio_write("log", "}")
+ elseif category == 3 then
+ texio_write(">")
+ elseif category == 4 then
+ texio_write(">")
+ elseif category == 5 then
+ texio_write(">>")
+ else
+ print("stop_file: unknown category", category)
+ end
+end
+callback.register("stop_file", stop_file_cb)
+texio.write = function(...)
+ if select("#",...) == 1 then
+ -- Suppress luaotfload's message (See src/fontloader/runtime/fontload-reference.lua)
+ local s = ...
+ if string.match(s, "^%(using cache: ")
+ or string.match(s, "^%(using write cache: ")
+ or string.match(s, "^%(using read cache: ")
+ or string.match(s, "^%(load luc: ")
+ or string.match(s, "^%(load cache: ") then
+ return texio_write("log", ...)
+ end
+ end
+ return texio_write(...)
+end
+]==])
+ initscript:close()
+end
+
+return {
+ create_initialization_script = create_initialization_script
+}
+end
+package.preload["texrunner.recovery"] = function(...)
+--[[
+ Copyright 2018 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local io = io
+local string = string
+local parse_aux_file = require "texrunner.auxfile".parse_aux_file
+local pathutil = require "texrunner.pathutil"
+local fsutil = require "texrunner.fsutil"
+local shellutil = require "texrunner.shellutil"
+local message = require "texrunner.message"
+
+local function create_missing_directories(args)
+ if string.find(args.execlog, "I can't write on file", 1, true) then
+ -- There is a possibility that there are some subfiles under subdirectories.
+ -- Directories for sub-auxfiles are not created automatically, so we need to provide them.
+ local report = parse_aux_file(args.auxfile, args.options.output_directory)
+ if report.made_new_directory then
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("Created missing directories.")
+ end
+ return true
+ end
+ end
+ return false
+end
+
+local function run_epstopdf(args)
+ local run = false
+ if args.options.shell_escape ~= false then -- (possibly restricted) \write18 enabled
+ for outfile, infile in string.gmatch(args.execlog, "%(epstopdf%)%s*Command: <r?epstopdf %-%-outfile=([%w%-/]+%.pdf) ([%w%-/]+%.eps)>") do
+ local infile_abs = pathutil.abspath(infile, args.original_wd)
+ if fsutil.isfile(infile_abs) then -- input file exists
+ local outfile_abs = pathutil.abspath(outfile, args.options.output_directory)
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("Running epstopdf on ", infile, ".")
+ end
+ local outdir = pathutil.dirname(outfile_abs)
+ if not fsutil.isdir(outdir) then
+ assert(fsutil.mkdir_rec(outdir))
+ end
+ local command = string.format("epstopdf --outfile=%s %s", shellutil.escape(outfile_abs), shellutil.escape(infile_abs))
+ message.exec(command)
+ local success = os.execute(command)
+ if type(success) == "number" then -- Lua 5.1 or LuaTeX
+ success = success == 0
+ end
+ run = run or success
+ end
+ end
+ end
+ return run
+end
+
+local function check_minted(args)
+ return string.find(args.execlog, "Package minted Error: Missing Pygments output; \\inputminted was") ~= nil
+end
+
+local function try_recovery(args)
+ local recovered = false
+ recovered = create_missing_directories(args)
+ recovered = run_epstopdf(args) or recovered
+ recovered = check_minted(args) or recovered
+ return recovered
+end
+
+return {
+ create_missing_directories = create_missing_directories,
+ run_epstopdf = run_epstopdf,
+ try_recovery = try_recovery,
+}
+end
+package.preload["texrunner.handleoption"] = function(...)
+local COPYRIGHT_NOTICE = [[
+Copyright (C) 2016,2018 ARATA Mizuki
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local pathutil = require "texrunner.pathutil"
+local shellutil = require "texrunner.shellutil"
+local parseoption = require "texrunner.option".parseoption
+local KnownEngines = require "texrunner.tex_engine"
+local message = require "texrunner.message"
+
+local function usage(arg)
+ io.write(string.format([[
+ClutTeX: Process TeX files without cluttering your working directory
+
+Usage:
+ %s [options] [--] FILE.tex
+
+Options:
+ -e, --engine=ENGINE Specify which TeX engine to use.
+ ENGINE is one of the following:
+ pdflatex, pdftex,
+ lualatex, luatex, luajittex,
+ xelatex, xetex, latex, etex, tex,
+ platex, eptex, ptex,
+ uplatex, euptex, uptex,
+ -o, --output=FILE The name of output file.
+ [default: JOBNAME.pdf or JOBNAME.dvi]
+ --fresh Clean intermediate files before running TeX.
+ Cannot be used with --output-directory.
+ --max-iterations=N Maximum number of running TeX to resolve
+ cross-references. [default: 3]
+ --start-with-draft Start with draft mode.
+ --[no-]change-directory Change directory before running TeX.
+ --watch Watch input files for change. Requires fswatch
+ program to be installed.
+ --tex-option=OPTION Pass OPTION to TeX as a single option.
+ --tex-options=OPTIONs Pass OPTIONs to TeX as multiple options.
+ --dvipdfmx-option[s]=OPTION[s] Same for dvipdfmx.
+ --makeindex=COMMAND+OPTIONs Command to generate index, such as
+ `makeindex' or `mendex'.
+ --bibtex=COMMAND+OPTIONs Command for BibTeX, such as
+ `bibtex' or `pbibtex'.
+ --biber[=COMMAND+OPTIONs] Command for Biber.
+ --makeglossaries[=COMMAND+OPTIONs] Command for makeglossaries.
+ -h, --help Print this message and exit.
+ -v, --version Print version information and exit.
+ -V, --verbose Be more verbose.
+ --color=WHEN Make ClutTeX's message colorful. WHEN is one of
+ `always', `auto', or `never'. [default: auto]
+ --includeonly=NAMEs Insert '\includeonly{NAMEs}'.
+
+ --[no-]shell-escape
+ --shell-restricted
+ --synctex=NUMBER
+ --fmt=FMTNAME
+ --[no-]file-line-error [default: yes]
+ --[no-]halt-on-error [default: yes]
+ --interaction=STRING [default: nonstopmode]
+ --jobname=STRING
+ --output-directory=DIR [default: somewhere in the temporary directory]
+ --output-format=FORMAT FORMAT is `pdf' or `dvi'. [default: pdf]
+
+%s
+]], arg[0] or 'texlua cluttex.lua', COPYRIGHT_NOTICE))
+end
+
+local option_spec = {
+ -- Options for ClutTeX
+ {
+ short = "e",
+ long = "engine",
+ param = true,
+ },
+ {
+ short = "o",
+ long = "output",
+ param = true,
+ },
+ {
+ long = "fresh",
+ },
+ {
+ long = "max-iterations",
+ param = true,
+ },
+ {
+ long = "start-with-draft",
+ },
+ {
+ long = "change-directory",
+ boolean = true,
+ },
+ {
+ long = "watch",
+ },
+ {
+ short = "h",
+ long = "help",
+ allow_single_hyphen = true,
+ },
+ {
+ short = "v",
+ long = "version",
+ },
+ {
+ short = "V",
+ long = "verbose",
+ },
+ {
+ long = "color",
+ param = true,
+ default = "always",
+ },
+ {
+ long = "includeonly",
+ param = true,
+ },
+ -- Options for TeX
+ {
+ long = "synctex",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "file-line-error",
+ boolean = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "interaction",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "halt-on-error",
+ boolean = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "shell-escape",
+ boolean = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "shell-restricted",
+ allow_single_hyphen = true,
+ },
+ {
+ long = "jobname",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "fmt",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "output-directory",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "output-format",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "tex-option",
+ param = true,
+ },
+ {
+ long = "tex-options",
+ param = true,
+ },
+ {
+ long = "dvipdfmx-option",
+ param = true,
+ },
+ {
+ long = "dvipdfmx-options",
+ param = true,
+ },
+ {
+ long = "makeindex",
+ param = true,
+ },
+ {
+ long = "bibtex",
+ param = true,
+ },
+ {
+ long = "biber",
+ param = true,
+ default = "biber",
+ },
+ {
+ long = "makeglossaries",
+ param = true,
+ default = "makeglossaries",
+ },
+}
+
+-- Default values for options
+local function set_default_values(options)
+ if options.max_iterations == nil then
+ options.max_iterations = 3
+ end
+
+ if options.interaction == nil then
+ options.interaction = "nonstopmode"
+ end
+
+ if options.file_line_error == nil then
+ options.file_line_error = true
+ end
+
+ if options.halt_on_error == nil then
+ options.halt_on_error = true
+ end
+end
+
+-- inputfile, engine, options = handle_cluttex_options(arg)
+local function handle_cluttex_options(arg)
+ -- Parse options
+ local option_and_params, non_option_index = parseoption(arg, option_spec)
+
+ -- Handle options
+ local options = {
+ tex_extraoptions = {},
+ dvipdfmx_extraoptions = {},
+ }
+ CLUTTEX_VERBOSITY = 0
+ for _,option in ipairs(option_and_params) do
+ local name = option[1]
+ local param = option[2]
+
+ if name == "engine" then
+ assert(options.engine == nil, "multiple --engine options")
+ options.engine = param
+
+ elseif name == "output" then
+ assert(options.output == nil, "multiple --output options")
+ options.output = param
+
+ elseif name == "fresh" then
+ assert(options.fresh == nil, "multiple --fresh options")
+ options.fresh = true
+
+ elseif name == "max-iterations" then
+ assert(options.max_iterations == nil, "multiple --max-iterations options")
+ options.max_iterations = assert(tonumber(param), "invalid value for --max-iterations option")
+ assert(options.max_iterations >= 1, "invalid value for --max-iterations option")
+
+ elseif name == "start-with-draft" then
+ assert(options.start_with_draft == nil, "multiple --start-with-draft options")
+ options.start_with_draft = true
+
+ elseif name == "watch" then
+ assert(options.watch == nil, "multiple --watch options")
+ options.watch = true
+
+ elseif name == "help" then
+ usage(arg)
+ os.exit(0)
+
+ elseif name == "version" then
+ io.stderr:write("cluttex ",CLUTTEX_VERSION,"\n")
+ os.exit(0)
+
+ elseif name == "verbose" then
+ CLUTTEX_VERBOSITY = CLUTTEX_VERBOSITY + 1
+
+ elseif name == "color" then
+ assert(options.color == nil, "multiple --collor options")
+ options.color = param
+ message.set_colors(options.color)
+
+ elseif name == "change-directory" then
+ assert(options.change_directory == nil, "multiple --change-directory options")
+ options.change_directory = param
+
+ elseif name == "includeonly" then
+ assert(options.includeonly == nil, "multiple --includeonly options")
+ options.includeonly = param
+
+ -- Options for TeX
+ elseif name == "synctex" then
+ assert(options.synctex == nil, "multiple --synctex options")
+ options.synctex = param
+
+ elseif name == "file-line-error" then
+ options.file_line_error = param
+
+ elseif name == "interaction" then
+ assert(options.interaction == nil, "multiple --interaction options")
+ assert(param == "batchmode" or param == "nonstopmode" or param == "scrollmode" or param == "errorstopmode", "invalid argument for --interaction")
+ options.interaction = param
+
+ elseif name == "halt-on-error" then
+ options.halt_on_error = param
+
+ elseif name == "shell-escape" then
+ assert(options.shell_escape == nil and options.shell_restricted == nil, "multiple --(no-)shell-escape or --shell-restricted options")
+ options.shell_escape = param
+
+ elseif name == "shell-restricted" then
+ assert(options.shell_escape == nil and options.shell_restricted == nil, "multiple --(no-)shell-escape or --shell-restricted options")
+ options.shell_restricted = true
+
+ elseif name == "jobname" then
+ assert(options.jobname == nil, "multiple --jobname options")
+ options.jobname = param
+
+ elseif name == "fmt" then
+ assert(options.fmt == nil, "multiple --fmt options")
+ options.fmt = param
+
+ elseif name == "output-directory" then
+ assert(options.output_directory == nil, "multiple --output-directory options")
+ options.output_directory = param
+
+ elseif name == "output-format" then
+ assert(options.output_format == nil, "multiple --output-format options")
+ assert(param == "pdf" or param == "dvi", "invalid argument for --output-format")
+ options.output_format = param
+
+ elseif name == "tex-option" then
+ table.insert(options.tex_extraoptions, shellutil.escape(param))
+
+ elseif name == "tex-options" then
+ table.insert(options.tex_extraoptions, param)
+
+ elseif name == "dvipdfmx-option" then
+ table.insert(options.dvipdfmx_extraoptions, shellutil.escape(param))
+
+ elseif name == "dvipdfmx-options" then
+ table.insert(options.dvipdfmx_extraoptions, param)
+
+ elseif name == "makeindex" then
+ assert(options.makeindex == nil, "multiple --makeindex options")
+ options.makeindex = param
+
+ elseif name == "bibtex" then
+ assert(options.bibtex == nil, "multiple --bibtex options")
+ assert(options.biber == nil, "multiple --bibtex/--biber options")
+ options.bibtex = param
+
+ elseif name == "biber" then
+ assert(options.biber == nil, "multiple --biber options")
+ assert(options.bibtex == nil, "multiple --bibtex/--biber options")
+ options.biber = param
+
+ elseif name == "makeglossaries" then
+ assert(options.makeglossaries == nil, "multiple --makeglossaries options")
+ options.makeglossaries = param
+
+ end
+ end
+
+ if options.color == nil then
+ message.set_colors("auto")
+ end
+
+ -- Handle non-options (i.e. input file)
+ if non_option_index > #arg then
+ -- No input file given
+ usage(arg)
+ os.exit(1)
+ elseif non_option_index < #arg then
+ message.error("Multiple input files are not supported.")
+ os.exit(1)
+ end
+ local inputfile = arg[non_option_index]
+
+ -- If run as 'cllualatex', then the default engine is lualatex
+ if options.engine == nil and type(arg[0]) == "string" then
+ local basename = pathutil.trimext(pathutil.basename(arg[0]))
+ local engine_part = string.match(basename, "^cl(%w+)$")
+ if engine_part and KnownEngines[engine_part] then
+ options.engine = engine_part
+ end
+ end
+
+ if options.engine == nil then
+ message.error("Engine not specified.")
+ os.exit(1)
+ end
+ local engine = KnownEngines[options.engine]
+ if not engine then
+ message.error("Unknown engine name '", options.engine, "'.")
+ os.exit(1)
+ end
+
+ set_default_values(options)
+
+ return inputfile, engine, options
+end
+
+return {
+ usage = usage,
+ handle_cluttex_options = handle_cluttex_options,
+}
+end
+package.preload["texrunner.isatty"] = function(...)
+--[[
+ Copyright 2018 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+if os.type == "unix" then
+ -- Try luaposix
+ local succ, M = pcall(function()
+ local isatty = require "posix.unistd".isatty
+ local fileno = require "posix.stdio".fileno
+ return {
+ isatty = function(file)
+ return isatty(fileno(file)) == 1
+ end,
+ }
+ end)
+ if succ then
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: isatty found via luaposix\n")
+ end
+ return M
+ else
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: luaposix not found: ", M, "\n")
+ end
+ end
+
+ -- Try LuaJIT-like FFI
+ local succ, M = pcall(function()
+ local ffi = require "ffi"
+ ffi.cdef[[
+int isatty(int fd);
+int fileno(void *stream);
+]]
+ local isatty = assert(ffi.C.isatty, "isatty not found")
+ local fileno = assert(ffi.C.fileno, "fileno not found")
+ return {
+ isatty = function(file)
+ -- LuaJIT converts Lua's file handles into FILE* (void*)
+ return isatty(fileno(file)) ~= 0
+ end
+ }
+ end)
+ if succ then
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: isatty found via FFI (Unix)\n")
+ end
+ return M
+ else
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: FFI (Unix) not found: ", M, "\n")
+ end
+ end
+
+else
+ -- Try LuaJIT
+ -- TODO: Try to detect MinTTY using GetFileInformationByHandleEx
+ local succ, M = pcall(function()
+ local ffi = require "ffi"
+ local bitlib = assert(bit32 or bit, "Neither bit32 (Lua 5.2) nor bit (LuaJIT) found") -- Lua 5.2 or LuaJIT
+ ffi.cdef[[
+int _isatty(int fd);
+int _fileno(void *stream);
+void *_get_osfhandle(int fd); // should return intptr_t
+typedef int BOOL;
+typedef uint32_t DWORD;
+typedef int FILE_INFO_BY_HANDLE_CLASS; // ???
+typedef struct _FILE_NAME_INFO {
+DWORD FileNameLength;
+uint16_t FileName[?];
+} FILE_NAME_INFO;
+DWORD GetFileType(void *hFile);
+BOOL GetFileInformationByHandleEx(void *hFile, FILE_INFO_BY_HANDLE_CLASS fic, void *fileinfo, DWORD dwBufferSize);
+BOOL GetConsoleMode(void *hConsoleHandle, DWORD* lpMode);
+BOOL SetConsoleMode(void *hConsoleHandle, DWORD dwMode);
+]]
+ local isatty = assert(ffi.C._isatty, "_isatty not found")
+ local fileno = assert(ffi.C._fileno, "_fileno not found")
+ local get_osfhandle = assert(ffi.C._get_osfhandle, "_get_osfhandle not found")
+ local GetFileType = assert(ffi.C.GetFileType, "GetFileType not found")
+ local GetFileInformationByHandleEx = assert(ffi.C.GetFileInformationByHandleEx, "GetFileInformationByHandleEx not found")
+ local GetConsoleMode = assert(ffi.C.GetConsoleMode, "GetConsoleMode not found")
+ local SetConsoleMode = assert(ffi.C.SetConsoleMode, "SetConsoleMode not found")
+ local function wide_to_narrow(array, length)
+ local t = {}
+ for i = 0, length - 1 do
+ table.insert(t, string.char(math.min(array[i], 0xff)))
+ end
+ return table.concat(t, "")
+ end
+ local function is_mintty(fd)
+ local handle = get_osfhandle(fd)
+ local filetype = GetFileType(handle)
+ if filetype ~= 0x0003 then -- not FILE_TYPE_PIPE (0x0003)
+ -- mintty must be a pipe
+ if CLUTTEX_VERBOSITY >= 4 then
+ io.stderr:write("ClutTeX: not a pipe\n")
+ end
+ return false
+ end
+ local nameinfo = ffi.new("FILE_NAME_INFO", 32768)
+ local FileNameInfo = 2 -- : FILE_INFO_BY_HANDLE_CLASS
+ if GetFileInformationByHandleEx(handle, FileNameInfo, nameinfo, ffi.sizeof("FILE_NAME_INFO", 32768)) ~= 0 then
+ local filename = wide_to_narrow(nameinfo.FileName, math.floor(nameinfo.FileNameLength / 2))
+ -- \(cygwin|msys)-<hex digits>-pty<N>-(from|to)-master
+ if CLUTTEX_VERBOSITY >= 4 then
+ io.stderr:write("ClutTeX: GetFileInformationByHandleEx returned ", filename, "\n")
+ end
+ local a, b = string.match(filename, "^\\(%w+)%-%x+%-pty%d+%-(%w+)%-master$")
+ if (a == "cygwin" or a == "msys") and (b == "from" or b == "to") then
+ return true
+ end
+ else
+ if CLUTTEX_VERBOSITY >= 4 then
+ io.stderr:write("ClutTeX: GetFileInformationByHandleEx failed\n")
+ end
+ end
+ return false
+ end
+ return {
+ isatty = function(file)
+ -- LuaJIT converts Lua's file handles into FILE* (void*)
+ local fd = fileno(file)
+ return isatty(fd) ~= 0 or is_mintty(fd)
+ end,
+ enable_console_colors = function(file)
+ local fd = fileno(file)
+ if isatty(fd) ~= 0 then
+ local handle = get_osfhandle(fd)
+ local modePtr = ffi.new("DWORD[1]")
+ local result = GetConsoleMode(handle, modePtr)
+ if result ~= 0 then
+ local ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
+ result = SetConsoleMode(handle, bitlib.bor(modePtr[0], ENABLE_VIRTUAL_TERMINAL_PROCESSING))
+ if result == 0 then
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: SetConsoleMode failed\n")
+ end
+ end
+ else
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: GetConsoleMode failed\n")
+ end
+ end
+ end
+ end,
+ }
+ end)
+ if succ then
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: isatty found via FFI (Windows)\n")
+ end
+ return M
+ else
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: FFI (Windows) not found: ", M, "\n")
+ end
+ end
+end
+
+return {
+ isatty = function(file)
+ return false
+ end,
+}
+end
+package.preload["texrunner.message"] = function(...)
+--[[
+ Copyright 2018 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local use_colors = false
+
+local function set_colors(mode)
+ local M
+ if mode == "always" then
+ use_colors = true
+ M = require "texrunner.isatty"
+ if M.enable_console_colors then
+ M.enable_console_colors(io.stderr)
+ end
+ elseif mode == "never" then
+ use_colors = false
+ elseif mode == "auto" then
+ M = require "texrunner.isatty"
+ use_colors = M.isatty(io.stderr)
+ else
+ error "The value of --color option must be one of 'auto', 'always', or 'never'."
+ end
+ if use_colors and M.enable_console_colors then
+ M.enable_console_colors(io.stderr)
+ end
+end
+
+-- ESCAPE: hex 1B = dec 27 = oct 33
+
+local CMD = {
+ reset = "\027[0m",
+ underline = "\027[4m",
+ fg_black = "\027[30m",
+ fg_red = "\027[31m",
+ fg_green = "\027[32m",
+ fg_yellow = "\027[33m",
+ fg_blue = "\027[34m",
+ fg_magenta = "\027[35m",
+ fg_cyan = "\027[36m",
+ fg_white = "\027[37m",
+ fg_reset = "\027[39m",
+ bg_black = "\027[40m",
+ bg_red = "\027[41m",
+ bg_green = "\027[42m",
+ bg_yellow = "\027[43m",
+ bg_blue = "\027[44m",
+ bg_magenta = "\027[45m",
+ bg_cyan = "\027[46m",
+ bg_white = "\027[47m",
+ bg_reset = "\027[49m",
+ fg_x_black = "\027[90m",
+ fg_x_red = "\027[91m",
+ fg_x_green = "\027[92m",
+ fg_x_yellow = "\027[93m",
+ fg_x_blue = "\027[94m",
+ fg_x_magenta = "\027[95m",
+ fg_x_cyan = "\027[96m",
+ fg_x_white = "\027[97m",
+ bg_x_black = "\027[100m",
+ bg_x_red = "\027[101m",
+ bg_x_green = "\027[102m",
+ bg_x_yellow = "\027[103m",
+ bg_x_blue = "\027[104m",
+ bg_x_magenta = "\027[105m",
+ bg_x_cyan = "\027[106m",
+ bg_x_white = "\027[107m",
+}
+
+local function exec_msg(commandline)
+ if use_colors then
+ io.stderr:write(CMD.fg_x_white, CMD.bg_red, "[EXEC]", CMD.reset, " ", CMD.fg_red, commandline, CMD.reset, "\n")
+ else
+ io.stderr:write("[EXEC] ", commandline, "\n")
+ end
+end
+
+local function error_msg(...)
+ local message = table.concat({...}, "")
+ if use_colors then
+ io.stderr:write(CMD.fg_x_white, CMD.bg_red, "[ERROR]", CMD.reset, " ", CMD.fg_red, message, CMD.reset, "\n")
+ else
+ io.stderr:write("[ERROR] ", message, "\n")
+ end
+end
+
+local function warn_msg(...)
+ local message = table.concat({...}, "")
+ if use_colors then
+ io.stderr:write(CMD.fg_x_white, CMD.bg_red, "[WARN]", CMD.reset, " ", CMD.fg_blue, message, CMD.reset, "\n")
+ else
+ io.stderr:write("[WARN] ", message, "\n")
+ end
+end
+
+local function diag_msg(...)
+ local message = table.concat({...}, "")
+ if use_colors then
+ io.stderr:write(CMD.fg_x_white, CMD.bg_red, "[DIAG]", CMD.reset, " ", CMD.fg_blue, message, CMD.reset, "\n")
+ else
+ io.stderr:write("[DIAG] ", message, "\n")
+ end
+end
+
+local function info_msg(...)
+ local message = table.concat({...}, "")
+ if use_colors then
+ io.stderr:write(CMD.fg_x_white, CMD.bg_red, "[INFO]", CMD.reset, " ", CMD.fg_magenta, message, CMD.reset, "\n")
+ else
+ io.stderr:write("[INFO] ", message, "\n")
+ end
+end
+
+return {
+ set_colors = set_colors,
+ exec = exec_msg,
+ error = error_msg,
+ warn = warn_msg,
+ diag = diag_msg,
+ info = info_msg,
+}
+end
+--[[
+ Copyright 2016,2018 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+CLUTTEX_VERSION = "v0.1"
+
+-- Standard libraries
+local coroutine = coroutine
+local tostring = tostring
+
+-- External libraries (included in texlua)
+local filesys = require "lfs"
+local md5 = require "md5"
+-- local kpse = require "kpse"
+
+-- My own modules
+local pathutil = require "texrunner.pathutil"
+local fsutil = require "texrunner.fsutil"
+local shellutil = require "texrunner.shellutil"
+local reruncheck = require "texrunner.reruncheck"
+local luatexinit = require "texrunner.luatexinit"
+local recoverylib = require "texrunner.recovery"
+local message = require "texrunner.message"
+local extract_bibtex_from_aux_file = require "texrunner.auxfile".extract_bibtex_from_aux_file
+local handle_cluttex_options = require "texrunner.handleoption".handle_cluttex_options
+
+-- arguments: input file name, jobname, etc...
+local function genOutputDirectory(...)
+ -- The name of the temporary directory is based on the path of input file.
+ local message = table.concat({...}, "\0")
+ local hash = md5.sumhexa(message)
+ local tmpdir = os.getenv("TMPDIR") or os.getenv("TMP") or os.getenv("TEMP")
+ if tmpdir == nil then
+ local home = os.getenv("HOME") or os.getenv("USERPROFILE") or error("environment variable 'TMPDIR' not set!")
+ tmpdir = pathutil.join(home, ".latex-build-temp")
+ end
+ return pathutil.join(tmpdir, 'latex-build-' .. hash)
+end
+
+local inputfile, engine, options = handle_cluttex_options(arg)
+
+local jobname = options.jobname or pathutil.basename(pathutil.trimext(inputfile))
+assert(jobname ~= "", "jobname cannot be empty")
+
+if options.output_format == nil then
+ options.output_format = "pdf"
+end
+local output_extension
+if options.output_format == "dvi" then
+ output_extension = engine.dvi_extension or "dvi"
+else
+ output_extension = "pdf"
+end
+
+if options.output == nil then
+ options.output = jobname .. "." .. output_extension
+end
+
+-- Prepare output directory
+if options.output_directory == nil then
+ local inputfile_abs = pathutil.abspath(inputfile)
+ options.output_directory = genOutputDirectory(inputfile_abs, jobname, options.engine)
+
+ if not fsutil.isdir(options.output_directory) then
+ assert(fsutil.mkdir_rec(options.output_directory))
+
+ elseif options.fresh then
+ -- The output directory exists and --fresh is given:
+ -- Remove all files in the output directory
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("Cleaning '", options.output_directory, "'...")
+ end
+ assert(fsutil.remove_rec(options.output_directory))
+ assert(filesys.mkdir(options.output_directory))
+ end
+
+elseif options.fresh then
+ message.error("--fresh and --output-directory cannot be used together.")
+ os.exit(1)
+end
+
+local pathsep = ":"
+if os.type == "windows" then
+ pathsep = ";"
+end
+
+local original_wd = filesys.currentdir()
+if options.change_directory then
+ local TEXINPUTS = os.getenv("TEXINPUTS") or ""
+ filesys.chdir(options.output_directory)
+ options.output = pathutil.abspath(options.output, original_wd)
+ os.setenv("TEXINPUTS", original_wd .. pathsep .. TEXINPUTS)
+end
+if options.bibtex or options.biber then
+ local BIBINPUTS = os.getenv("BIBINPUTS") or ""
+ options.output = pathutil.abspath(options.output, original_wd)
+ os.setenv("BIBINPUTS", original_wd .. pathsep .. BIBINPUTS)
+end
+
+-- Set `max_print_line' environment variable if not already set.
+if os.getenv("max_print_line") == nil then
+ os.setenv("max_print_line", "65536")
+end
+-- TODO: error_line, half_error_line
+--[[
+ According to texmf.cnf:
+ 45 < error_line < 255,
+ 30 < half_error_line < error_line - 15,
+ 60 <= max_print_line.
+]]
+
+local function path_in_output_directory(ext)
+ return pathutil.join(options.output_directory, jobname .. "." .. ext)
+end
+
+local recorderfile = path_in_output_directory("fls")
+local recorderfile2 = path_in_output_directory("cluttex-fls")
+
+local tex_options = {
+ interaction = options.interaction,
+ file_line_error = options.file_line_error,
+ halt_on_error = options.halt_on_error,
+ synctex = options.synctex,
+ output_directory = options.output_directory,
+ shell_escape = options.shell_escape,
+ shell_restricted = options.shell_restricted,
+ jobname = options.jobname,
+ fmt = options.fmt,
+ extraoptions = options.tex_extraoptions,
+}
+if options.output_format ~= "pdf" and engine.supports_pdf_generation then
+ tex_options.output_format = options.output_format
+end
+
+-- Setup LuaTeX initialization script
+if engine.is_luatex then
+ local initscriptfile = path_in_output_directory("cluttexinit.lua")
+ luatexinit.create_initialization_script(initscriptfile, tex_options)
+ tex_options.lua_initialization_script = initscriptfile
+end
+
+-- Run TeX command (*tex, *latex)
+-- should_rerun, newauxstatus = single_run([auxstatus])
+-- This function should be run in a coroutine.
+local function single_run(auxstatus, iteration)
+ local minted = false
+ local bibtex_aux_hash = nil
+ local mainauxfile = path_in_output_directory("aux")
+ if fsutil.isfile(recorderfile) then
+ -- Recorder file already exists
+ local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options)
+ if engine.is_luatex and fsutil.isfile(recorderfile2) then
+ filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap)
+ end
+ auxstatus = reruncheck.collectfileinfo(filelist, auxstatus)
+ for _,fileinfo in ipairs(filelist) do
+ if string.match(fileinfo.path, "minted/minted%.sty$") then
+ minted = true
+ break
+ end
+ end
+ if options.bibtex then
+ local biblines = extract_bibtex_from_aux_file(mainauxfile, options.output_directory)
+ if #biblines > 0 then
+ bibtex_aux_hash = md5.sum(table.concat(biblines, "\n"))
+ end
+ end
+ else
+ -- This is the first execution
+ if auxstatus ~= nil then
+ message.error("Recorder file was not generated during the execution!")
+ os.exit(1)
+ end
+ auxstatus = {}
+ end
+ --local timestamp = os.time()
+
+ if options.includeonly then
+ tex_options.tex_injection = string.format("%s\\includeonly{%s}", tex_options.tex_injection or "", options.includeonly)
+ end
+
+ if minted and not (tex_options.tex_injection and string.find(tex_options.tex_injection,"minted") == nil) then
+ tex_options.tex_injection = string.format("%s\\PassOptionsToPackage{outputdir=%s}{minted}", tex_options.tex_injection or "", options.output_directory)
+ end
+
+ local current_tex_options, lightweight_mode = tex_options, false
+ if iteration == 1 and options.start_with_draft then
+ current_tex_options = {}
+ for k,v in pairs(tex_options) do
+ current_tex_options[k] = v
+ end
+ if engine.supports_draftmode then
+ current_tex_options.draftmode = true
+ options.start_with_draft = false
+ end
+ current_tex_options.interaction = "batchmode"
+ lightweight_mode = true
+ else
+ current_tex_options.draftmode = false
+ end
+
+ local command = engine:build_command(inputfile, current_tex_options)
+
+ local execlog -- the contents of .log file
+
+ local recovered = false
+ local function recover()
+ -- Check log file
+ if not execlog then
+ local logfile = assert(io.open(path_in_output_directory("log")))
+ execlog = logfile:read("*a")
+ logfile:close()
+ end
+ recovered = recoverylib.try_recovery{
+ execlog = execlog,
+ auxfile = path_in_output_directory("aux"),
+ options = options,
+ original_wd = original_wd,
+ }
+ return recovered
+ end
+ coroutine.yield(command, recover) -- Execute the command
+ if recovered then
+ return true, {}
+ end
+
+ local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options)
+ if engine.is_luatex and fsutil.isfile(recorderfile2) then
+ filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap)
+ end
+
+ if not execlog then
+ local logfile = assert(io.open(path_in_output_directory("log")))
+ execlog = logfile:read("*a")
+ logfile:close()
+ end
+
+ if options.makeindex then
+ -- Look for .idx files and run MakeIndex
+ for _,file in ipairs(filelist) do
+ if pathutil.ext(file.path) == "idx" then
+ -- Run makeindex if the .idx file is new or updated
+ local idxfileinfo = {path = file.path, abspath = file.abspath, kind = "auxiliary"}
+ local output_ind = pathutil.replaceext(file.abspath, "ind")
+ if reruncheck.comparefileinfo({idxfileinfo}, auxstatus) or reruncheck.comparefiletime(file.abspath, output_ind, auxstatus) then
+ local idx_dir = pathutil.dirname(file.abspath)
+ local makeindex_command = {
+ "cd", shellutil.escape(idx_dir), "&&",
+ options.makeindex, -- Do not escape options.makeindex to allow additional options
+ "-o", pathutil.basename(output_ind),
+ pathutil.basename(file.abspath)
+ }
+ coroutine.yield(table.concat(makeindex_command, " "))
+ table.insert(filelist, {path = output_ind, abspath = output_ind, kind = "auxiliary"})
+ else
+ local succ, err = filesys.touch(output_ind)
+ if not succ then
+ message.warn("Failed to touch " .. output_ind .. " (" .. err .. ")")
+ end
+ end
+ end
+ end
+ else
+ -- Check log file
+ if string.find(execlog, "No file [^\n]+%.ind%.") then
+ message.diag("You may want to use --makeindex option.")
+ end
+ end
+
+ if options.makeglossaries then
+ -- Look for .glo files and run makeglossaries
+ for _,file in ipairs(filelist) do
+ if pathutil.ext(file.path) == "glo" then
+ -- Run makeglossaries if the .glo file is new or updated
+ local glofileinfo = {path = file.path, abspath = file.abspath, kind = "auxiliary"}
+ local output_gls = pathutil.replaceext(file.abspath, "gls")
+ if reruncheck.comparefileinfo({glofileinfo}, auxstatus) or reruncheck.comparefiletime(file.abspath, output_gls, auxstatus) then
+ local makeglossaries_command = {
+ options.makeglossaries,
+ "-d", shellutil.escape(options.output_directory),
+ pathutil.trimext(pathutil.basename(file.path))
+ }
+ coroutine.yield(table.concat(makeglossaries_command, " "))
+ table.insert(filelist, {path = output_gls, abspath = output_gls, kind = "auxiliary"})
+ else
+ local succ, err = filesys.touch(output_gls)
+ if not succ then
+ message.warn("Failed to touch " .. output_ind .. " (" .. err .. ")")
+ end
+ end
+ end
+ end
+ else
+ -- Check log file
+ if string.find(execlog, "No file [^\n]+%.gls%.") then
+ message.diag("You may want to use --makeglossaries option.")
+ end
+ end
+
+ if options.bibtex then
+ local biblines2 = extract_bibtex_from_aux_file(mainauxfile, options.output_directory)
+ local bibtex_aux_hash2
+ if #biblines2 > 0 then
+ bibtex_aux_hash2 = md5.sum(table.concat(biblines2, "\n"))
+ end
+ local output_bbl = path_in_output_directory("bbl")
+ if bibtex_aux_hash ~= bibtex_aux_hash2 or reruncheck.comparefiletime(mainauxfile, output_bbl, auxstatus) then
+ -- The input for BibTeX command has changed...
+ local bibtex_command = {
+ "cd", shellutil.escape(options.output_directory), "&&",
+ options.bibtex,
+ pathutil.basename(mainauxfile)
+ }
+ coroutine.yield(table.concat(bibtex_command, " "))
+ else
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("No need to run BibTeX.")
+ end
+ local succ, err = filesys.touch(output_bbl)
+ if not succ then
+ message.warn("Failed to touch " .. output_bbl .. " (" .. err .. ")")
+ end
+ end
+ elseif options.biber then
+ for _,file in ipairs(filelist) do
+ if pathutil.ext(file.path) == "bcf" then
+ -- Run biber if the .bcf file is new or updated
+ local bcffileinfo = {path = file.path, abspath = file.abspath, kind = "auxiliary"}
+ local output_bbl = pathutil.replaceext(file.abspath, "bbl")
+ if reruncheck.comparefileinfo({bcffileinfo}, auxstatus) or reruncheck.comparefiletime(file.abspath, output_bbl, auxstatus) then
+ local bbl_dir = pathutil.dirname(file.abspath)
+ local biber_command = {
+ options.biber, -- Do not escape options.biber to allow additional options
+ "--output-directory", shellutil.escape(options.output_directory),
+ pathutil.basename(file.abspath)
+ }
+ coroutine.yield(table.concat(biber_command, " "))
+ table.insert(filelist, {path = output_bbl, abspath = output_bbl, kind = "auxiliary"})
+ else
+ local succ, err = filesys.touch(output_bbl)
+ if not succ then
+ message.warn("Failed to touch " .. output_bbl .. " (" .. err .. ")")
+ end
+ end
+ end
+ end
+ else
+ -- Check log file
+ if string.find(execlog, "No file [^\n]+%.bbl%.") then
+ message.diag("You may want to use --bibtex or --biber option.")
+ end
+ end
+
+ if string.find(execlog, "No pages of output.") then
+ return "No pages of output."
+ end
+
+ local should_rerun, auxstatus = reruncheck.comparefileinfo(filelist, auxstatus)
+ return should_rerun or lightweight_mode, auxstatus
+end
+
+-- Run (La)TeX (possibly multiple times) and produce a PDF file.
+-- This function should be run in a coroutine.
+local function do_typeset_c()
+ local iteration = 0
+ local should_rerun, auxstatus
+ repeat
+ iteration = iteration + 1
+ should_rerun, auxstatus = single_run(auxstatus, iteration)
+ if should_rerun == "No pages of output." then
+ message.warn("No pages of output.")
+ return
+ end
+ until not should_rerun or iteration >= options.max_iterations
+
+ if should_rerun then
+ message.warn("LaTeX should be run once more.")
+ end
+
+ -- Successful
+ if options.output_format == "dvi" or engine.supports_pdf_generation then
+ -- Output file (DVI/PDF) is generated in the output directory
+ local outfile = path_in_output_directory(output_extension)
+ local oncopyerror
+ if os.type == "windows" then
+ oncopyerror = function()
+ message.error("Failed to copy file. Some applications may be locking the ", string.upper(options.output_format), " file.")
+ return false
+ end
+ end
+ coroutine.yield(fsutil.copy_command(outfile, options.output), oncopyerror)
+ if #options.dvipdfmx_extraoptions > 0 then
+ message.warn("--dvipdfmx-option[s] are ignored.")
+ end
+
+ else
+ -- DVI file is generated, but PDF file is wanted
+ local dvifile = path_in_output_directory("dvi")
+ local dvipdfmx_command = {"dvipdfmx", "-o", shellutil.escape(options.output)}
+ for _,v in ipairs(options.dvipdfmx_extraoptions) do
+ table.insert(dvipdfmx_command, v)
+ end
+ table.insert(dvipdfmx_command, shellutil.escape(dvifile))
+ coroutine.yield(table.concat(dvipdfmx_command, " "))
+ end
+
+ -- Copy SyncTeX file if necessary
+ if options.output_format == "pdf" then
+ local synctex = tonumber(options.synctex or "0")
+ local synctex_ext = nil
+ if synctex > 0 then
+ -- Compressed SyncTeX file (.synctex.gz)
+ synctex_ext = "synctex.gz"
+ elseif synctex < 0 then
+ -- Uncompressed SyncTeX file (.synctex)
+ synctex_ext = "synctex"
+ end
+ if synctex_ext then
+ coroutine.yield(fsutil.copy_command(path_in_output_directory(synctex_ext), pathutil.replaceext(options.output, synctex_ext)))
+ end
+ end
+end
+
+local function do_typeset()
+ -- Execute the command string yielded by do_typeset_c
+ for command, recover in coroutine.wrap(do_typeset_c) do
+ message.exec(command)
+ local success, termination, status_or_signal = os.execute(command)
+ if type(success) == "number" then -- Lua 5.1 or LuaTeX
+ local code = success
+ success = code == 0
+ termination = nil
+ status_or_signal = code
+ end
+ if not success and not (recover and recover()) then
+ if termination == "exit" then
+ message.error("Command exited abnormally: exit status ", tostring(status_or_signal))
+ elseif termination == "signal" then
+ message.error("Command exited abnormally: signal ", tostring(status_or_signal))
+ else
+ message.error("Command exited abnormally: ", tostring(status_or_signal))
+ end
+ return false, termination, status_or_signal
+ end
+ end
+ -- Successful
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("Command exited successfully")
+ end
+ return true
+end
+
+if options.watch then
+ -- Watch mode
+ local success, status = do_typeset()
+ local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options)
+ if engine.is_luatex and fsutil.isfile(recorderfile2) then
+ filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap)
+ end
+ local input_files_to_watch = {}
+ for _,fileinfo in ipairs(filelist) do
+ if fileinfo.kind == "input" then
+ table.insert(input_files_to_watch, fileinfo.abspath)
+ end
+ end
+ local fswatch_command = {"fswatch", "--event=Updated", "--"}
+ for _,path in ipairs(input_files_to_watch) do
+ table.insert(fswatch_command, shellutil.escape(path))
+ end
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.exec(table.concat(fswatch_command, " "))
+ end
+ local fswatch = assert(io.popen(table.concat(fswatch_command, " "), "r"))
+ for l in fswatch:lines() do
+ local found = false
+ for _,path in ipairs(input_files_to_watch) do
+ if l == path then
+ found = true
+ break
+ end
+ end
+ if found then
+ local success, status = do_typeset()
+ if not success then
+ -- Not successful
+ end
+ end
+ end
+
+else
+ -- Not in watch mode
+ local success, status = do_typeset()
+ if not success then
+ os.exit(1)
+ end
+end
diff --git a/Master/bin/x86_64-cygwin/cllualatex b/Master/bin/x86_64-cygwin/cllualatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/x86_64-cygwin/cllualatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/x86_64-cygwin/cluttex b/Master/bin/x86_64-cygwin/cluttex
new file mode 120000
index 00000000000..1061de93aae
--- /dev/null
+++ b/Master/bin/x86_64-cygwin/cluttex
@@ -0,0 +1 @@
+../../texmf-dist/scripts/cluttex/cluttex.lua \ No newline at end of file
diff --git a/Master/bin/x86_64-cygwin/clxelatex b/Master/bin/x86_64-cygwin/clxelatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/x86_64-cygwin/clxelatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/x86_64-darwin/cllualatex b/Master/bin/x86_64-darwin/cllualatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/x86_64-darwin/cllualatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/x86_64-darwin/cluttex b/Master/bin/x86_64-darwin/cluttex
new file mode 120000
index 00000000000..1061de93aae
--- /dev/null
+++ b/Master/bin/x86_64-darwin/cluttex
@@ -0,0 +1 @@
+../../texmf-dist/scripts/cluttex/cluttex.lua \ No newline at end of file
diff --git a/Master/bin/x86_64-darwin/clxelatex b/Master/bin/x86_64-darwin/clxelatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/x86_64-darwin/clxelatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/x86_64-darwinlegacy/cllualatex b/Master/bin/x86_64-darwinlegacy/cllualatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/x86_64-darwinlegacy/cllualatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/x86_64-darwinlegacy/cluttex b/Master/bin/x86_64-darwinlegacy/cluttex
new file mode 120000
index 00000000000..1061de93aae
--- /dev/null
+++ b/Master/bin/x86_64-darwinlegacy/cluttex
@@ -0,0 +1 @@
+../../texmf-dist/scripts/cluttex/cluttex.lua \ No newline at end of file
diff --git a/Master/bin/x86_64-darwinlegacy/clxelatex b/Master/bin/x86_64-darwinlegacy/clxelatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/x86_64-darwinlegacy/clxelatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/x86_64-linux/cllualatex b/Master/bin/x86_64-linux/cllualatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/x86_64-linux/cllualatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/x86_64-linux/cluttex b/Master/bin/x86_64-linux/cluttex
new file mode 120000
index 00000000000..1061de93aae
--- /dev/null
+++ b/Master/bin/x86_64-linux/cluttex
@@ -0,0 +1 @@
+../../texmf-dist/scripts/cluttex/cluttex.lua \ No newline at end of file
diff --git a/Master/bin/x86_64-linux/clxelatex b/Master/bin/x86_64-linux/clxelatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/x86_64-linux/clxelatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/x86_64-linuxmusl/cllualatex b/Master/bin/x86_64-linuxmusl/cllualatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/x86_64-linuxmusl/cllualatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/x86_64-linuxmusl/cluttex b/Master/bin/x86_64-linuxmusl/cluttex
new file mode 120000
index 00000000000..1061de93aae
--- /dev/null
+++ b/Master/bin/x86_64-linuxmusl/cluttex
@@ -0,0 +1 @@
+../../texmf-dist/scripts/cluttex/cluttex.lua \ No newline at end of file
diff --git a/Master/bin/x86_64-linuxmusl/clxelatex b/Master/bin/x86_64-linuxmusl/clxelatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/x86_64-linuxmusl/clxelatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/x86_64-solaris/cllualatex b/Master/bin/x86_64-solaris/cllualatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/x86_64-solaris/cllualatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/bin/x86_64-solaris/cluttex b/Master/bin/x86_64-solaris/cluttex
new file mode 120000
index 00000000000..1061de93aae
--- /dev/null
+++ b/Master/bin/x86_64-solaris/cluttex
@@ -0,0 +1 @@
+../../texmf-dist/scripts/cluttex/cluttex.lua \ No newline at end of file
diff --git a/Master/bin/x86_64-solaris/clxelatex b/Master/bin/x86_64-solaris/clxelatex
new file mode 120000
index 00000000000..9c8398680de
--- /dev/null
+++ b/Master/bin/x86_64-solaris/clxelatex
@@ -0,0 +1 @@
+cluttex \ No newline at end of file
diff --git a/Master/texmf-dist/doc/support/cluttex/CHANGELOG.md b/Master/texmf-dist/doc/support/cluttex/CHANGELOG.md
new file mode 100644
index 00000000000..32408b1518e
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/CHANGELOG.md
@@ -0,0 +1,13 @@
+Version 0.1 (2018-10-10)
+-----
+
+Initial release.
+
+Basic features:
+
+* Does not clutter your working directory with `.aux`, `.log`, etc. files.
+* Does not prompt for input when there is a (La)TeX error.
+* With pTeX-like engines, automatically run dvipdfmx to produce PDF file.
+* Automatically re-run (La)TeX to resolve cross-references and other things.
+* Watch input files for change (requires an external program). [`--watch` option]
+* Support for MakeIndex, BibTeX, Biber, makeglossaries commands. [`--makeindex`, `--bibtex`, `--biber`, `--makeglossaries` options]
diff --git a/Master/texmf-dist/doc/support/cluttex/COPYING b/Master/texmf-dist/doc/support/cluttex/COPYING
new file mode 100644
index 00000000000..94a9ed024d3
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/COPYING
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/Master/texmf-dist/doc/support/cluttex/Makefile b/Master/texmf-dist/doc/support/cluttex/Makefile
new file mode 100644
index 00000000000..778f0d3da7e
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/Makefile
@@ -0,0 +1,37 @@
+all: bin/cluttex bin/cluttex.bat
+
+.PHONY: all archive
+
+sources= \
+ src/texrunner/pathutil.lua \
+ src/texrunner/pathutil_unix.lua \
+ src/texrunner/pathutil_windows.lua \
+ src/texrunner/shellutil.lua \
+ src/texrunner/shellutil_unix.lua \
+ src/texrunner/shellutil_windows.lua \
+ src/texrunner/fsutil.lua \
+ src/texrunner/option.lua \
+ src/texrunner/tex_engine.lua \
+ src/texrunner/reruncheck.lua \
+ src/texrunner/auxfile.lua \
+ src/texrunner/luatexinit.lua \
+ src/texrunner/recovery.lua \
+ src/texrunner/handleoption.lua \
+ src/texrunner/isatty.lua \
+ src/texrunner/message.lua \
+ src/cluttex.lua
+
+bin/cluttex: $(sources) build.lua
+ @mkdir -p bin
+ lua build.lua --unix-shellscript $@
+ lua checkglobal.lua $@
+ chmod +x $@
+
+bin/cluttex.bat: $(sources) build.lua
+ @mkdir -p bin
+ lua build.lua --windows-batchfile $@
+ lua checkglobal.lua $@
+
+archive:
+ git archive --format=tar --prefix=cluttex/ -o cluttex.tar HEAD
+ gzip -k9 cluttex.tar
diff --git a/Master/texmf-dist/doc/support/cluttex/README.md b/Master/texmf-dist/doc/support/cluttex/README.md
new file mode 100644
index 00000000000..be317c4fda5
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/README.md
@@ -0,0 +1,122 @@
+ClutTeX: Process LaTeX document without cluttering your directory
+=====
+
+ClutTeX is a program to automatically process your LaTeX document.
+If necessary, it re-runs (La)TeX program to resolve cross-references and everything.
+
+One of its main feature is that, it does not clutter your working directory (but the final `.pdf` file is still brought for you).
+
+Japanese blog: [TeX 実行の自動化ツールを作った (ClutTeX)](https://blog.miz-ar.info/2016/12/cluttex/)
+
+Features
+-----
+
+* Does not clutter your working directory with `.aux`, `.log`, etc. files.
+* Does not prompt for input when there is a (La)TeX error.
+* With pTeX-like engines, automatically run dvipdfmx to produce PDF file.
+* Automatically re-run (La)TeX to resolve cross-references and other things.
+* Watch input files for change (requires an external program). [`--watch` option]
+* Support for MakeIndex, BibTeX, Biber, makeglossaries commands. [`--makeindex`, `--bibtex`, `--biber`, `--makeglossaries` options]
+
+Usage
+-----
+
+`$ cluttex -e pdflatex file.tex`
+
+More general form:
+
+`$ cluttex [OPTIONS] [--] INPUT.tex`
+
+See [example/](example/) for some examples.
+
+Install
+-----
+
+Click [Clone or download] button on GitHub and [Download ZIP].
+Unpack `cluttex-master.zip` and copy `bin/cluttex` (or `bin/cluttex.bat` on Windows) to somewhere in PATH.
+
+Command-line Options
+-----
+
+* `-e`, `--engine=ENGINE`
+ Specify which TeX engine/format to use.
+ `ENGINE` is one of the following:
+ `pdflatex`, `pdftex`,
+ `lualatex`, `luatex`, `luajittex`,
+ `xelatex`, `xetex`,
+ `latex`, `etex`, `tex`,
+ `platex`, `eptex`, `ptex`,
+ `uplatex`, `euptex`, `uptex`.
+* `-o`, `--output=FILE`
+ The name of output file. [default: `JOBNAME.FORMAT`]
+* `--fresh`
+ Clean intermediate files before running TeX.
+ Cannot be used with `--output-directory`.
+* `--max-iterations=N`
+ Maximum number of running TeX to resolve cross-references.
+ [default: 3]
+* `--[no-]change-directory`
+ Change the current working directory to the output directory when running TeX.
+* `--watch`
+ Watch input files for change.
+ Requires [fswatch](http://emcrisostomo.github.io/fswatch/) program to be installed.
+* `--color[=WHEN]`
+ Make ClutTeX's message colorful.
+ `WHEN` is one of `always`, `auto`, or `never`.
+ [default: `auto` if `--color` is omitted, `always` if `=WHEN` is omitted]
+* `--includeonly=NAMEs`
+ Insert `\includeonly{NAMEs}`.
+* `--tex-option=OPTION`
+ Pass `OPTION` to TeX as a single option.
+* `--tex-options=OPTIONs`
+ Pass `OPTIONs` to TeX as multiple options.
+* `--dvipdfmx-option[s]=OPTION[s]`
+ Same for dvipdfmx.
+* `-h`, `--help`
+ Print this message and exit.
+* `-v`, `--version`
+ Print version information and exit.
+* `-V`, `--verbose`
+ Be more verbose.
+
+Options to run auxiliary programs:
+
+* `--makeindex=COMMAND`
+ Use MakeIndex program to process `.idx` files.
+ (e.g. `--makeindex=makeindex`, or `--makeindex=mendex`)
+* `--bibtex=COMMAND`
+ Use BibTeX program to produce `.bbl` file from `.aux` files.
+ (e.g. `--bibtex=bibtex`, or `--bibtex=upbibtex`)
+* `--biber[=COMMAND]`
+ Use Biber program to produce `.bbl` file from `.bcf` file.
+* `--makeglossaries[=COMMAND]`
+ Use makeglossaries program to produce `.gls` file from `.glo` file.
+
+TeX-compatible options:
+
+* `--[no-]shell-escape`
+* `--shell-restricted`
+* `--synctex=NUMBER`
+* `--[no-]file-line-error`
+ [default: yes]
+* `--[no-]halt-on-error`
+ [default: yes]
+* `--interaction=STRING`
+ (`STRING`=`batchmode`/`nonstopmode`/`scrollmode`/`errorstopmode`)
+ [default: `nonstopmode`]
+* `--jobname=STRING`
+* `--fmt=FORMAT`
+* `--output-directory=DIR`
+ [default: somewhere in the temporary directory]
+* `--output-format=FORMAT`
+ Set output format (`pdf` or `dvi`).
+ [default: `pdf`]
+
+For TeX-compatible options, single-hypen forms are allowed (e.g. `-synctex=1` in addition to `--synctex=1`).
+
+If run as `cllualatex` or `clxelatex`, then the default engine is `lualatex` or `xelatex`, accordingly.
+
+License
+-----
+
+This program is distributed under GNU General Public License, version 3.
diff --git a/Master/texmf-dist/doc/support/cluttex/build.lua b/Master/texmf-dist/doc/support/cluttex/build.lua
new file mode 100644
index 00000000000..bda6cf637a2
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/build.lua
@@ -0,0 +1,162 @@
+--[[
+ Copyright 2016, 2018 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local srcdir = "src/"
+local mode
+local default_os
+if arg[1] == "--unix-shellscript" then
+ default_os, mode = "unix", "shellscript"
+ table.remove(arg, 1)
+elseif arg[1] == "--windows-batchfile" then
+ default_os, mode = "windows", "batchfile"
+ table.remove(arg, 1)
+end
+local outfile = arg[1]
+
+local modules = {
+ {
+ name = "texrunner.pathutil",
+ path = "texrunner/pathutil.lua",
+ path_unix = "texrunner/pathutil_unix.lua",
+ path_windows = "texrunner/pathutil_windows.lua",
+ },
+ {
+ name = "texrunner.shellutil",
+ path = "texrunner/shellutil.lua",
+ path_unix = "texrunner/shellutil_unix.lua",
+ path_windows = "texrunner/shellutil_windows.lua",
+ },
+ {
+ name = "texrunner.fsutil",
+ path = "texrunner/fsutil.lua",
+ },
+ {
+ name = "texrunner.option",
+ path = "texrunner/option.lua",
+ },
+ {
+ name = "texrunner.tex_engine",
+ path = "texrunner/tex_engine.lua",
+ },
+ {
+ name = "texrunner.reruncheck",
+ path = "texrunner/reruncheck.lua",
+ },
+ {
+ name = "texrunner.auxfile",
+ path = "texrunner/auxfile.lua",
+ },
+ {
+ name = "texrunner.luatexinit",
+ path = "texrunner/luatexinit.lua",
+ },
+ {
+ name = "texrunner.recovery",
+ path = "texrunner/recovery.lua",
+ },
+ {
+ name = "texrunner.handleoption",
+ path = "texrunner/handleoption.lua",
+ },
+ {
+ name = "texrunner.isatty",
+ path = "texrunner/isatty.lua",
+ },
+ {
+ name = "texrunner.message",
+ path = "texrunner/message.lua",
+ },
+}
+
+local imported_globals = {"io", "os", "string", "table", "package", "require", "assert", "error", "ipairs", "type", "select", "arg"}
+
+-- TODO: This code may interfere with the string literal embedded in luatexinit.lua
+local function strip_global_imports(code)
+ local function repl(s1, s2)
+ if s1 == s2 then
+ for i, v in ipairs(imported_globals) do
+ if v == s1 then
+ return ""
+ end
+ end
+ end
+ return nil
+ end
+ return (code:gsub("local (%w+) = (%w+)\n", repl))
+end
+
+local function strip_test_code(code)
+ return (code:gsub("%-%- TEST CODE\n(.-)%-%- END TEST CODE\n", ""))
+end
+
+local function load_module_code(path)
+ assert(loadfile(srcdir .. path)) -- Check syntax
+ return strip_test_code(assert(io.open(srcdir .. path, "r")):read("*a"))
+end
+
+assert(loadfile(srcdir .. "cluttex.lua")) -- Check syntax
+
+local shebang = nil
+local main = assert(io.open(srcdir .. "cluttex.lua", "r")):read("*a")
+if main:sub(1,2) == "#!" then
+ -- shebang
+ shebang,main = main:match("^([^\n]+\n)(.*)$")
+end
+
+local lines = {}
+if mode == "batchfile" then
+ lines[1] = [=[
+::dummy:: --[[
+@texlua "%~f0" %*
+@goto :eof
+]]
+]=]
+else
+ if shebang then
+ lines[1] = shebang
+ end
+end
+
+table.insert(lines, string.format("local %s = %s\n", table.concat(imported_globals, ", "), table.concat(imported_globals, ", ")))
+table.insert(lines, "local CLUTTEX_VERBOSITY, CLUTTEX_VERSION\n")
+
+if default_os then
+ table.insert(lines, string.format("os.type = os.type or %q\n", default_os))
+end
+
+-- LuajitTeX doesn't seem to set package.loaded table...
+table.insert(lines, "if lfs and not package.loaded['lfs'] then package.loaded['lfs'] = lfs end\n")
+
+for _,m in ipairs(modules) do
+ if m.path_windows or m.path_unix then
+ table.insert(lines, 'if os.type == "windows" then\n')
+ table.insert(lines, string.format("package.preload[%q] = function(...)\n%send\n", m.name, load_module_code(m.path_windows or m.path)))
+ table.insert(lines, 'else\n')
+ table.insert(lines, string.format("package.preload[%q] = function(...)\n%send\n", m.name, load_module_code(m.path_unix or m.path)))
+ table.insert(lines, 'end\n')
+ else
+ table.insert(lines, string.format("package.preload[%q] = function(...)\n%send\n", m.name, load_module_code(m.path)))
+ end
+end
+table.insert(lines, strip_global_imports(main))
+
+if outfile then
+ io.output(assert(io.open(outfile, "wb")))
+end
+io.write(table.concat(lines, ""))
diff --git a/Master/texmf-dist/doc/support/cluttex/checkglobal.lua b/Master/texmf-dist/doc/support/cluttex/checkglobal.lua
new file mode 100644
index 00000000000..4b86ef9f6c4
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/checkglobal.lua
@@ -0,0 +1,69 @@
+if #arg == 0 then
+ io.stderr:write[[
+Usage: lua checkglobal.lua <file.lua>
+]]
+ os.exit(1)
+end
+local f = assert(io.popen("luac -l -l " .. arg[1]))
+local known_global = {
+ -- Basic
+ _G = true,
+ _VERSION = true,
+ assert = true,
+ collectgarbage = true,
+ dofile = true,
+ error = true,
+ getmetatable = true,
+ ipairs = true,
+ load = true,
+ loadfile = true,
+ next = true,
+ pairs = true,
+ pcall = true,
+ print = true,
+ rawequal = true,
+ rawget = true,
+ rawlen = true,
+ rawset = true,
+ require = true,
+ select = true,
+ setmetatable = true,
+ tonumber = true,
+ tostring = true,
+ type = true,
+ xpcall = true,
+
+ -- Standard modules
+ bit32 = true, -- Lua 5.2
+ coroutine = true,
+ debug = true,
+ io = true,
+ math = true,
+ os = true,
+ package = true,
+ string = true,
+ table = true,
+ -- Lua 5.3 adds 'utf8' module
+
+ -- LuaJIT / LuaTeX extensions
+ bit = true,
+ lfs = true,
+
+ -- Others
+ arg = true, -- command line argument
+}
+local result = true
+for line in f:lines() do
+ local m = line:match("; _ENV \"(%w+)\"")
+ if m then
+ if not known_global[m] then
+ print("Unknown global variable: ", m)
+ result = false
+ end
+ end
+end
+if result then
+ os.exit(0)
+else
+ os.exit(1)
+end
diff --git a/Master/texmf-dist/doc/support/cluttex/example/README.md b/Master/texmf-dist/doc/support/cluttex/example/README.md
new file mode 100644
index 00000000000..c24ebc0ba95
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/example/README.md
@@ -0,0 +1,68 @@
+Example documents and How to process them
+=====
+
+* `simple`
+
+```sh
+$ cd simple
+$ cluttex -e pdflatex main.tex
+```
+
+* `simple-ja`
+
+```sh
+$ cd simple-ja
+$ cluttex -e platex main-platex.tex
+$ cluttex -e lualatex main-luatexja.tex
+```
+
+* `include`
+
+```sh
+$ cd include
+$ cluttex -e pdflatex main.tex
+```
+
+* `makeindex`
+
+```sh
+$ cd makeindex
+$ cluttex -e pdflatex --makeindex=makeindex main.tex
+```
+
+* `bibtex`
+
+```sh
+$ cd bibtex
+$ cluttex -e pdflatex --bibtex=bibtex main.tex
+```
+
+* `biblatex`
+
+```sh
+$ cd biblatex
+$ cluttex -e pdflatex --biber main.tex
+```
+
+* `empty`
+
+```sh
+$ cd empty
+$ cluttex -e pdflatex main.tex
+```
+
+Should print `[WARN] No pages of output.`
+
+* `minted`
+
+```sh
+$ cd minted
+$ cluttex -e pdflatex --shell-escape main.tex
+```
+
+* `epstopdf`
+
+```sh
+$ cd epstopdf
+$ cluttex -e pdflatex --change-directory main.tex
+```
diff --git a/Master/texmf-dist/doc/support/cluttex/example/biblatex/foo.bib b/Master/texmf-dist/doc/support/cluttex/example/biblatex/foo.bib
new file mode 100644
index 00000000000..5aaa58dacb2
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/example/biblatex/foo.bib
@@ -0,0 +1,14 @@
+@book{TeXbook,
+author = "Donald E. Knuth",
+title = "The {\TeX}book",
+publisher = "Addison-Wesley",
+address = "Reading, Massachusetts",
+year = 1984
+}
+@book{LaTeX-Lamport,
+author = "Leslie Lamport",
+title = "\LaTeX: A Document Preparation System",
+publisher = "Addison-Wesley",
+address = "Reading, Massachusetts",
+year = 1994
+} \ No newline at end of file
diff --git a/Master/texmf-dist/doc/support/cluttex/example/biblatex/main.tex b/Master/texmf-dist/doc/support/cluttex/example/biblatex/main.tex
new file mode 100644
index 00000000000..69ac565053c
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/example/biblatex/main.tex
@@ -0,0 +1,8 @@
+\documentclass{article}
+\usepackage[backend=biber]{biblatex}
+\addbibresource{foo.bib}
+\begin{document}
+foo\cite{TeXbook}
+\include{sub}
+\printbibliography
+\end{document}
diff --git a/Master/texmf-dist/doc/support/cluttex/example/biblatex/sub.tex b/Master/texmf-dist/doc/support/cluttex/example/biblatex/sub.tex
new file mode 100644
index 00000000000..02251b5c5cb
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/example/biblatex/sub.tex
@@ -0,0 +1 @@
+baz\cite{LaTeX-Lamport}
diff --git a/Master/texmf-dist/doc/support/cluttex/example/bibtex/baz.bib b/Master/texmf-dist/doc/support/cluttex/example/bibtex/baz.bib
new file mode 100644
index 00000000000..6c8a08e2cd9
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/example/bibtex/baz.bib
@@ -0,0 +1,7 @@
+@book{LaTeX-Lamport,
+author = "Leslie Lamport",
+title = "\LaTeX: A Document Preparation System",
+publisher = "Addison-Wesley",
+address = "Reading, Massachusetts",
+year = 1994
+}
diff --git a/Master/texmf-dist/doc/support/cluttex/example/bibtex/foo.bib b/Master/texmf-dist/doc/support/cluttex/example/bibtex/foo.bib
new file mode 100644
index 00000000000..2d9dbed71e6
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/example/bibtex/foo.bib
@@ -0,0 +1,7 @@
+@book{TeXbook,
+author = "Donald E. Knuth",
+title = "The {\TeX}book",
+publisher = "Addison-Wesley",
+address = "Reading, Massachusetts",
+year = 1984
+}
diff --git a/Master/texmf-dist/doc/support/cluttex/example/bibtex/main.tex b/Master/texmf-dist/doc/support/cluttex/example/bibtex/main.tex
new file mode 100644
index 00000000000..e5f288045f5
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/example/bibtex/main.tex
@@ -0,0 +1,7 @@
+\documentclass{article}
+\begin{document}
+foo\cite{TeXbook}
+\include{sub}
+\bibliographystyle{plain}
+\bibliography{foo,baz}
+\end{document}
diff --git a/Master/texmf-dist/doc/support/cluttex/example/bibtex/sub.tex b/Master/texmf-dist/doc/support/cluttex/example/bibtex/sub.tex
new file mode 100644
index 00000000000..02251b5c5cb
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/example/bibtex/sub.tex
@@ -0,0 +1 @@
+baz\cite{LaTeX-Lamport}
diff --git a/Master/texmf-dist/doc/support/cluttex/example/empty/main.tex b/Master/texmf-dist/doc/support/cluttex/example/empty/main.tex
new file mode 100644
index 00000000000..26047ba6ac0
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/example/empty/main.tex
@@ -0,0 +1,3 @@
+\documentclass{article}
+\begin{document}
+\end{document}
diff --git a/Master/texmf-dist/doc/support/cluttex/example/epstopdf/drawing.eps b/Master/texmf-dist/doc/support/cluttex/example/epstopdf/drawing.eps
new file mode 100644
index 00000000000..8c986c52fa3
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/example/epstopdf/drawing.eps
@@ -0,0 +1,99 @@
+%!PS-Adobe-3.0 EPSF-3.0
+%%Creator: cairo 1.14.8 (http://cairographics.org)
+%%CreationDate: Fri Dec 23 22:28:39 2016
+%%Pages: 1
+%%DocumentData: Clean7Bit
+%%LanguageLevel: 2
+%%BoundingBox: 0 -1 150 131
+%%EndComments
+%%BeginProlog
+save
+50 dict begin
+/q { gsave } bind def
+/Q { grestore } bind def
+/cm { 6 array astore concat } bind def
+/w { setlinewidth } bind def
+/J { setlinecap } bind def
+/j { setlinejoin } bind def
+/M { setmiterlimit } bind def
+/d { setdash } bind def
+/m { moveto } bind def
+/l { lineto } bind def
+/c { curveto } bind def
+/h { closepath } bind def
+/re { exch dup neg 3 1 roll 5 3 roll moveto 0 rlineto
+ 0 exch rlineto 0 rlineto closepath } bind def
+/S { stroke } bind def
+/f { fill } bind def
+/f* { eofill } bind def
+/n { newpath } bind def
+/W { clip } bind def
+/W* { eoclip } bind def
+/BT { } bind def
+/ET { } bind def
+/pdfmark where { pop globaldict /?pdfmark /exec load put }
+ { globaldict begin /?pdfmark /pop load def /pdfmark
+ /cleartomark load def end } ifelse
+/BDC { mark 3 1 roll /BDC pdfmark } bind def
+/EMC { mark /EMC pdfmark } bind def
+/cairo_store_point { /cairo_point_y exch def /cairo_point_x exch def } def
+/Tj { show currentpoint cairo_store_point } bind def
+/TJ {
+ {
+ dup
+ type /stringtype eq
+ { show } { -0.001 mul 0 cairo_font_matrix dtransform rmoveto } ifelse
+ } forall
+ currentpoint cairo_store_point
+} bind def
+/cairo_selectfont { cairo_font_matrix aload pop pop pop 0 0 6 array astore
+ cairo_font exch selectfont cairo_point_x cairo_point_y moveto } bind def
+/Tf { pop /cairo_font exch def /cairo_font_matrix where
+ { pop cairo_selectfont } if } bind def
+/Td { matrix translate cairo_font_matrix matrix concatmatrix dup
+ /cairo_font_matrix exch def dup 4 get exch 5 get cairo_store_point
+ /cairo_font where { pop cairo_selectfont } if } bind def
+/Tm { 2 copy 8 2 roll 6 array astore /cairo_font_matrix exch def
+ cairo_store_point /cairo_font where { pop cairo_selectfont } if } bind def
+/g { setgray } bind def
+/rg { setrgbcolor } bind def
+/d1 { setcachedevice } bind def
+%%EndProlog
+%%BeginSetup
+%%EndSetup
+%%Page: 1 1
+%%BeginPageSetup
+%%PageBoundingBox: 0 -1 150 131
+%%EndPageSetup
+q 0 -1 150 132 rectclip q
+0 g
+108.344 25.261 m 68.359 37.691 l 35.453 11.793 l 34.914 53.664 l 0.121
+76.957 l 39.773 90.406 l 51.172 130.695 l 76.219 97.14 l 118.059 98.75 l
+ 93.887 64.558 l h
+108.344 25.261 m f
+1 g
+0.24 w
+1 J
+1 j
+[] 0.0 d
+4 M q 1 0 0 -1 0 130.816025 cm
+108.344 105.555 m 68.359 93.125 l 35.453 119.023 l 34.914 77.152 l 0.121
+ 53.859 l 39.773 40.41 l 51.172 0.121 l 76.219 33.676 l 118.059 32.066 l
+ 93.887 66.258 l h
+108.344 105.555 m S Q
+1 0 0 rg
+149.488 29.836 m 149.488 13.425 136.184 0.121 119.773 0.121 c 103.363 0.121
+ 90.059 13.425 90.059 29.836 c 90.059 46.246 103.363 59.55 119.773 59.55
+ c 136.184 59.55 149.488 46.246 149.488 29.836 c h
+149.488 29.836 m f
+1 g
+q 1 0 0 -1 0 130.816025 cm
+149.488 100.98 m 149.488 117.391 136.184 130.695 119.773 130.695 c 103.363
+ 130.695 90.059 117.391 90.059 100.98 c 90.059 84.57 103.363 71.266 119.773
+ 71.266 c 136.184 71.266 149.488 84.57 149.488 100.98 c h
+149.488 100.98 m S Q
+Q Q
+showpage
+%%Trailer
+end restore
+%%EOF
diff --git a/Master/texmf-dist/doc/support/cluttex/example/epstopdf/fig/drawing2.eps b/Master/texmf-dist/doc/support/cluttex/example/epstopdf/fig/drawing2.eps
new file mode 100644
index 00000000000..8c986c52fa3
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/example/epstopdf/fig/drawing2.eps
@@ -0,0 +1,99 @@
+%!PS-Adobe-3.0 EPSF-3.0
+%%Creator: cairo 1.14.8 (http://cairographics.org)
+%%CreationDate: Fri Dec 23 22:28:39 2016
+%%Pages: 1
+%%DocumentData: Clean7Bit
+%%LanguageLevel: 2
+%%BoundingBox: 0 -1 150 131
+%%EndComments
+%%BeginProlog
+save
+50 dict begin
+/q { gsave } bind def
+/Q { grestore } bind def
+/cm { 6 array astore concat } bind def
+/w { setlinewidth } bind def
+/J { setlinecap } bind def
+/j { setlinejoin } bind def
+/M { setmiterlimit } bind def
+/d { setdash } bind def
+/m { moveto } bind def
+/l { lineto } bind def
+/c { curveto } bind def
+/h { closepath } bind def
+/re { exch dup neg 3 1 roll 5 3 roll moveto 0 rlineto
+ 0 exch rlineto 0 rlineto closepath } bind def
+/S { stroke } bind def
+/f { fill } bind def
+/f* { eofill } bind def
+/n { newpath } bind def
+/W { clip } bind def
+/W* { eoclip } bind def
+/BT { } bind def
+/ET { } bind def
+/pdfmark where { pop globaldict /?pdfmark /exec load put }
+ { globaldict begin /?pdfmark /pop load def /pdfmark
+ /cleartomark load def end } ifelse
+/BDC { mark 3 1 roll /BDC pdfmark } bind def
+/EMC { mark /EMC pdfmark } bind def
+/cairo_store_point { /cairo_point_y exch def /cairo_point_x exch def } def
+/Tj { show currentpoint cairo_store_point } bind def
+/TJ {
+ {
+ dup
+ type /stringtype eq
+ { show } { -0.001 mul 0 cairo_font_matrix dtransform rmoveto } ifelse
+ } forall
+ currentpoint cairo_store_point
+} bind def
+/cairo_selectfont { cairo_font_matrix aload pop pop pop 0 0 6 array astore
+ cairo_font exch selectfont cairo_point_x cairo_point_y moveto } bind def
+/Tf { pop /cairo_font exch def /cairo_font_matrix where
+ { pop cairo_selectfont } if } bind def
+/Td { matrix translate cairo_font_matrix matrix concatmatrix dup
+ /cairo_font_matrix exch def dup 4 get exch 5 get cairo_store_point
+ /cairo_font where { pop cairo_selectfont } if } bind def
+/Tm { 2 copy 8 2 roll 6 array astore /cairo_font_matrix exch def
+ cairo_store_point /cairo_font where { pop cairo_selectfont } if } bind def
+/g { setgray } bind def
+/rg { setrgbcolor } bind def
+/d1 { setcachedevice } bind def
+%%EndProlog
+%%BeginSetup
+%%EndSetup
+%%Page: 1 1
+%%BeginPageSetup
+%%PageBoundingBox: 0 -1 150 131
+%%EndPageSetup
+q 0 -1 150 132 rectclip q
+0 g
+108.344 25.261 m 68.359 37.691 l 35.453 11.793 l 34.914 53.664 l 0.121
+76.957 l 39.773 90.406 l 51.172 130.695 l 76.219 97.14 l 118.059 98.75 l
+ 93.887 64.558 l h
+108.344 25.261 m f
+1 g
+0.24 w
+1 J
+1 j
+[] 0.0 d
+4 M q 1 0 0 -1 0 130.816025 cm
+108.344 105.555 m 68.359 93.125 l 35.453 119.023 l 34.914 77.152 l 0.121
+ 53.859 l 39.773 40.41 l 51.172 0.121 l 76.219 33.676 l 118.059 32.066 l
+ 93.887 66.258 l h
+108.344 105.555 m S Q
+1 0 0 rg
+149.488 29.836 m 149.488 13.425 136.184 0.121 119.773 0.121 c 103.363 0.121
+ 90.059 13.425 90.059 29.836 c 90.059 46.246 103.363 59.55 119.773 59.55
+ c 136.184 59.55 149.488 46.246 149.488 29.836 c h
+149.488 29.836 m f
+1 g
+q 1 0 0 -1 0 130.816025 cm
+149.488 100.98 m 149.488 117.391 136.184 130.695 119.773 130.695 c 103.363
+ 130.695 90.059 117.391 90.059 100.98 c 90.059 84.57 103.363 71.266 119.773
+ 71.266 c 136.184 71.266 149.488 84.57 149.488 100.98 c h
+149.488 100.98 m S Q
+Q Q
+showpage
+%%Trailer
+end restore
+%%EOF
diff --git a/Master/texmf-dist/doc/support/cluttex/example/epstopdf/main.tex b/Master/texmf-dist/doc/support/cluttex/example/epstopdf/main.tex
new file mode 100644
index 00000000000..a0055359d59
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/example/epstopdf/main.tex
@@ -0,0 +1,9 @@
+\documentclass{article}
+\usepackage{graphicx}
+\usepackage{epstopdf}
+\begin{document}
+\includegraphics{drawing.eps}
+%\includegraphics{draw ing.eps}
+\graphicspath{{fig/}}
+\includegraphics{drawing2.eps}
+\end{document}
diff --git a/Master/texmf-dist/doc/support/cluttex/example/include/main.tex b/Master/texmf-dist/doc/support/cluttex/example/include/main.tex
new file mode 100644
index 00000000000..3e3f3f62abb
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/example/include/main.tex
@@ -0,0 +1,12 @@
+\documentclass{article}
+\begin{document}
+\tableofcontents
+
+Hello!
+\ref{first section}
+\ref{second section}
+\input{sub}
+\include{sub2}
+\include{path/to/file}
+
+\end{document}
diff --git a/Master/texmf-dist/doc/support/cluttex/example/include/path/to/file.tex b/Master/texmf-dist/doc/support/cluttex/example/include/path/to/file.tex
new file mode 100644
index 00000000000..324cbbb1654
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/example/include/path/to/file.tex
@@ -0,0 +1,3 @@
+\section{Third section}
+\label{third section}
+To be written...
diff --git a/Master/texmf-dist/doc/support/cluttex/example/include/sub.tex b/Master/texmf-dist/doc/support/cluttex/example/include/sub.tex
new file mode 100644
index 00000000000..ef7d3fe7cbd
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/example/include/sub.tex
@@ -0,0 +1,3 @@
+\section{First section}
+\label{first section}
+This is a sample document
diff --git a/Master/texmf-dist/doc/support/cluttex/example/include/sub2.tex b/Master/texmf-dist/doc/support/cluttex/example/include/sub2.tex
new file mode 100644
index 00000000000..1c69a279ccf
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/example/include/sub2.tex
@@ -0,0 +1,3 @@
+\section{Second section}
+\label{second section}
+...to test subfiles.
diff --git a/Master/texmf-dist/doc/support/cluttex/example/makeindex/main.tex b/Master/texmf-dist/doc/support/cluttex/example/makeindex/main.tex
new file mode 100644
index 00000000000..81889db6410
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/example/makeindex/main.tex
@@ -0,0 +1,15 @@
+\documentclass{article}
+\usepackage{makeidx}
+\usepackage{hyperref}
+\makeindex
+\begin{document}
+\section{Polynomial}
+Polynomial\index{polynomial}
+
+\section{Resultant}
+Resultant\index{resultant}
+
+%GCD\index{GCD}
+
+\printindex
+\end{document}
diff --git a/Master/texmf-dist/doc/support/cluttex/example/minted/file.lua b/Master/texmf-dist/doc/support/cluttex/example/minted/file.lua
new file mode 100644
index 00000000000..d452436fe5f
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/example/minted/file.lua
@@ -0,0 +1,3 @@
+function greet()
+ print("Goodbye world!")
+end
diff --git a/Master/texmf-dist/doc/support/cluttex/example/minted/main.tex b/Master/texmf-dist/doc/support/cluttex/example/minted/main.tex
new file mode 100644
index 00000000000..bbf231b24db
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/example/minted/main.tex
@@ -0,0 +1,11 @@
+\documentclass{article}
+\usepackage{minted}
+\begin{document}
+\begin{minted}{lua}
+function greet()
+ print("Hello world!")
+end
+\end{minted}
+
+\inputminted{lua}{file.lua}
+\end{document}
diff --git a/Master/texmf-dist/doc/support/cluttex/example/simple-ja/main-luatexja.tex b/Master/texmf-dist/doc/support/cluttex/example/simple-ja/main-luatexja.tex
new file mode 100644
index 00000000000..30f13b58db4
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/example/simple-ja/main-luatexja.tex
@@ -0,0 +1,6 @@
+\documentclass{ltjsarticle}
+\usepackage{luatexja-ruby}
+\begin{document}
+Hello world!
+こんにちは、\ruby{世界}{せかい}。さようなら、\ruby{進捗}{しんちょく}。
+\end{document}
diff --git a/Master/texmf-dist/doc/support/cluttex/example/simple-ja/main-platex.tex b/Master/texmf-dist/doc/support/cluttex/example/simple-ja/main-platex.tex
new file mode 100644
index 00000000000..79044aec704
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/example/simple-ja/main-platex.tex
@@ -0,0 +1,5 @@
+\documentclass{jsarticle}
+\begin{document}
+Hello world!
+こんにちは、世界。さようなら、進捗。
+\end{document}
diff --git a/Master/texmf-dist/doc/support/cluttex/example/simple/main.tex b/Master/texmf-dist/doc/support/cluttex/example/simple/main.tex
new file mode 100644
index 00000000000..6b879104a23
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/example/simple/main.tex
@@ -0,0 +1,4 @@
+\documentclass{article}
+\begin{document}
+Hello world!
+\end{document}
diff --git a/Master/texmf-dist/doc/support/cluttex/src/cluttex.lua b/Master/texmf-dist/doc/support/cluttex/src/cluttex.lua
new file mode 100644
index 00000000000..3223cbad55e
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/src/cluttex.lua
@@ -0,0 +1,517 @@
+#!/usr/bin/env texlua
+--[[
+ Copyright 2016,2018 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+CLUTTEX_VERSION = "v0.1"
+
+-- Standard libraries
+local table = table
+local os = os
+local io = io
+local string = string
+local ipairs = ipairs
+local coroutine = coroutine
+local tostring = tostring
+
+-- External libraries (included in texlua)
+local filesys = require "lfs"
+local md5 = require "md5"
+-- local kpse = require "kpse"
+
+-- My own modules
+local pathutil = require "texrunner.pathutil"
+local fsutil = require "texrunner.fsutil"
+local shellutil = require "texrunner.shellutil"
+local reruncheck = require "texrunner.reruncheck"
+local luatexinit = require "texrunner.luatexinit"
+local recoverylib = require "texrunner.recovery"
+local message = require "texrunner.message"
+local extract_bibtex_from_aux_file = require "texrunner.auxfile".extract_bibtex_from_aux_file
+local handle_cluttex_options = require "texrunner.handleoption".handle_cluttex_options
+
+-- arguments: input file name, jobname, etc...
+local function genOutputDirectory(...)
+ -- The name of the temporary directory is based on the path of input file.
+ local message = table.concat({...}, "\0")
+ local hash = md5.sumhexa(message)
+ local tmpdir = os.getenv("TMPDIR") or os.getenv("TMP") or os.getenv("TEMP")
+ if tmpdir == nil then
+ local home = os.getenv("HOME") or os.getenv("USERPROFILE") or error("environment variable 'TMPDIR' not set!")
+ tmpdir = pathutil.join(home, ".latex-build-temp")
+ end
+ return pathutil.join(tmpdir, 'latex-build-' .. hash)
+end
+
+local inputfile, engine, options = handle_cluttex_options(arg)
+
+local jobname = options.jobname or pathutil.basename(pathutil.trimext(inputfile))
+assert(jobname ~= "", "jobname cannot be empty")
+
+if options.output_format == nil then
+ options.output_format = "pdf"
+end
+local output_extension
+if options.output_format == "dvi" then
+ output_extension = engine.dvi_extension or "dvi"
+else
+ output_extension = "pdf"
+end
+
+if options.output == nil then
+ options.output = jobname .. "." .. output_extension
+end
+
+-- Prepare output directory
+if options.output_directory == nil then
+ local inputfile_abs = pathutil.abspath(inputfile)
+ options.output_directory = genOutputDirectory(inputfile_abs, jobname, options.engine)
+
+ if not fsutil.isdir(options.output_directory) then
+ assert(fsutil.mkdir_rec(options.output_directory))
+
+ elseif options.fresh then
+ -- The output directory exists and --fresh is given:
+ -- Remove all files in the output directory
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("Cleaning '", options.output_directory, "'...")
+ end
+ assert(fsutil.remove_rec(options.output_directory))
+ assert(filesys.mkdir(options.output_directory))
+ end
+
+elseif options.fresh then
+ message.error("--fresh and --output-directory cannot be used together.")
+ os.exit(1)
+end
+
+local pathsep = ":"
+if os.type == "windows" then
+ pathsep = ";"
+end
+
+local original_wd = filesys.currentdir()
+if options.change_directory then
+ local TEXINPUTS = os.getenv("TEXINPUTS") or ""
+ filesys.chdir(options.output_directory)
+ options.output = pathutil.abspath(options.output, original_wd)
+ os.setenv("TEXINPUTS", original_wd .. pathsep .. TEXINPUTS)
+end
+if options.bibtex or options.biber then
+ local BIBINPUTS = os.getenv("BIBINPUTS") or ""
+ options.output = pathutil.abspath(options.output, original_wd)
+ os.setenv("BIBINPUTS", original_wd .. pathsep .. BIBINPUTS)
+end
+
+-- Set `max_print_line' environment variable if not already set.
+if os.getenv("max_print_line") == nil then
+ os.setenv("max_print_line", "65536")
+end
+-- TODO: error_line, half_error_line
+--[[
+ According to texmf.cnf:
+ 45 < error_line < 255,
+ 30 < half_error_line < error_line - 15,
+ 60 <= max_print_line.
+]]
+
+local function path_in_output_directory(ext)
+ return pathutil.join(options.output_directory, jobname .. "." .. ext)
+end
+
+local recorderfile = path_in_output_directory("fls")
+local recorderfile2 = path_in_output_directory("cluttex-fls")
+
+local tex_options = {
+ interaction = options.interaction,
+ file_line_error = options.file_line_error,
+ halt_on_error = options.halt_on_error,
+ synctex = options.synctex,
+ output_directory = options.output_directory,
+ shell_escape = options.shell_escape,
+ shell_restricted = options.shell_restricted,
+ jobname = options.jobname,
+ fmt = options.fmt,
+ extraoptions = options.tex_extraoptions,
+}
+if options.output_format ~= "pdf" and engine.supports_pdf_generation then
+ tex_options.output_format = options.output_format
+end
+
+-- Setup LuaTeX initialization script
+if engine.is_luatex then
+ local initscriptfile = path_in_output_directory("cluttexinit.lua")
+ luatexinit.create_initialization_script(initscriptfile, tex_options)
+ tex_options.lua_initialization_script = initscriptfile
+end
+
+-- Run TeX command (*tex, *latex)
+-- should_rerun, newauxstatus = single_run([auxstatus])
+-- This function should be run in a coroutine.
+local function single_run(auxstatus, iteration)
+ local minted = false
+ local bibtex_aux_hash = nil
+ local mainauxfile = path_in_output_directory("aux")
+ if fsutil.isfile(recorderfile) then
+ -- Recorder file already exists
+ local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options)
+ if engine.is_luatex and fsutil.isfile(recorderfile2) then
+ filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap)
+ end
+ auxstatus = reruncheck.collectfileinfo(filelist, auxstatus)
+ for _,fileinfo in ipairs(filelist) do
+ if string.match(fileinfo.path, "minted/minted%.sty$") then
+ minted = true
+ break
+ end
+ end
+ if options.bibtex then
+ local biblines = extract_bibtex_from_aux_file(mainauxfile, options.output_directory)
+ if #biblines > 0 then
+ bibtex_aux_hash = md5.sum(table.concat(biblines, "\n"))
+ end
+ end
+ else
+ -- This is the first execution
+ if auxstatus ~= nil then
+ message.error("Recorder file was not generated during the execution!")
+ os.exit(1)
+ end
+ auxstatus = {}
+ end
+ --local timestamp = os.time()
+
+ if options.includeonly then
+ tex_options.tex_injection = string.format("%s\\includeonly{%s}", tex_options.tex_injection or "", options.includeonly)
+ end
+
+ if minted and not (tex_options.tex_injection and string.find(tex_options.tex_injection,"minted") == nil) then
+ tex_options.tex_injection = string.format("%s\\PassOptionsToPackage{outputdir=%s}{minted}", tex_options.tex_injection or "", options.output_directory)
+ end
+
+ local current_tex_options, lightweight_mode = tex_options, false
+ if iteration == 1 and options.start_with_draft then
+ current_tex_options = {}
+ for k,v in pairs(tex_options) do
+ current_tex_options[k] = v
+ end
+ if engine.supports_draftmode then
+ current_tex_options.draftmode = true
+ options.start_with_draft = false
+ end
+ current_tex_options.interaction = "batchmode"
+ lightweight_mode = true
+ else
+ current_tex_options.draftmode = false
+ end
+
+ local command = engine:build_command(inputfile, current_tex_options)
+
+ local execlog -- the contents of .log file
+
+ local recovered = false
+ local function recover()
+ -- Check log file
+ if not execlog then
+ local logfile = assert(io.open(path_in_output_directory("log")))
+ execlog = logfile:read("*a")
+ logfile:close()
+ end
+ recovered = recoverylib.try_recovery{
+ execlog = execlog,
+ auxfile = path_in_output_directory("aux"),
+ options = options,
+ original_wd = original_wd,
+ }
+ return recovered
+ end
+ coroutine.yield(command, recover) -- Execute the command
+ if recovered then
+ return true, {}
+ end
+
+ local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options)
+ if engine.is_luatex and fsutil.isfile(recorderfile2) then
+ filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap)
+ end
+
+ if not execlog then
+ local logfile = assert(io.open(path_in_output_directory("log")))
+ execlog = logfile:read("*a")
+ logfile:close()
+ end
+
+ if options.makeindex then
+ -- Look for .idx files and run MakeIndex
+ for _,file in ipairs(filelist) do
+ if pathutil.ext(file.path) == "idx" then
+ -- Run makeindex if the .idx file is new or updated
+ local idxfileinfo = {path = file.path, abspath = file.abspath, kind = "auxiliary"}
+ local output_ind = pathutil.replaceext(file.abspath, "ind")
+ if reruncheck.comparefileinfo({idxfileinfo}, auxstatus) or reruncheck.comparefiletime(file.abspath, output_ind, auxstatus) then
+ local idx_dir = pathutil.dirname(file.abspath)
+ local makeindex_command = {
+ "cd", shellutil.escape(idx_dir), "&&",
+ options.makeindex, -- Do not escape options.makeindex to allow additional options
+ "-o", pathutil.basename(output_ind),
+ pathutil.basename(file.abspath)
+ }
+ coroutine.yield(table.concat(makeindex_command, " "))
+ table.insert(filelist, {path = output_ind, abspath = output_ind, kind = "auxiliary"})
+ else
+ local succ, err = filesys.touch(output_ind)
+ if not succ then
+ message.warn("Failed to touch " .. output_ind .. " (" .. err .. ")")
+ end
+ end
+ end
+ end
+ else
+ -- Check log file
+ if string.find(execlog, "No file [^\n]+%.ind%.") then
+ message.diag("You may want to use --makeindex option.")
+ end
+ end
+
+ if options.makeglossaries then
+ -- Look for .glo files and run makeglossaries
+ for _,file in ipairs(filelist) do
+ if pathutil.ext(file.path) == "glo" then
+ -- Run makeglossaries if the .glo file is new or updated
+ local glofileinfo = {path = file.path, abspath = file.abspath, kind = "auxiliary"}
+ local output_gls = pathutil.replaceext(file.abspath, "gls")
+ if reruncheck.comparefileinfo({glofileinfo}, auxstatus) or reruncheck.comparefiletime(file.abspath, output_gls, auxstatus) then
+ local makeglossaries_command = {
+ options.makeglossaries,
+ "-d", shellutil.escape(options.output_directory),
+ pathutil.trimext(pathutil.basename(file.path))
+ }
+ coroutine.yield(table.concat(makeglossaries_command, " "))
+ table.insert(filelist, {path = output_gls, abspath = output_gls, kind = "auxiliary"})
+ else
+ local succ, err = filesys.touch(output_gls)
+ if not succ then
+ message.warn("Failed to touch " .. output_ind .. " (" .. err .. ")")
+ end
+ end
+ end
+ end
+ else
+ -- Check log file
+ if string.find(execlog, "No file [^\n]+%.gls%.") then
+ message.diag("You may want to use --makeglossaries option.")
+ end
+ end
+
+ if options.bibtex then
+ local biblines2 = extract_bibtex_from_aux_file(mainauxfile, options.output_directory)
+ local bibtex_aux_hash2
+ if #biblines2 > 0 then
+ bibtex_aux_hash2 = md5.sum(table.concat(biblines2, "\n"))
+ end
+ local output_bbl = path_in_output_directory("bbl")
+ if bibtex_aux_hash ~= bibtex_aux_hash2 or reruncheck.comparefiletime(mainauxfile, output_bbl, auxstatus) then
+ -- The input for BibTeX command has changed...
+ local bibtex_command = {
+ "cd", shellutil.escape(options.output_directory), "&&",
+ options.bibtex,
+ pathutil.basename(mainauxfile)
+ }
+ coroutine.yield(table.concat(bibtex_command, " "))
+ else
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("No need to run BibTeX.")
+ end
+ local succ, err = filesys.touch(output_bbl)
+ if not succ then
+ message.warn("Failed to touch " .. output_bbl .. " (" .. err .. ")")
+ end
+ end
+ elseif options.biber then
+ for _,file in ipairs(filelist) do
+ if pathutil.ext(file.path) == "bcf" then
+ -- Run biber if the .bcf file is new or updated
+ local bcffileinfo = {path = file.path, abspath = file.abspath, kind = "auxiliary"}
+ local output_bbl = pathutil.replaceext(file.abspath, "bbl")
+ if reruncheck.comparefileinfo({bcffileinfo}, auxstatus) or reruncheck.comparefiletime(file.abspath, output_bbl, auxstatus) then
+ local bbl_dir = pathutil.dirname(file.abspath)
+ local biber_command = {
+ options.biber, -- Do not escape options.biber to allow additional options
+ "--output-directory", shellutil.escape(options.output_directory),
+ pathutil.basename(file.abspath)
+ }
+ coroutine.yield(table.concat(biber_command, " "))
+ table.insert(filelist, {path = output_bbl, abspath = output_bbl, kind = "auxiliary"})
+ else
+ local succ, err = filesys.touch(output_bbl)
+ if not succ then
+ message.warn("Failed to touch " .. output_bbl .. " (" .. err .. ")")
+ end
+ end
+ end
+ end
+ else
+ -- Check log file
+ if string.find(execlog, "No file [^\n]+%.bbl%.") then
+ message.diag("You may want to use --bibtex or --biber option.")
+ end
+ end
+
+ if string.find(execlog, "No pages of output.") then
+ return "No pages of output."
+ end
+
+ local should_rerun, auxstatus = reruncheck.comparefileinfo(filelist, auxstatus)
+ return should_rerun or lightweight_mode, auxstatus
+end
+
+-- Run (La)TeX (possibly multiple times) and produce a PDF file.
+-- This function should be run in a coroutine.
+local function do_typeset_c()
+ local iteration = 0
+ local should_rerun, auxstatus
+ repeat
+ iteration = iteration + 1
+ should_rerun, auxstatus = single_run(auxstatus, iteration)
+ if should_rerun == "No pages of output." then
+ message.warn("No pages of output.")
+ return
+ end
+ until not should_rerun or iteration >= options.max_iterations
+
+ if should_rerun then
+ message.warn("LaTeX should be run once more.")
+ end
+
+ -- Successful
+ if options.output_format == "dvi" or engine.supports_pdf_generation then
+ -- Output file (DVI/PDF) is generated in the output directory
+ local outfile = path_in_output_directory(output_extension)
+ local oncopyerror
+ if os.type == "windows" then
+ oncopyerror = function()
+ message.error("Failed to copy file. Some applications may be locking the ", string.upper(options.output_format), " file.")
+ return false
+ end
+ end
+ coroutine.yield(fsutil.copy_command(outfile, options.output), oncopyerror)
+ if #options.dvipdfmx_extraoptions > 0 then
+ message.warn("--dvipdfmx-option[s] are ignored.")
+ end
+
+ else
+ -- DVI file is generated, but PDF file is wanted
+ local dvifile = path_in_output_directory("dvi")
+ local dvipdfmx_command = {"dvipdfmx", "-o", shellutil.escape(options.output)}
+ for _,v in ipairs(options.dvipdfmx_extraoptions) do
+ table.insert(dvipdfmx_command, v)
+ end
+ table.insert(dvipdfmx_command, shellutil.escape(dvifile))
+ coroutine.yield(table.concat(dvipdfmx_command, " "))
+ end
+
+ -- Copy SyncTeX file if necessary
+ if options.output_format == "pdf" then
+ local synctex = tonumber(options.synctex or "0")
+ local synctex_ext = nil
+ if synctex > 0 then
+ -- Compressed SyncTeX file (.synctex.gz)
+ synctex_ext = "synctex.gz"
+ elseif synctex < 0 then
+ -- Uncompressed SyncTeX file (.synctex)
+ synctex_ext = "synctex"
+ end
+ if synctex_ext then
+ coroutine.yield(fsutil.copy_command(path_in_output_directory(synctex_ext), pathutil.replaceext(options.output, synctex_ext)))
+ end
+ end
+end
+
+local function do_typeset()
+ -- Execute the command string yielded by do_typeset_c
+ for command, recover in coroutine.wrap(do_typeset_c) do
+ message.exec(command)
+ local success, termination, status_or_signal = os.execute(command)
+ if type(success) == "number" then -- Lua 5.1 or LuaTeX
+ local code = success
+ success = code == 0
+ termination = nil
+ status_or_signal = code
+ end
+ if not success and not (recover and recover()) then
+ if termination == "exit" then
+ message.error("Command exited abnormally: exit status ", tostring(status_or_signal))
+ elseif termination == "signal" then
+ message.error("Command exited abnormally: signal ", tostring(status_or_signal))
+ else
+ message.error("Command exited abnormally: ", tostring(status_or_signal))
+ end
+ return false, termination, status_or_signal
+ end
+ end
+ -- Successful
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("Command exited successfully")
+ end
+ return true
+end
+
+if options.watch then
+ -- Watch mode
+ local success, status = do_typeset()
+ local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options)
+ if engine.is_luatex and fsutil.isfile(recorderfile2) then
+ filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap)
+ end
+ local input_files_to_watch = {}
+ for _,fileinfo in ipairs(filelist) do
+ if fileinfo.kind == "input" then
+ table.insert(input_files_to_watch, fileinfo.abspath)
+ end
+ end
+ local fswatch_command = {"fswatch", "--event=Updated", "--"}
+ for _,path in ipairs(input_files_to_watch) do
+ table.insert(fswatch_command, shellutil.escape(path))
+ end
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.exec(table.concat(fswatch_command, " "))
+ end
+ local fswatch = assert(io.popen(table.concat(fswatch_command, " "), "r"))
+ for l in fswatch:lines() do
+ local found = false
+ for _,path in ipairs(input_files_to_watch) do
+ if l == path then
+ found = true
+ break
+ end
+ end
+ if found then
+ local success, status = do_typeset()
+ if not success then
+ -- Not successful
+ end
+ end
+ end
+
+else
+ -- Not in watch mode
+ local success, status = do_typeset()
+ if not success then
+ os.exit(1)
+ end
+end
diff --git a/Master/texmf-dist/doc/support/cluttex/src/texrunner/auxfile.lua b/Master/texmf-dist/doc/support/cluttex/src/texrunner/auxfile.lua
new file mode 100644
index 00000000000..0c69eefd136
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/src/texrunner/auxfile.lua
@@ -0,0 +1,71 @@
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local string_match = string.match
+local pathutil = require "texrunner.pathutil"
+local filesys = require "lfs"
+local fsutil = require "texrunner.fsutil"
+local message = require "texrunner.message"
+
+-- for LaTeX
+local function parse_aux_file(auxfile, outdir, report, seen)
+ report = report or {}
+ seen = seen or {}
+ seen[auxfile] = true
+ for l in io.lines(auxfile) do
+ local subauxfile = string_match(l, "\\@input{(.+)}")
+ if subauxfile then
+ if fsutil.isfile(subauxfile) then
+ parse_aux_file(pathutil.join(outdir, subauxfile), outdir, report, seen)
+ else
+ local dir = pathutil.join(outdir, pathutil.dirname(subauxfile))
+ if not fsutil.isdir(dir) then
+ assert(fsutil.mkdir_rec(dir))
+ report.made_new_directory = true
+ end
+ end
+ end
+ end
+ return report
+end
+
+-- \citation, \bibdata, \bibstyle and \@input
+local function extract_bibtex_from_aux_file(auxfile, outdir, biblines)
+ biblines = biblines or {}
+ for l in io.lines(auxfile) do
+ local name = string_match(l, "\\([%a@]+)")
+ if name == "citation" or name == "bibdata" or name == "bibstyle" then
+ table.insert(biblines, l)
+ if CLUTTEX_VERBOSITY >= 2 then
+ message.info("BibTeX line: ", l)
+ end
+ elseif name == "@input" then
+ local subauxfile = string_match(l, "\\@input{(.+)}")
+ if subauxfile and fsutil.isfile(subauxfile) then
+ extract_bibtex_from_aux_file(pathutil.join(outdir, subauxfile), outdir, biblines)
+ end
+ end
+ end
+ return biblines
+end
+
+return {
+ parse_aux_file = parse_aux_file,
+ extract_bibtex_from_aux_file = extract_bibtex_from_aux_file,
+}
diff --git a/Master/texmf-dist/doc/support/cluttex/src/texrunner/fsutil.lua b/Master/texmf-dist/doc/support/cluttex/src/texrunner/fsutil.lua
new file mode 100644
index 00000000000..335ad6f14ff
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/src/texrunner/fsutil.lua
@@ -0,0 +1,83 @@
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local assert = assert
+local os = os
+local os_execute = os.execute
+local os_remove = os.remove
+local filesys = require "lfs"
+local pathutil = require "texrunner.pathutil"
+local shellutil = require "texrunner.shellutil"
+local escape = shellutil.escape
+
+local copy_command
+if os.type == "windows" then
+ function copy_command(from, to)
+ -- TODO: What if `from` begins with a slash?
+ return "copy " .. escape(from) .. " " .. escape(to) .. " > NUL"
+ end
+else
+ function copy_command(from, to)
+ -- TODO: What if `from` begins with a hypen?
+ return "cp " .. escape(from) .. " " .. escape(to)
+ end
+end
+
+local isfile = filesys.isfile or function(path)
+ return filesys.attributes(path, "mode") == "file"
+end
+
+local isdir = filesys.isdir or function(path)
+ return filesys.attributes(path, "mode") == "directory"
+end
+
+local function mkdir_rec(path)
+ local succ, err = filesys.mkdir(path)
+ if not succ then
+ succ, err = mkdir_rec(pathutil.parentdir(path))
+ if succ then
+ return filesys.mkdir(path)
+ end
+ end
+ return succ, err
+end
+
+local function remove_rec(path)
+ if isdir(path) then
+ for file in filesys.dir(path) do
+ if file ~= "." and file ~= ".." then
+ local succ, err = remove_rec(pathutil.join(path, file))
+ if not succ then
+ return succ, err
+ end
+ end
+ end
+ return filesys.rmdir(path)
+ else
+ return os_remove(path)
+ end
+end
+
+return {
+ copy_command = copy_command,
+ isfile = isfile,
+ isdir = isdir,
+ mkdir_rec = mkdir_rec,
+ remove_rec = remove_rec,
+}
diff --git a/Master/texmf-dist/doc/support/cluttex/src/texrunner/handleoption.lua b/Master/texmf-dist/doc/support/cluttex/src/texrunner/handleoption.lua
new file mode 100644
index 00000000000..7ea478c24b9
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/src/texrunner/handleoption.lua
@@ -0,0 +1,416 @@
+local COPYRIGHT_NOTICE = [[
+Copyright (C) 2016,2018 ARATA Mizuki
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local pathutil = require "texrunner.pathutil"
+local shellutil = require "texrunner.shellutil"
+local parseoption = require "texrunner.option".parseoption
+local KnownEngines = require "texrunner.tex_engine"
+local message = require "texrunner.message"
+
+local function usage(arg)
+ io.write(string.format([[
+ClutTeX: Process TeX files without cluttering your working directory
+
+Usage:
+ %s [options] [--] FILE.tex
+
+Options:
+ -e, --engine=ENGINE Specify which TeX engine to use.
+ ENGINE is one of the following:
+ pdflatex, pdftex,
+ lualatex, luatex, luajittex,
+ xelatex, xetex, latex, etex, tex,
+ platex, eptex, ptex,
+ uplatex, euptex, uptex,
+ -o, --output=FILE The name of output file.
+ [default: JOBNAME.pdf or JOBNAME.dvi]
+ --fresh Clean intermediate files before running TeX.
+ Cannot be used with --output-directory.
+ --max-iterations=N Maximum number of running TeX to resolve
+ cross-references. [default: 3]
+ --start-with-draft Start with draft mode.
+ --[no-]change-directory Change directory before running TeX.
+ --watch Watch input files for change. Requires fswatch
+ program to be installed.
+ --tex-option=OPTION Pass OPTION to TeX as a single option.
+ --tex-options=OPTIONs Pass OPTIONs to TeX as multiple options.
+ --dvipdfmx-option[s]=OPTION[s] Same for dvipdfmx.
+ --makeindex=COMMAND+OPTIONs Command to generate index, such as
+ `makeindex' or `mendex'.
+ --bibtex=COMMAND+OPTIONs Command for BibTeX, such as
+ `bibtex' or `pbibtex'.
+ --biber[=COMMAND+OPTIONs] Command for Biber.
+ --makeglossaries[=COMMAND+OPTIONs] Command for makeglossaries.
+ -h, --help Print this message and exit.
+ -v, --version Print version information and exit.
+ -V, --verbose Be more verbose.
+ --color=WHEN Make ClutTeX's message colorful. WHEN is one of
+ `always', `auto', or `never'. [default: auto]
+ --includeonly=NAMEs Insert '\includeonly{NAMEs}'.
+
+ --[no-]shell-escape
+ --shell-restricted
+ --synctex=NUMBER
+ --fmt=FMTNAME
+ --[no-]file-line-error [default: yes]
+ --[no-]halt-on-error [default: yes]
+ --interaction=STRING [default: nonstopmode]
+ --jobname=STRING
+ --output-directory=DIR [default: somewhere in the temporary directory]
+ --output-format=FORMAT FORMAT is `pdf' or `dvi'. [default: pdf]
+
+%s
+]], arg[0] or 'texlua cluttex.lua', COPYRIGHT_NOTICE))
+end
+
+local option_spec = {
+ -- Options for ClutTeX
+ {
+ short = "e",
+ long = "engine",
+ param = true,
+ },
+ {
+ short = "o",
+ long = "output",
+ param = true,
+ },
+ {
+ long = "fresh",
+ },
+ {
+ long = "max-iterations",
+ param = true,
+ },
+ {
+ long = "start-with-draft",
+ },
+ {
+ long = "change-directory",
+ boolean = true,
+ },
+ {
+ long = "watch",
+ },
+ {
+ short = "h",
+ long = "help",
+ allow_single_hyphen = true,
+ },
+ {
+ short = "v",
+ long = "version",
+ },
+ {
+ short = "V",
+ long = "verbose",
+ },
+ {
+ long = "color",
+ param = true,
+ default = "always",
+ },
+ {
+ long = "includeonly",
+ param = true,
+ },
+ -- Options for TeX
+ {
+ long = "synctex",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "file-line-error",
+ boolean = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "interaction",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "halt-on-error",
+ boolean = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "shell-escape",
+ boolean = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "shell-restricted",
+ allow_single_hyphen = true,
+ },
+ {
+ long = "jobname",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "fmt",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "output-directory",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "output-format",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "tex-option",
+ param = true,
+ },
+ {
+ long = "tex-options",
+ param = true,
+ },
+ {
+ long = "dvipdfmx-option",
+ param = true,
+ },
+ {
+ long = "dvipdfmx-options",
+ param = true,
+ },
+ {
+ long = "makeindex",
+ param = true,
+ },
+ {
+ long = "bibtex",
+ param = true,
+ },
+ {
+ long = "biber",
+ param = true,
+ default = "biber",
+ },
+ {
+ long = "makeglossaries",
+ param = true,
+ default = "makeglossaries",
+ },
+}
+
+-- Default values for options
+local function set_default_values(options)
+ if options.max_iterations == nil then
+ options.max_iterations = 3
+ end
+
+ if options.interaction == nil then
+ options.interaction = "nonstopmode"
+ end
+
+ if options.file_line_error == nil then
+ options.file_line_error = true
+ end
+
+ if options.halt_on_error == nil then
+ options.halt_on_error = true
+ end
+end
+
+-- inputfile, engine, options = handle_cluttex_options(arg)
+local function handle_cluttex_options(arg)
+ -- Parse options
+ local option_and_params, non_option_index = parseoption(arg, option_spec)
+
+ -- Handle options
+ local options = {
+ tex_extraoptions = {},
+ dvipdfmx_extraoptions = {},
+ }
+ CLUTTEX_VERBOSITY = 0
+ for _,option in ipairs(option_and_params) do
+ local name = option[1]
+ local param = option[2]
+
+ if name == "engine" then
+ assert(options.engine == nil, "multiple --engine options")
+ options.engine = param
+
+ elseif name == "output" then
+ assert(options.output == nil, "multiple --output options")
+ options.output = param
+
+ elseif name == "fresh" then
+ assert(options.fresh == nil, "multiple --fresh options")
+ options.fresh = true
+
+ elseif name == "max-iterations" then
+ assert(options.max_iterations == nil, "multiple --max-iterations options")
+ options.max_iterations = assert(tonumber(param), "invalid value for --max-iterations option")
+ assert(options.max_iterations >= 1, "invalid value for --max-iterations option")
+
+ elseif name == "start-with-draft" then
+ assert(options.start_with_draft == nil, "multiple --start-with-draft options")
+ options.start_with_draft = true
+
+ elseif name == "watch" then
+ assert(options.watch == nil, "multiple --watch options")
+ options.watch = true
+
+ elseif name == "help" then
+ usage(arg)
+ os.exit(0)
+
+ elseif name == "version" then
+ io.stderr:write("cluttex ",CLUTTEX_VERSION,"\n")
+ os.exit(0)
+
+ elseif name == "verbose" then
+ CLUTTEX_VERBOSITY = CLUTTEX_VERBOSITY + 1
+
+ elseif name == "color" then
+ assert(options.color == nil, "multiple --collor options")
+ options.color = param
+ message.set_colors(options.color)
+
+ elseif name == "change-directory" then
+ assert(options.change_directory == nil, "multiple --change-directory options")
+ options.change_directory = param
+
+ elseif name == "includeonly" then
+ assert(options.includeonly == nil, "multiple --includeonly options")
+ options.includeonly = param
+
+ -- Options for TeX
+ elseif name == "synctex" then
+ assert(options.synctex == nil, "multiple --synctex options")
+ options.synctex = param
+
+ elseif name == "file-line-error" then
+ options.file_line_error = param
+
+ elseif name == "interaction" then
+ assert(options.interaction == nil, "multiple --interaction options")
+ assert(param == "batchmode" or param == "nonstopmode" or param == "scrollmode" or param == "errorstopmode", "invalid argument for --interaction")
+ options.interaction = param
+
+ elseif name == "halt-on-error" then
+ options.halt_on_error = param
+
+ elseif name == "shell-escape" then
+ assert(options.shell_escape == nil and options.shell_restricted == nil, "multiple --(no-)shell-escape or --shell-restricted options")
+ options.shell_escape = param
+
+ elseif name == "shell-restricted" then
+ assert(options.shell_escape == nil and options.shell_restricted == nil, "multiple --(no-)shell-escape or --shell-restricted options")
+ options.shell_restricted = true
+
+ elseif name == "jobname" then
+ assert(options.jobname == nil, "multiple --jobname options")
+ options.jobname = param
+
+ elseif name == "fmt" then
+ assert(options.fmt == nil, "multiple --fmt options")
+ options.fmt = param
+
+ elseif name == "output-directory" then
+ assert(options.output_directory == nil, "multiple --output-directory options")
+ options.output_directory = param
+
+ elseif name == "output-format" then
+ assert(options.output_format == nil, "multiple --output-format options")
+ assert(param == "pdf" or param == "dvi", "invalid argument for --output-format")
+ options.output_format = param
+
+ elseif name == "tex-option" then
+ table.insert(options.tex_extraoptions, shellutil.escape(param))
+
+ elseif name == "tex-options" then
+ table.insert(options.tex_extraoptions, param)
+
+ elseif name == "dvipdfmx-option" then
+ table.insert(options.dvipdfmx_extraoptions, shellutil.escape(param))
+
+ elseif name == "dvipdfmx-options" then
+ table.insert(options.dvipdfmx_extraoptions, param)
+
+ elseif name == "makeindex" then
+ assert(options.makeindex == nil, "multiple --makeindex options")
+ options.makeindex = param
+
+ elseif name == "bibtex" then
+ assert(options.bibtex == nil, "multiple --bibtex options")
+ assert(options.biber == nil, "multiple --bibtex/--biber options")
+ options.bibtex = param
+
+ elseif name == "biber" then
+ assert(options.biber == nil, "multiple --biber options")
+ assert(options.bibtex == nil, "multiple --bibtex/--biber options")
+ options.biber = param
+
+ elseif name == "makeglossaries" then
+ assert(options.makeglossaries == nil, "multiple --makeglossaries options")
+ options.makeglossaries = param
+
+ end
+ end
+
+ if options.color == nil then
+ message.set_colors("auto")
+ end
+
+ -- Handle non-options (i.e. input file)
+ if non_option_index > #arg then
+ -- No input file given
+ usage(arg)
+ os.exit(1)
+ elseif non_option_index < #arg then
+ message.error("Multiple input files are not supported.")
+ os.exit(1)
+ end
+ local inputfile = arg[non_option_index]
+
+ -- If run as 'cllualatex', then the default engine is lualatex
+ if options.engine == nil and type(arg[0]) == "string" then
+ local basename = pathutil.trimext(pathutil.basename(arg[0]))
+ local engine_part = string.match(basename, "^cl(%w+)$")
+ if engine_part and KnownEngines[engine_part] then
+ options.engine = engine_part
+ end
+ end
+
+ if options.engine == nil then
+ message.error("Engine not specified.")
+ os.exit(1)
+ end
+ local engine = KnownEngines[options.engine]
+ if not engine then
+ message.error("Unknown engine name '", options.engine, "'.")
+ os.exit(1)
+ end
+
+ set_default_values(options)
+
+ return inputfile, engine, options
+end
+
+return {
+ usage = usage,
+ handle_cluttex_options = handle_cluttex_options,
+}
diff --git a/Master/texmf-dist/doc/support/cluttex/src/texrunner/isatty.lua b/Master/texmf-dist/doc/support/cluttex/src/texrunner/isatty.lua
new file mode 100644
index 00000000000..9969f691636
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/src/texrunner/isatty.lua
@@ -0,0 +1,179 @@
+--[[
+ Copyright 2018 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+if os.type == "unix" then
+ -- Try luaposix
+ local succ, M = pcall(function()
+ local isatty = require "posix.unistd".isatty
+ local fileno = require "posix.stdio".fileno
+ return {
+ isatty = function(file)
+ return isatty(fileno(file)) == 1
+ end,
+ }
+ end)
+ if succ then
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: isatty found via luaposix\n")
+ end
+ return M
+ else
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: luaposix not found: ", M, "\n")
+ end
+ end
+
+ -- Try LuaJIT-like FFI
+ local succ, M = pcall(function()
+ local ffi = require "ffi"
+ ffi.cdef[[
+int isatty(int fd);
+int fileno(void *stream);
+]]
+ local isatty = assert(ffi.C.isatty, "isatty not found")
+ local fileno = assert(ffi.C.fileno, "fileno not found")
+ return {
+ isatty = function(file)
+ -- LuaJIT converts Lua's file handles into FILE* (void*)
+ return isatty(fileno(file)) ~= 0
+ end
+ }
+ end)
+ if succ then
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: isatty found via FFI (Unix)\n")
+ end
+ return M
+ else
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: FFI (Unix) not found: ", M, "\n")
+ end
+ end
+
+else
+ -- Try LuaJIT
+ -- TODO: Try to detect MinTTY using GetFileInformationByHandleEx
+ local succ, M = pcall(function()
+ local ffi = require "ffi"
+ local bitlib = assert(bit32 or bit, "Neither bit32 (Lua 5.2) nor bit (LuaJIT) found") -- Lua 5.2 or LuaJIT
+ ffi.cdef[[
+int _isatty(int fd);
+int _fileno(void *stream);
+void *_get_osfhandle(int fd); // should return intptr_t
+typedef int BOOL;
+typedef uint32_t DWORD;
+typedef int FILE_INFO_BY_HANDLE_CLASS; // ???
+typedef struct _FILE_NAME_INFO {
+DWORD FileNameLength;
+uint16_t FileName[?];
+} FILE_NAME_INFO;
+DWORD GetFileType(void *hFile);
+BOOL GetFileInformationByHandleEx(void *hFile, FILE_INFO_BY_HANDLE_CLASS fic, void *fileinfo, DWORD dwBufferSize);
+BOOL GetConsoleMode(void *hConsoleHandle, DWORD* lpMode);
+BOOL SetConsoleMode(void *hConsoleHandle, DWORD dwMode);
+]]
+ local isatty = assert(ffi.C._isatty, "_isatty not found")
+ local fileno = assert(ffi.C._fileno, "_fileno not found")
+ local get_osfhandle = assert(ffi.C._get_osfhandle, "_get_osfhandle not found")
+ local GetFileType = assert(ffi.C.GetFileType, "GetFileType not found")
+ local GetFileInformationByHandleEx = assert(ffi.C.GetFileInformationByHandleEx, "GetFileInformationByHandleEx not found")
+ local GetConsoleMode = assert(ffi.C.GetConsoleMode, "GetConsoleMode not found")
+ local SetConsoleMode = assert(ffi.C.SetConsoleMode, "SetConsoleMode not found")
+ local function wide_to_narrow(array, length)
+ local t = {}
+ for i = 0, length - 1 do
+ table.insert(t, string.char(math.min(array[i], 0xff)))
+ end
+ return table.concat(t, "")
+ end
+ local function is_mintty(fd)
+ local handle = get_osfhandle(fd)
+ local filetype = GetFileType(handle)
+ if filetype ~= 0x0003 then -- not FILE_TYPE_PIPE (0x0003)
+ -- mintty must be a pipe
+ if CLUTTEX_VERBOSITY >= 4 then
+ io.stderr:write("ClutTeX: not a pipe\n")
+ end
+ return false
+ end
+ local nameinfo = ffi.new("FILE_NAME_INFO", 32768)
+ local FileNameInfo = 2 -- : FILE_INFO_BY_HANDLE_CLASS
+ if GetFileInformationByHandleEx(handle, FileNameInfo, nameinfo, ffi.sizeof("FILE_NAME_INFO", 32768)) ~= 0 then
+ local filename = wide_to_narrow(nameinfo.FileName, math.floor(nameinfo.FileNameLength / 2))
+ -- \(cygwin|msys)-<hex digits>-pty<N>-(from|to)-master
+ if CLUTTEX_VERBOSITY >= 4 then
+ io.stderr:write("ClutTeX: GetFileInformationByHandleEx returned ", filename, "\n")
+ end
+ local a, b = string.match(filename, "^\\(%w+)%-%x+%-pty%d+%-(%w+)%-master$")
+ if (a == "cygwin" or a == "msys") and (b == "from" or b == "to") then
+ return true
+ end
+ else
+ if CLUTTEX_VERBOSITY >= 4 then
+ io.stderr:write("ClutTeX: GetFileInformationByHandleEx failed\n")
+ end
+ end
+ return false
+ end
+ return {
+ isatty = function(file)
+ -- LuaJIT converts Lua's file handles into FILE* (void*)
+ local fd = fileno(file)
+ return isatty(fd) ~= 0 or is_mintty(fd)
+ end,
+ enable_console_colors = function(file)
+ local fd = fileno(file)
+ if isatty(fd) ~= 0 then
+ local handle = get_osfhandle(fd)
+ local modePtr = ffi.new("DWORD[1]")
+ local result = GetConsoleMode(handle, modePtr)
+ if result ~= 0 then
+ local ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
+ result = SetConsoleMode(handle, bitlib.bor(modePtr[0], ENABLE_VIRTUAL_TERMINAL_PROCESSING))
+ if result == 0 then
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: SetConsoleMode failed\n")
+ end
+ end
+ else
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: GetConsoleMode failed\n")
+ end
+ end
+ end
+ end,
+ }
+ end)
+ if succ then
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: isatty found via FFI (Windows)\n")
+ end
+ return M
+ else
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: FFI (Windows) not found: ", M, "\n")
+ end
+ end
+end
+
+return {
+ isatty = function(file)
+ return false
+ end,
+}
diff --git a/Master/texmf-dist/doc/support/cluttex/src/texrunner/luatexinit.lua b/Master/texmf-dist/doc/support/cluttex/src/texrunner/luatexinit.lua
new file mode 100644
index 00000000000..2e2ad971efb
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/src/texrunner/luatexinit.lua
@@ -0,0 +1,99 @@
+local function create_initialization_script(filename, options)
+ local initscript = assert(io.open(filename,"w"))
+ if type(options.file_line_error) == "boolean" then
+ initscript:write(string.format("texconfig.file_line_error = %s\n", options.file_line_error))
+ end
+ if type(options.halt_on_error) == "boolean" then
+ initscript:write(string.format("texconfig.halt_on_error = %s\n", options.halt_on_error))
+ end
+ initscript:write([==[
+local print = print
+local io_open = io.open
+local io_write = io.write
+local os_execute = os.execute
+local texio_write = texio.write
+local texio_write_nl = texio.write_nl
+]==])
+
+ -- Packages coded in Lua doesn't follow -output-directory option and doesn't write command to the log file
+ initscript:write(string.format("local output_directory = %q\n", options.output_directory))
+ initscript:write([==[
+local luawritelog
+local function openluawritelog()
+ if not luawritelog then
+ luawritelog = assert(io_open(output_directory .. "/" .. tex.jobname .. ".cluttex-fls", "w"))
+ end
+ return luawritelog
+end
+io.open = function(fname, mode)
+ -- luatexja-ruby
+ if mode == "w" and fname == tex.jobname .. ".ltjruby" then
+ fname = output_directory .. "/" .. fname
+ end
+ if type(mode) == "string" and string.find(mode, "w") ~= nil then
+ -- write mode
+ openluawritelog():write("OUTPUT " .. fname .. "\n")
+ end
+ return io_open(fname, mode)
+end
+os.execute = function(...)
+ texio_write_nl("log", string.format("CLUTTEX_EXEC %s", ...), "")
+ return os_execute(...)
+end
+]==])
+
+ -- Silence some of the TeX output to the terminal.
+ initscript:write([==[
+local function start_file_cb(category, filename)
+ if category == 1 then -- a normal data file, like a TeX source
+ texio_write_nl("log", "("..filename)
+ elseif category == 2 then -- a font map coupling font names to resources
+ texio_write("log", "{"..filename)
+ elseif category == 3 then -- an image file (png, pdf, etc)
+ texio_write("<"..filename)
+ elseif category == 4 then -- an embedded font subset
+ texio_write("<"..filename)
+ elseif category == 5 then -- a fully embedded font
+ texio_write("<<"..filename)
+ else
+ print("start_file: unknown category", category, filename)
+ end
+end
+callback.register("start_file", start_file_cb)
+local function stop_file_cb(category)
+ if category == 1 then
+ texio_write("log", ")")
+ elseif category == 2 then
+ texio_write("log", "}")
+ elseif category == 3 then
+ texio_write(">")
+ elseif category == 4 then
+ texio_write(">")
+ elseif category == 5 then
+ texio_write(">>")
+ else
+ print("stop_file: unknown category", category)
+ end
+end
+callback.register("stop_file", stop_file_cb)
+texio.write = function(...)
+ if select("#",...) == 1 then
+ -- Suppress luaotfload's message (See src/fontloader/runtime/fontload-reference.lua)
+ local s = ...
+ if string.match(s, "^%(using cache: ")
+ or string.match(s, "^%(using write cache: ")
+ or string.match(s, "^%(using read cache: ")
+ or string.match(s, "^%(load luc: ")
+ or string.match(s, "^%(load cache: ") then
+ return texio_write("log", ...)
+ end
+ end
+ return texio_write(...)
+end
+]==])
+ initscript:close()
+end
+
+return {
+ create_initialization_script = create_initialization_script
+}
diff --git a/Master/texmf-dist/doc/support/cluttex/src/texrunner/message.lua b/Master/texmf-dist/doc/support/cluttex/src/texrunner/message.lua
new file mode 100644
index 00000000000..662711021d5
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/src/texrunner/message.lua
@@ -0,0 +1,135 @@
+--[[
+ Copyright 2018 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local use_colors = false
+
+local function set_colors(mode)
+ local M
+ if mode == "always" then
+ use_colors = true
+ M = require "texrunner.isatty"
+ if M.enable_console_colors then
+ M.enable_console_colors(io.stderr)
+ end
+ elseif mode == "never" then
+ use_colors = false
+ elseif mode == "auto" then
+ M = require "texrunner.isatty"
+ use_colors = M.isatty(io.stderr)
+ else
+ error "The value of --color option must be one of 'auto', 'always', or 'never'."
+ end
+ if use_colors and M.enable_console_colors then
+ M.enable_console_colors(io.stderr)
+ end
+end
+
+-- ESCAPE: hex 1B = dec 27 = oct 33
+
+local CMD = {
+ reset = "\027[0m",
+ underline = "\027[4m",
+ fg_black = "\027[30m",
+ fg_red = "\027[31m",
+ fg_green = "\027[32m",
+ fg_yellow = "\027[33m",
+ fg_blue = "\027[34m",
+ fg_magenta = "\027[35m",
+ fg_cyan = "\027[36m",
+ fg_white = "\027[37m",
+ fg_reset = "\027[39m",
+ bg_black = "\027[40m",
+ bg_red = "\027[41m",
+ bg_green = "\027[42m",
+ bg_yellow = "\027[43m",
+ bg_blue = "\027[44m",
+ bg_magenta = "\027[45m",
+ bg_cyan = "\027[46m",
+ bg_white = "\027[47m",
+ bg_reset = "\027[49m",
+ fg_x_black = "\027[90m",
+ fg_x_red = "\027[91m",
+ fg_x_green = "\027[92m",
+ fg_x_yellow = "\027[93m",
+ fg_x_blue = "\027[94m",
+ fg_x_magenta = "\027[95m",
+ fg_x_cyan = "\027[96m",
+ fg_x_white = "\027[97m",
+ bg_x_black = "\027[100m",
+ bg_x_red = "\027[101m",
+ bg_x_green = "\027[102m",
+ bg_x_yellow = "\027[103m",
+ bg_x_blue = "\027[104m",
+ bg_x_magenta = "\027[105m",
+ bg_x_cyan = "\027[106m",
+ bg_x_white = "\027[107m",
+}
+
+local function exec_msg(commandline)
+ if use_colors then
+ io.stderr:write(CMD.fg_x_white, CMD.bg_red, "[EXEC]", CMD.reset, " ", CMD.fg_red, commandline, CMD.reset, "\n")
+ else
+ io.stderr:write("[EXEC] ", commandline, "\n")
+ end
+end
+
+local function error_msg(...)
+ local message = table.concat({...}, "")
+ if use_colors then
+ io.stderr:write(CMD.fg_x_white, CMD.bg_red, "[ERROR]", CMD.reset, " ", CMD.fg_red, message, CMD.reset, "\n")
+ else
+ io.stderr:write("[ERROR] ", message, "\n")
+ end
+end
+
+local function warn_msg(...)
+ local message = table.concat({...}, "")
+ if use_colors then
+ io.stderr:write(CMD.fg_x_white, CMD.bg_red, "[WARN]", CMD.reset, " ", CMD.fg_blue, message, CMD.reset, "\n")
+ else
+ io.stderr:write("[WARN] ", message, "\n")
+ end
+end
+
+local function diag_msg(...)
+ local message = table.concat({...}, "")
+ if use_colors then
+ io.stderr:write(CMD.fg_x_white, CMD.bg_red, "[DIAG]", CMD.reset, " ", CMD.fg_blue, message, CMD.reset, "\n")
+ else
+ io.stderr:write("[DIAG] ", message, "\n")
+ end
+end
+
+local function info_msg(...)
+ local message = table.concat({...}, "")
+ if use_colors then
+ io.stderr:write(CMD.fg_x_white, CMD.bg_red, "[INFO]", CMD.reset, " ", CMD.fg_magenta, message, CMD.reset, "\n")
+ else
+ io.stderr:write("[INFO] ", message, "\n")
+ end
+end
+
+return {
+ set_colors = set_colors,
+ exec = exec_msg,
+ error = error_msg,
+ warn = warn_msg,
+ diag = diag_msg,
+ info = info_msg,
+}
diff --git a/Master/texmf-dist/doc/support/cluttex/src/texrunner/option.lua b/Master/texmf-dist/doc/support/cluttex/src/texrunner/option.lua
new file mode 100644
index 00000000000..839e3a40d67
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/src/texrunner/option.lua
@@ -0,0 +1,149 @@
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+-- options_and_params, i = parseoption(arg, options)
+-- options[i] = {short = "o", long = "option" [, param = true] [, boolean = true] [, allow_single_hyphen = false]}
+-- arg[i], arg[i + 1], ..., arg[#arg] are non-options
+local function parseoption(arg, options)
+ local i = 1
+ local option_and_params = {}
+ while i <= #arg do
+ if arg[i] == "--" then
+ -- Stop handling options
+ i = i + 1
+ break
+ elseif arg[i]:sub(1,2) == "--" then
+ -- Long option
+ local name,param = arg[i]:match("^([^=]+)=(.*)$", 3)
+ name = name or arg[i]:sub(3)
+ local opt = nil
+ for _,o in ipairs(options) do
+ if o.long then
+ if o.long == name then
+ if o.param then
+ if param then
+ -- --option=param
+ else
+ if o.default ~= nil then
+ param = o.default
+ else
+ -- --option param
+ assert(i + 1 <= #arg, "argument missing after " .. arg[i] .. " option")
+ param = arg[i + 1]
+ i = i + 1
+ end
+ end
+ else
+ -- --option
+ param = true
+ end
+ opt = o
+ break
+ elseif o.boolean and name == "no-" .. o.long then
+ -- --no-option
+ opt = o
+ break
+ end
+ end
+ end
+ if opt then
+ table.insert(option_and_params, {opt.long, param})
+ else
+ -- Unknown long option
+ error("unknown long option: " .. arg[i])
+ end
+ elseif arg[i]:sub(1,1) == "-" then
+ local name,param = arg[i]:match("^([^=]+)=(.*)$", 2)
+ name = name or arg[i]:sub(2)
+ local opt = nil
+ for _,o in ipairs(options) do
+ if o.long and o.allow_single_hyphen then
+ if o.long == name then
+ if o.param then
+ if param then
+ -- -option=param
+ else
+ if o.default ~= nil then
+ param = o.default
+ else
+ -- -option param
+ assert(i + 1 <= #arg, "argument missing after " .. arg[i] .. " option")
+ param = arg[i + 1]
+ i = i + 1
+ end
+ end
+ else
+ -- -option
+ param = true
+ end
+ opt = o
+ break
+ elseif o.boolean and name == "no-" .. o.long then
+ -- -no-option
+ opt = o
+ break
+ end
+ elseif o.long and #name >= 2 and (o.long == name or (o.boolean and name == "no-" .. o.long)) then
+ error("You must supply two hyphens (i.e. --" .. name .. ") for long option")
+ end
+ end
+ if opt == nil then
+ -- Short option
+ name = arg[i]:sub(2,2)
+ for _,o in ipairs(options) do
+ if o.short then
+ if o.short == name then
+ if o.param then
+ if #arg[i] > 2 then
+ -- -oparam
+ param = arg[i]:sub(3)
+ else
+ -- -o param
+ assert(i + 1 <= #arg, "argument missing after " .. arg[i] .. " option")
+ param = arg[i + 1]
+ i = i + 1
+ end
+ else
+ -- -o
+ assert(#arg[i] == 2, "combining multiple short options like -abc is not supported")
+ param = true
+ end
+ opt = o
+ break
+ end
+ end
+ end
+ end
+ if opt then
+ table.insert(option_and_params, {opt.long or opt.short, param})
+ else
+ error("unknown short option: " .. arg[i])
+ end
+ else
+ -- arg[i] is not an option
+ break
+ end
+ i = i + 1
+ end
+ return option_and_params, i
+end
+
+return {
+ parseoption = parseoption;
+}
diff --git a/Master/texmf-dist/doc/support/cluttex/src/texrunner/pathutil.lua b/Master/texmf-dist/doc/support/cluttex/src/texrunner/pathutil.lua
new file mode 100644
index 00000000000..342195dcee6
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/src/texrunner/pathutil.lua
@@ -0,0 +1,35 @@
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+--[[
+ This module provides:
+ pathutil.basename(path)
+ pathutil.dirname(path)
+ pathutil.trimext(path)
+ pathutil.ext(path)
+ pathutil.replaceext(path, newext)
+ pathutil.join(...)
+ pathutil.abspath(path [, cwd])
+]]
+
+if os.type == "windows" then
+ return require("texrunner.pathutil_windows")
+else
+ return require("texrunner.pathutil_unix")
+end
diff --git a/Master/texmf-dist/doc/support/cluttex/src/texrunner/pathutil_unix.lua b/Master/texmf-dist/doc/support/cluttex/src/texrunner/pathutil_unix.lua
new file mode 100644
index 00000000000..2fc72415edb
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/src/texrunner/pathutil_unix.lua
@@ -0,0 +1,214 @@
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+-- pathutil module for *nix
+
+local assert = assert
+local select = select
+local string = string
+local string_find = string.find
+local string_sub = string.sub
+local string_match = string.match
+local string_gsub = string.gsub
+local filesys = require "lfs"
+
+local function basename(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "/", i + 1, true)
+ if j == nil then
+ return string_sub(path, i + 1)
+ elseif j == #path then
+ return string_sub(path, i + 1, -2)
+ end
+ i = j
+ end
+end
+
+-- TEST CODE
+assert(basename("/path/to/file") == "file")
+assert(basename("/path/to/directory/") == "directory")
+assert(basename("/file") == "file")
+assert(basename("file") == "file")
+-- END TEST CODE
+
+local function dirname(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "/", i + 1, true)
+ if j == nil then
+ if i == 0 then
+ -- No directory portion
+ return "."
+ elseif i == 1 then
+ -- Root
+ return "/"
+ else
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ end
+ i = j
+ end
+end
+
+-- TEST CODE
+assert(dirname("/path/to/file") == "/path/to")
+assert(dirname("/path/to/directory/") == "/path/to/directory")
+assert(dirname("/file") == "/")
+assert(dirname("file") == ".")
+-- END TEST CODE
+
+local function parentdir(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "/", i + 1, true)
+ if j == nil then
+ if i == 0 then
+ -- No directory portion
+ return "."
+ elseif i == 1 then
+ -- Root
+ return "/"
+ else
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ elseif j == #path then
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ i = j
+ end
+end
+
+-- TEST CODE
+assert(parentdir("/path/to/file") == "/path/to")
+assert(parentdir("/path/to/directory/") == "/path/to")
+assert(parentdir("/file") == "/")
+assert(parentdir("file") == ".")
+-- END TEST CODE
+
+local function trimext(path)
+ return (string_gsub(path, "%.[^/%.]*$", ""))
+end
+
+-- TEST CODE
+assert(trimext("/path/to/file.ext") == "/path/to/file")
+assert(trimext("/path/t.o/file") == "/path/t.o/file")
+assert(trimext("file.ext") == "file")
+assert(trimext("file.e.xt") == "file.e")
+assert(trimext("file.ext.") == "file.ext")
+assert(trimext("file") == "file")
+-- END TEST CODE
+
+local function ext(path)
+ return string_match(path, "%.([^/%.]*)$") or ""
+end
+
+-- TEST CODE
+assert(ext("/path/to/file.ext") == "ext")
+assert(ext("/path/t.o/file") == "")
+assert(ext("file.ext") == "ext")
+assert(ext("file.e.xt") == "xt")
+assert(ext("file.ext.") == "")
+assert(ext("file") == "")
+-- END TEST CODE
+
+local function replaceext(path, newext)
+ local newpath, n = string_gsub(path, "%.([^/%.]*)$", function() return "." .. newext end)
+ if n == 0 then
+ return newpath .. "." .. newext
+ else
+ return newpath
+ end
+end
+
+-- TEST CODE
+assert(replaceext("/path/to/file.ext", "tor") == "/path/to/file.tor")
+assert(replaceext("/path/t.o/file", "tor") == "/path/t.o/file.tor")
+assert(replaceext("file.ext", "tor") == "file.tor")
+assert(replaceext("file.e.xt", "tor") == "file.e.tor")
+assert(replaceext("file.ext.", "tor") == "file.ext.tor")
+assert(replaceext("file", "tor") == "file.tor")
+-- END TEST CODE
+
+local function joinpath2(x, y)
+ local xd = x
+ if string_sub(x, -1) ~= "/" then
+ xd = x .. "/"
+ end
+ if y == "." then
+ return xd
+ elseif y == ".." then
+ return dirname(x)
+ else
+ if string_sub(y, 1, 2) == "./" then
+ return xd .. string_sub(y, 3)
+ else
+ return xd .. y
+ end
+ end
+end
+
+local function joinpath(...)
+ local n = select("#", ...)
+ if n == 2 then
+ return joinpath2(...)
+ elseif n == 0 then
+ return "."
+ elseif n == 1 then
+ return ...
+ else
+ return joinpath(joinpath2(...), select(3, ...))
+ end
+end
+
+-- TEST CODE
+assert(joinpath("/path/", "to", "somewhere") == "/path/to/somewhere")
+assert(joinpath("/path/", "to", "somewhere", "..") == "/path/to")
+assert(joinpath("/path/", "to", "somewhere", "..", "elsewhere") == "/path/to/elsewhere")
+assert(joinpath("/path/", "to", "./somewhere.txt") == "/path/to/somewhere.txt")
+-- END TEST CODE
+
+local function abspath(path, cwd)
+ if string_sub(path, 1, 1) == "/" then
+ -- absolute path
+ return path
+ else
+ cwd = cwd or filesys.currentdir()
+ return joinpath2(cwd, path)
+ end
+end
+
+-- TEST CODE
+assert(abspath("world.txt", "/hello") == "/hello/world.txt")
+assert(abspath("/world.txt", "/hello") == "/world.txt")
+-- END TEST CODE
+
+return {
+ basename = basename,
+ dirname = dirname,
+ parentdir = parentdir,
+ trimext = trimext,
+ ext = ext,
+ replaceext = replaceext,
+ join = joinpath,
+ abspath = abspath,
+}
diff --git a/Master/texmf-dist/doc/support/cluttex/src/texrunner/pathutil_windows.lua b/Master/texmf-dist/doc/support/cluttex/src/texrunner/pathutil_windows.lua
new file mode 100644
index 00000000000..8b15efd0e4d
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/src/texrunner/pathutil_windows.lua
@@ -0,0 +1,222 @@
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+-- pathutil module
+
+local assert = assert
+local select = select
+local string = string
+local string_find = string.find
+local string_sub = string.sub
+local string_match = string.match
+local string_gsub = string.gsub
+local filesys = require "lfs"
+
+local function basename(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "[\\/]", i + 1)
+ if j == nil then
+ return string_sub(path, i + 1)
+ elseif j == #path then
+ return string_sub(path, i + 1, -2)
+ end
+ i = j
+ end
+end
+
+-- TEST CODE
+assert(basename("/path/to/file") == "file")
+assert(basename("/path/to/directory/") == "directory")
+assert(basename([[c:\path\to/directory\]]) == "directory")
+assert(basename("/file") == "file")
+assert(basename("file") == "file")
+-- END TEST CODE
+
+local function dirname(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "[\\/]", i + 1)
+ if j == nil then
+ if i == 0 then
+ -- No directory portion
+ return "."
+ elseif i == 1 then
+ -- Root
+ return string_sub(path, 1, 1)
+ else
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ end
+ i = j
+ end
+end
+
+-- TEST CODE
+assert(dirname("/path/to/file") == "/path/to")
+assert(dirname("/path/to/directory/") == "/path/to/directory")
+assert(dirname([[c:/path\to/file]]) == [[c:/path\to]])
+assert(dirname("/file") == "/")
+assert(dirname("file") == ".")
+-- END TEST CODE
+
+local function parentdir(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "[\\/]", i + 1)
+ if j == nil then
+ if i == 0 then
+ -- No directory portion
+ return "."
+ elseif i == 1 then
+ -- Root
+ return string_sub(path, 1, 1)
+ else
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ elseif j == #path then
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ i = j
+ end
+end
+
+-- TEST CODE
+assert(parentdir("/path/to/file") == "/path/to")
+assert(parentdir("/path/to/directory/") == "/path/to")
+assert(parentdir("/file") == "/")
+assert(parentdir("file") == ".")
+-- END TEST CODE
+
+local function trimext(path)
+ return (string_gsub(path, "%.[^\\/%.]*$", ""))
+end
+
+-- TEST CODE
+assert(trimext("/path/to/file.ext") == "/path/to/file")
+assert(trimext("/path/t.o/file") == "/path/t.o/file")
+assert(trimext([[c:/path/t.o\file]]) == [[c:/path/t.o\file]])
+assert(trimext("file.ext") == "file")
+assert(trimext("file.e.xt") == "file.e")
+assert(trimext("file.ext.") == "file.ext")
+assert(trimext("file") == "file")
+-- END TEST CODE
+
+local function ext(path)
+ return string_match(path, "%.([^\\/%.]*)$") or ""
+end
+
+-- TEST CODE
+assert(ext("/path/to/file.ext") == "ext")
+assert(ext("/path/t.o/file") == "")
+assert(ext([[c:/path/t.o\file]]) == "")
+assert(ext("file.ext") == "ext")
+assert(ext("file.e.xt") == "xt")
+assert(ext("file.ext.") == "")
+assert(ext("file") == "")
+-- END TEST CODE
+
+local function replaceext(path, newext)
+ local newpath, n = string_gsub(path, "%.([^\\/%.]*)$", function() return "." .. newext end)
+ if n == 0 then
+ return newpath .. "." .. newext
+ else
+ return newpath
+ end
+end
+
+-- TEST CODE
+assert(replaceext("/path/to/file.ext", "tor") == "/path/to/file.tor")
+assert(replaceext("/path/t.o/file", "tor") == "/path/t.o/file.tor")
+assert(replaceext([[c:/path/t.o\file]], "tor") == [[c:/path/t.o\file.tor]])
+assert(replaceext("file.ext", "tor") == "file.tor")
+assert(replaceext("file.e.xt", "tor") == "file.e.tor")
+assert(replaceext("file.ext.", "tor") == "file.ext.tor")
+assert(replaceext("file", "tor") == "file.tor")
+-- END TEST CODE
+
+local function joinpath2(x, y)
+ local xd = x
+ local last = string_sub(x, -1)
+ if last ~= "/" and last ~= "\\" then
+ xd = x .. "\\"
+ end
+ if y == "." then
+ return xd
+ elseif y == ".." then
+ return dirname(x)
+ else
+ if string_match(y, "^%.[\\/]") then
+ return xd .. string_sub(y, 3)
+ else
+ return xd .. y
+ end
+ end
+end
+
+local function joinpath(...)
+ local n = select("#", ...)
+ if n == 2 then
+ return joinpath2(...)
+ elseif n == 0 then
+ return "."
+ elseif n == 1 then
+ return ...
+ else
+ return joinpath(joinpath2(...), select(3, ...))
+ end
+end
+
+-- TEST CODE
+assert(joinpath("/path/", "to", "somewhere") == [[/path/to\somewhere]])
+assert(joinpath("/path/", "to", "somewhere", "..") == [[/path/to]])
+assert(joinpath("/path/", "to", "somewhere", "..", "elsewhere") == [[/path/to\elsewhere]])
+assert(joinpath("/path/", "to", "./somewhere.txt") == "/path/to/somewhere.txt")
+-- END TEST CODE
+
+-- https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
+local function isabspath(path)
+ local init = string_sub(path, 1, 1)
+ return init == "\\" or init == "/" or string_match(path, "^%a:[/\\]")
+end
+
+local function abspath(path, cwd)
+ if isabspath(path) then
+ -- absolute path
+ return path
+ else
+ -- TODO: relative path with a drive letter is not supported
+ cwd = cwd or filesys.currentdir()
+ return joinpath2(cwd, path)
+ end
+end
+
+return {
+ basename = basename,
+ dirname = dirname,
+ parentdir = parentdir,
+ trimext = trimext,
+ ext = ext,
+ replaceext = replaceext,
+ join = joinpath,
+ abspath = abspath,
+}
diff --git a/Master/texmf-dist/doc/support/cluttex/src/texrunner/recovery.lua b/Master/texmf-dist/doc/support/cluttex/src/texrunner/recovery.lua
new file mode 100644
index 00000000000..db33b8fb524
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/src/texrunner/recovery.lua
@@ -0,0 +1,86 @@
+--[[
+ Copyright 2018 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local io = io
+local string = string
+local parse_aux_file = require "texrunner.auxfile".parse_aux_file
+local pathutil = require "texrunner.pathutil"
+local fsutil = require "texrunner.fsutil"
+local shellutil = require "texrunner.shellutil"
+local message = require "texrunner.message"
+
+local function create_missing_directories(args)
+ if string.find(args.execlog, "I can't write on file", 1, true) then
+ -- There is a possibility that there are some subfiles under subdirectories.
+ -- Directories for sub-auxfiles are not created automatically, so we need to provide them.
+ local report = parse_aux_file(args.auxfile, args.options.output_directory)
+ if report.made_new_directory then
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("Created missing directories.")
+ end
+ return true
+ end
+ end
+ return false
+end
+
+local function run_epstopdf(args)
+ local run = false
+ if args.options.shell_escape ~= false then -- (possibly restricted) \write18 enabled
+ for outfile, infile in string.gmatch(args.execlog, "%(epstopdf%)%s*Command: <r?epstopdf %-%-outfile=([%w%-/]+%.pdf) ([%w%-/]+%.eps)>") do
+ local infile_abs = pathutil.abspath(infile, args.original_wd)
+ if fsutil.isfile(infile_abs) then -- input file exists
+ local outfile_abs = pathutil.abspath(outfile, args.options.output_directory)
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("Running epstopdf on ", infile, ".")
+ end
+ local outdir = pathutil.dirname(outfile_abs)
+ if not fsutil.isdir(outdir) then
+ assert(fsutil.mkdir_rec(outdir))
+ end
+ local command = string.format("epstopdf --outfile=%s %s", shellutil.escape(outfile_abs), shellutil.escape(infile_abs))
+ message.exec(command)
+ local success = os.execute(command)
+ if type(success) == "number" then -- Lua 5.1 or LuaTeX
+ success = success == 0
+ end
+ run = run or success
+ end
+ end
+ end
+ return run
+end
+
+local function check_minted(args)
+ return string.find(args.execlog, "Package minted Error: Missing Pygments output; \\inputminted was") ~= nil
+end
+
+local function try_recovery(args)
+ local recovered = false
+ recovered = create_missing_directories(args)
+ recovered = run_epstopdf(args) or recovered
+ recovered = check_minted(args) or recovered
+ return recovered
+end
+
+return {
+ create_missing_directories = create_missing_directories,
+ run_epstopdf = run_epstopdf,
+ try_recovery = try_recovery,
+}
diff --git a/Master/texmf-dist/doc/support/cluttex/src/texrunner/reruncheck.lua b/Master/texmf-dist/doc/support/cluttex/src/texrunner/reruncheck.lua
new file mode 100644
index 00000000000..d1602bc5f76
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/src/texrunner/reruncheck.lua
@@ -0,0 +1,250 @@
+--[[
+ Copyright 2016,2018 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local io = io
+local assert = assert
+local filesys = require "lfs"
+local md5 = require "md5"
+local fsutil = require "texrunner.fsutil"
+local pathutil = require "texrunner.pathutil"
+local message = require "texrunner.message"
+
+local function md5sum_file(path)
+ local f = assert(io.open(path, "rb"))
+ local contents = f:read("*a")
+ f:close()
+ return md5.sum(contents)
+end
+
+-- filelist, filemap = parse_recorder_file("jobname.fls", options [, filelist, filemap])
+-- filelist[i] = {path = "...", abspath = "...", kind = "input" or "output" or "auxiliary"}
+local function parse_recorder_file(file, options, filelist, filemap)
+ filelist = filelist or {}
+ filemap = filemap or {}
+ for l in io.lines(file) do
+ local t,path = l:match("^(%w+) (.*)$")
+ if t == "PWD" then
+ -- Ignore
+
+ elseif t == "INPUT" then
+ local abspath = pathutil.abspath(path)
+ local fileinfo = filemap[abspath]
+ if not fileinfo then
+ if fsutil.isfile(path) then
+ local kind = "input"
+ local ext = pathutil.ext(path)
+ if ext == "bbl" then
+ kind = "auxiliary"
+ end
+ fileinfo = {path = path, abspath = abspath, kind = kind}
+ table.insert(filelist, fileinfo)
+ filemap[abspath] = fileinfo
+ else
+ -- Maybe a command execution
+ end
+ else
+ if #path < #fileinfo.path then
+ fileinfo.path = path
+ end
+ if fileinfo.kind == "output" then
+ -- The files listed in both INPUT and OUTPUT are considered to be auxiliary files.
+ fileinfo.kind = "auxiliary"
+ end
+ end
+
+ elseif t == "OUTPUT" then
+ local abspath = pathutil.abspath(path)
+ local fileinfo = filemap[abspath]
+ if not fileinfo then
+ local kind = "output"
+ local ext = pathutil.ext(path)
+ if ext == "out" then
+ -- hyperref bookmarks file
+ kind = "auxiliary"
+ elseif options.makeindex and ext == "idx" then
+ -- Treat .idx files (to be processed by MakeIndex) as auxiliary
+ kind = "auxiliary"
+ -- ...and .ind files
+ elseif ext == "bcf" then -- biber
+ kind = "auxiliary"
+ elseif ext == "glo" then -- makeglossaries
+ kind = "auxiliary"
+ end
+ fileinfo = {path = path, abspath = abspath, kind = kind}
+ table.insert(filelist, fileinfo)
+ filemap[abspath] = fileinfo
+ else
+ if #path < #fileinfo.path then
+ fileinfo.path = path
+ end
+ if fileinfo.kind == "input" then
+ -- The files listed in both INPUT and OUTPUT are considered to be auxiliary files.
+ fileinfo.kind = "auxiliary"
+ end
+ end
+
+ else
+ message.warning("Unrecognized line in recorder file '", file, "': ", l)
+ end
+ end
+ return filelist, filemap
+end
+
+-- auxstatus = collectfileinfo(filelist [, auxstatus])
+local function collectfileinfo(filelist, auxstatus)
+ auxstatus = auxstatus or {}
+ for i,fileinfo in ipairs(filelist) do
+ local path = fileinfo.abspath
+ if fsutil.isfile(path) then
+ local status = auxstatus[path] or {}
+ auxstatus[path] = status
+ if fileinfo.kind == "input" then
+ status.mtime = status.mtime or filesys.attributes(path, "modification")
+ elseif fileinfo.kind == "auxiliary" then
+ status.mtime = status.mtime or filesys.attributes(path, "modification")
+ status.size = status.size or filesys.attributes(path, "size")
+ status.md5sum = status.md5sum or md5sum_file(path)
+ end
+ end
+ end
+ return auxstatus
+end
+
+local function binarytohex(s)
+ return (s:gsub(".", function(c) return string.format("%02x", string.byte(c)) end))
+end
+
+-- should_rerun, newauxstatus = comparefileinfo(auxfiles, auxstatus)
+local function comparefileinfo(filelist, auxstatus)
+ local should_rerun = false
+ local newauxstatus = {}
+ for i,fileinfo in ipairs(filelist) do
+ local path = fileinfo.abspath
+ if fsutil.isfile(path) then
+ if fileinfo.kind == "input" then
+ -- Input file: User might have modified while running TeX.
+ local mtime = filesys.attributes(path, "modification")
+ if auxstatus[path] and auxstatus[path].mtime then
+ if auxstatus[path].mtime < mtime then
+ -- Input file was updated during execution
+ message.info("Input file '", fileinfo.path, "' was modified (by user, or some external commands).")
+ newauxstatus[path] = {mtime = mtime}
+ return true, newauxstatus
+ end
+ else
+ -- New input file
+ end
+
+ elseif fileinfo.kind == "auxiliary" then
+ -- Auxiliary file: Compare file contents.
+ if auxstatus[path] then
+ -- File was touched during execution
+ local really_modified = false
+ local modified_because = nil
+ local size = filesys.attributes(path, "size")
+ if auxstatus[path].size ~= size then
+ really_modified = true
+ if auxstatus[path].size then
+ modified_because = string.format("size: %d -> %d", auxstatus[path].size, size)
+ else
+ modified_because = string.format("size: (N/A) -> %d", size)
+ end
+ newauxstatus[path] = {size = size}
+ else
+ local md5sum = md5sum_file(path)
+ if auxstatus[path].md5sum ~= md5sum then
+ really_modified = true
+ if auxstatus[path].md5sum then
+ modified_because = string.format("md5: %s -> %s", binarytohex(auxstatus[path].md5sum), binarytohex(md5sum))
+ else
+ modified_because = string.format("md5: (N/A) -> %s", binarytohex(md5sum))
+ end
+ end
+ newauxstatus[path] = {size = size, md5sum = md5sum}
+ end
+ if really_modified then
+ message.info("File '", fileinfo.path, "' was modified (", modified_because, ").")
+ should_rerun = true
+ else
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("File '", fileinfo.path, "' unmodified (size and md5sum).")
+ end
+ end
+ else
+ -- New file
+ if path:sub(-4) == ".aux" then
+ local size = filesys.attributes(path, "size")
+ if size == 8 then
+ local auxfile = io.open(path, "rb")
+ local contents = auxfile:read("*a")
+ auxfile:close()
+ if contents == "\\relax \n" then
+ -- The .aux file is new, but it is almost empty
+ else
+ should_rerun = true
+ end
+ newauxstatus[path] = {size = size, md5sum = md5.sum(contents)}
+ else
+ should_rerun = true
+ newauxstatus[path] = {size = size}
+ end
+ else
+ should_rerun = true
+ end
+ if should_rerun then
+ message.info("New auxiliary file '", fileinfo.path, "'.")
+ else
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("Ignoring almost-empty auxiliary file '", fileinfo.path, "'.")
+ end
+ end
+ end
+ if should_rerun then
+ break
+ end
+ end
+ else
+ -- Auxiliary file is not really a file???
+ end
+ end
+ return should_rerun, newauxstatus
+end
+
+-- true if src is newer than dst
+local function comparefiletime(srcpath, dstpath, auxstatus)
+ if not filesys.isfile(dstpath) then
+ return true
+ end
+ local src_info = auxstatus[srcpath]
+ if src_info then
+ local src_mtime = src_info.mtime
+ if src_mtime then
+ local dst_mtime = filesys.attributes(dstpath, "modification")
+ return src_mtime > dst_mtime
+ end
+ end
+ return false
+end
+
+return {
+ parse_recorder_file = parse_recorder_file;
+ collectfileinfo = collectfileinfo;
+ comparefileinfo = comparefileinfo;
+ comparefiletime = comparefiletime;
+}
diff --git a/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil.lua b/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil.lua
new file mode 100644
index 00000000000..14068662e26
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil.lua
@@ -0,0 +1,26 @@
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+-- This module provides: shellutil.escape(s)
+
+if os.type == "windows" then
+ return require("texrunner.shellutil_windows")
+else
+ return require("texrunner.shellutil_unix")
+end
diff --git a/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_unix.lua b/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_unix.lua
new file mode 100644
index 00000000000..c506e26ffba
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_unix.lua
@@ -0,0 +1,62 @@
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local assert = assert
+local string_match = string.match
+local table = table
+local table_insert = table.insert
+local table_concat = table.concat
+
+-- s: string
+local function escape(s)
+ local len = #s
+ local result = {}
+ local t,i = string_match(s, "^([^']*)()")
+ assert(t)
+ if t ~= "" then
+ table_insert(result, "'")
+ table_insert(result, t)
+ table_insert(result, "'")
+ end
+ while i < len do
+ t,i = string_match(s, "^('+)()", i)
+ assert(t)
+ table_insert(result, '"')
+ table_insert(result, t)
+ table_insert(result, '"')
+ t,i = string_match(s, "^([^']*)()", i)
+ assert(t)
+ if t ~= "" then
+ table_insert(result, "'")
+ table_insert(result, t)
+ table_insert(result, "'")
+ end
+ end
+ return table_concat(result, "")
+end
+
+-- TEST CODE
+assert(escape([[Hello world!]]) == [['Hello world!']])
+assert(escape([[Hello' world!]]) == [['Hello'"'"' world!']])
+assert(escape([[Hello' world!"]]) == [['Hello'"'"' world!"']])
+-- END TEST CODE
+
+return {
+ escape = escape,
+}
diff --git a/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_windows.lua b/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_windows.lua
new file mode 100644
index 00000000000..cd0cc11fe9d
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_windows.lua
@@ -0,0 +1,35 @@
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local string_gsub = string.gsub
+
+-- s: string
+local function escape(s)
+ return '"' .. string_gsub(string_gsub(s, '(\\*)"', '%1%1\\"'), '(\\+)$', '%1%1') .. '"'
+end
+
+-- TEST CODE
+assert(escape([[Hello world!]]) == [["Hello world!"]])
+assert(escape([[Hello" world!]]) == [["Hello\" world!"]])
+assert(escape([[Hello\" world!"]]) == [["Hello\\\" world!\""]])
+-- END TEST CODE
+
+return {
+ escape = escape,
+}
diff --git a/Master/texmf-dist/doc/support/cluttex/src/texrunner/tex_engine.lua b/Master/texmf-dist/doc/support/cluttex/src/texrunner/tex_engine.lua
new file mode 100644
index 00000000000..e15e0f46a37
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/src/texrunner/tex_engine.lua
@@ -0,0 +1,159 @@
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local table = table
+local setmetatable = setmetatable
+local ipairs = ipairs
+
+local shellutil = require "texrunner.shellutil"
+
+--[[
+engine.name: string
+engine.type = "onePass" or "twoPass"
+engine:build_command(inputfile, options)
+ options:
+ halt_on_error: boolean
+ interaction: string
+ file_line_error: boolean
+ synctex: string
+ shell_escape: boolean
+ shell_restricted: boolean
+ jobname: string
+ output_directory: string
+ extraoptions: a list of strings
+ output_format: "pdf" or "dvi"
+ draftmode: boolean (pdfTeX / XeTeX / LuaTeX)
+ fmt: string
+ tex_injection: string
+ lua_initialization_script: string (LuaTeX only)
+engine.executable: string
+engine.supports_pdf_generation: boolean
+engine.dvi_extension: string
+engine.supports_draftmode: boolean
+engine.is_luatex: true or nil
+]]
+
+local engine_meta = {}
+engine_meta.__index = engine_meta
+engine_meta.dvi_extension = "dvi"
+function engine_meta:build_command(inputfile, options)
+ local command = {self.executable, "-recorder"}
+ if options.fmt then
+ table.insert(command, "-fmt=" .. options.fmt)
+ end
+ if options.halt_on_error then
+ table.insert(command, "-halt-on-error")
+ end
+ if options.interaction then
+ table.insert(command, "-interaction=" .. options.interaction)
+ end
+ if options.file_line_error then
+ table.insert(command, "-file-line-error")
+ end
+ if options.synctex then
+ table.insert(command, "-synctex=" .. shellutil.escape(options.synctex))
+ end
+ if options.shell_escape == false then
+ table.insert(command, "-no-shell-escape")
+ elseif options.shell_restricted == true then
+ table.insert(command, "-shell-restricted")
+ elseif options.shell_escape == true then
+ table.insert(command, "-shell-escape")
+ end
+ if options.jobname then
+ table.insert(command, "-jobname=" .. shellutil.escape(options.jobname))
+ end
+ if options.output_directory then
+ table.insert(command, "-output-directory=" .. shellutil.escape(options.output_directory))
+ end
+ if self.handle_additional_options then
+ self:handle_additional_options(command, options)
+ end
+ if options.extraoptions then
+ for _,v in ipairs(options.extraoptions) do
+ table.insert(command, v)
+ end
+ end
+ if type(options.tex_injection) == "string" then
+ table.insert(command, shellutil.escape(options.tex_injection .. "\\input " .. inputfile)) -- TODO: what if filename contains spaces?
+ else
+ table.insert(command, shellutil.escape(inputfile))
+ end
+ return table.concat(command, " ")
+end
+
+local function engine(name, supports_pdf_generation, handle_additional_options)
+ return setmetatable({
+ name = name,
+ executable = name,
+ supports_pdf_generation = supports_pdf_generation,
+ handle_additional_options = handle_additional_options,
+ supports_draftmode = supports_pdf_generation,
+ }, engine_meta)
+end
+
+local function handle_pdftex_options(self, args, options)
+ if options.draftmode then
+ table.insert(args, "-draftmode")
+ elseif options.output_format == "dvi" then
+ table.insert(args, "-output-format=dvi")
+ end
+end
+
+local function handle_xetex_options(self, args, options)
+ if options.output_format == "dvi" or options.draftmode then
+ table.insert(args, "-no-pdf")
+ end
+end
+
+local function handle_luatex_options(self, args, options)
+ if options.lua_initialization_script then
+ table.insert(args, "--lua="..shellutil.escape(options.lua_initialization_script))
+ end
+ handle_pdftex_options(self, args, options)
+end
+
+local function is_luatex(e)
+ e.is_luatex = true
+ return e
+end
+
+local KnownEngines = {
+ ["pdftex"] = engine("pdftex", true, handle_pdftex_options),
+ ["pdflatex"] = engine("pdflatex", true, handle_pdftex_options),
+ ["luatex"] = is_luatex(engine("luatex", true, handle_luatex_options)),
+ ["lualatex"] = is_luatex(engine("lualatex", true, handle_luatex_options)),
+ ["luajittex"] = is_luatex(engine("luajittex", true, handle_luatex_options)),
+ ["xetex"] = engine("xetex", true, handle_xetex_options),
+ ["xelatex"] = engine("xelatex", true, handle_xetex_options),
+ ["tex"] = engine("tex", false),
+ ["etex"] = engine("etex", false),
+ ["latex"] = engine("latex", false),
+ ["ptex"] = engine("ptex", false),
+ ["eptex"] = engine("eptex", false),
+ ["platex"] = engine("platex", false),
+ ["uptex"] = engine("uptex", false),
+ ["euptex"] = engine("euptex", false),
+ ["uplatex"] = engine("uplatex", false),
+}
+
+KnownEngines["xetex"].dvi_extension = "xdv"
+KnownEngines["xelatex"].dvi_extension = "xdv"
+
+return KnownEngines
diff --git a/Master/texmf-dist/scripts/cluttex/cluttex.lua b/Master/texmf-dist/scripts/cluttex/cluttex.lua
new file mode 100755
index 00000000000..5506364e92e
--- /dev/null
+++ b/Master/texmf-dist/scripts/cluttex/cluttex.lua
@@ -0,0 +1,2595 @@
+#!/usr/bin/env texlua
+local io, os, string, table, package, require, assert, error, ipairs, type, select, arg = io, os, string, table, package, require, assert, error, ipairs, type, select, arg
+local CLUTTEX_VERBOSITY, CLUTTEX_VERSION
+os.type = os.type or "unix"
+if lfs and not package.loaded['lfs'] then package.loaded['lfs'] = lfs end
+if os.type == "windows" then
+package.preload["texrunner.pathutil"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+-- pathutil module
+
+local assert = assert
+local select = select
+local string = string
+local string_find = string.find
+local string_sub = string.sub
+local string_match = string.match
+local string_gsub = string.gsub
+local filesys = require "lfs"
+
+local function basename(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "[\\/]", i + 1)
+ if j == nil then
+ return string_sub(path, i + 1)
+ elseif j == #path then
+ return string_sub(path, i + 1, -2)
+ end
+ i = j
+ end
+end
+
+
+local function dirname(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "[\\/]", i + 1)
+ if j == nil then
+ if i == 0 then
+ -- No directory portion
+ return "."
+ elseif i == 1 then
+ -- Root
+ return string_sub(path, 1, 1)
+ else
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ end
+ i = j
+ end
+end
+
+
+local function parentdir(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "[\\/]", i + 1)
+ if j == nil then
+ if i == 0 then
+ -- No directory portion
+ return "."
+ elseif i == 1 then
+ -- Root
+ return string_sub(path, 1, 1)
+ else
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ elseif j == #path then
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ i = j
+ end
+end
+
+
+local function trimext(path)
+ return (string_gsub(path, "%.[^\\/%.]*$", ""))
+end
+
+
+local function ext(path)
+ return string_match(path, "%.([^\\/%.]*)$") or ""
+end
+
+
+local function replaceext(path, newext)
+ local newpath, n = string_gsub(path, "%.([^\\/%.]*)$", function() return "." .. newext end)
+ if n == 0 then
+ return newpath .. "." .. newext
+ else
+ return newpath
+ end
+end
+
+
+local function joinpath2(x, y)
+ local xd = x
+ local last = string_sub(x, -1)
+ if last ~= "/" and last ~= "\\" then
+ xd = x .. "\\"
+ end
+ if y == "." then
+ return xd
+ elseif y == ".." then
+ return dirname(x)
+ else
+ if string_match(y, "^%.[\\/]") then
+ return xd .. string_sub(y, 3)
+ else
+ return xd .. y
+ end
+ end
+end
+
+local function joinpath(...)
+ local n = select("#", ...)
+ if n == 2 then
+ return joinpath2(...)
+ elseif n == 0 then
+ return "."
+ elseif n == 1 then
+ return ...
+ else
+ return joinpath(joinpath2(...), select(3, ...))
+ end
+end
+
+
+-- https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
+local function isabspath(path)
+ local init = string_sub(path, 1, 1)
+ return init == "\\" or init == "/" or string_match(path, "^%a:[/\\]")
+end
+
+local function abspath(path, cwd)
+ if isabspath(path) then
+ -- absolute path
+ return path
+ else
+ -- TODO: relative path with a drive letter is not supported
+ cwd = cwd or filesys.currentdir()
+ return joinpath2(cwd, path)
+ end
+end
+
+return {
+ basename = basename,
+ dirname = dirname,
+ parentdir = parentdir,
+ trimext = trimext,
+ ext = ext,
+ replaceext = replaceext,
+ join = joinpath,
+ abspath = abspath,
+}
+end
+else
+package.preload["texrunner.pathutil"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+-- pathutil module for *nix
+
+local assert = assert
+local select = select
+local string = string
+local string_find = string.find
+local string_sub = string.sub
+local string_match = string.match
+local string_gsub = string.gsub
+local filesys = require "lfs"
+
+local function basename(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "/", i + 1, true)
+ if j == nil then
+ return string_sub(path, i + 1)
+ elseif j == #path then
+ return string_sub(path, i + 1, -2)
+ end
+ i = j
+ end
+end
+
+
+local function dirname(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "/", i + 1, true)
+ if j == nil then
+ if i == 0 then
+ -- No directory portion
+ return "."
+ elseif i == 1 then
+ -- Root
+ return "/"
+ else
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ end
+ i = j
+ end
+end
+
+
+local function parentdir(path)
+ local i = 0
+ while true do
+ local j = string_find(path, "/", i + 1, true)
+ if j == nil then
+ if i == 0 then
+ -- No directory portion
+ return "."
+ elseif i == 1 then
+ -- Root
+ return "/"
+ else
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ elseif j == #path then
+ -- Directory portion without trailing slash
+ return string_sub(path, 1, i - 1)
+ end
+ i = j
+ end
+end
+
+
+local function trimext(path)
+ return (string_gsub(path, "%.[^/%.]*$", ""))
+end
+
+
+local function ext(path)
+ return string_match(path, "%.([^/%.]*)$") or ""
+end
+
+
+local function replaceext(path, newext)
+ local newpath, n = string_gsub(path, "%.([^/%.]*)$", function() return "." .. newext end)
+ if n == 0 then
+ return newpath .. "." .. newext
+ else
+ return newpath
+ end
+end
+
+
+local function joinpath2(x, y)
+ local xd = x
+ if string_sub(x, -1) ~= "/" then
+ xd = x .. "/"
+ end
+ if y == "." then
+ return xd
+ elseif y == ".." then
+ return dirname(x)
+ else
+ if string_sub(y, 1, 2) == "./" then
+ return xd .. string_sub(y, 3)
+ else
+ return xd .. y
+ end
+ end
+end
+
+local function joinpath(...)
+ local n = select("#", ...)
+ if n == 2 then
+ return joinpath2(...)
+ elseif n == 0 then
+ return "."
+ elseif n == 1 then
+ return ...
+ else
+ return joinpath(joinpath2(...), select(3, ...))
+ end
+end
+
+
+local function abspath(path, cwd)
+ if string_sub(path, 1, 1) == "/" then
+ -- absolute path
+ return path
+ else
+ cwd = cwd or filesys.currentdir()
+ return joinpath2(cwd, path)
+ end
+end
+
+
+return {
+ basename = basename,
+ dirname = dirname,
+ parentdir = parentdir,
+ trimext = trimext,
+ ext = ext,
+ replaceext = replaceext,
+ join = joinpath,
+ abspath = abspath,
+}
+end
+end
+if os.type == "windows" then
+package.preload["texrunner.shellutil"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local string_gsub = string.gsub
+
+-- s: string
+local function escape(s)
+ return '"' .. string_gsub(string_gsub(s, '(\\*)"', '%1%1\\"'), '(\\+)$', '%1%1') .. '"'
+end
+
+
+return {
+ escape = escape,
+}
+end
+else
+package.preload["texrunner.shellutil"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local assert = assert
+local string_match = string.match
+local table = table
+local table_insert = table.insert
+local table_concat = table.concat
+
+-- s: string
+local function escape(s)
+ local len = #s
+ local result = {}
+ local t,i = string_match(s, "^([^']*)()")
+ assert(t)
+ if t ~= "" then
+ table_insert(result, "'")
+ table_insert(result, t)
+ table_insert(result, "'")
+ end
+ while i < len do
+ t,i = string_match(s, "^('+)()", i)
+ assert(t)
+ table_insert(result, '"')
+ table_insert(result, t)
+ table_insert(result, '"')
+ t,i = string_match(s, "^([^']*)()", i)
+ assert(t)
+ if t ~= "" then
+ table_insert(result, "'")
+ table_insert(result, t)
+ table_insert(result, "'")
+ end
+ end
+ return table_concat(result, "")
+end
+
+
+return {
+ escape = escape,
+}
+end
+end
+package.preload["texrunner.fsutil"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local assert = assert
+local os = os
+local os_execute = os.execute
+local os_remove = os.remove
+local filesys = require "lfs"
+local pathutil = require "texrunner.pathutil"
+local shellutil = require "texrunner.shellutil"
+local escape = shellutil.escape
+
+local copy_command
+if os.type == "windows" then
+ function copy_command(from, to)
+ -- TODO: What if `from` begins with a slash?
+ return "copy " .. escape(from) .. " " .. escape(to) .. " > NUL"
+ end
+else
+ function copy_command(from, to)
+ -- TODO: What if `from` begins with a hypen?
+ return "cp " .. escape(from) .. " " .. escape(to)
+ end
+end
+
+local isfile = filesys.isfile or function(path)
+ return filesys.attributes(path, "mode") == "file"
+end
+
+local isdir = filesys.isdir or function(path)
+ return filesys.attributes(path, "mode") == "directory"
+end
+
+local function mkdir_rec(path)
+ local succ, err = filesys.mkdir(path)
+ if not succ then
+ succ, err = mkdir_rec(pathutil.parentdir(path))
+ if succ then
+ return filesys.mkdir(path)
+ end
+ end
+ return succ, err
+end
+
+local function remove_rec(path)
+ if isdir(path) then
+ for file in filesys.dir(path) do
+ if file ~= "." and file ~= ".." then
+ local succ, err = remove_rec(pathutil.join(path, file))
+ if not succ then
+ return succ, err
+ end
+ end
+ end
+ return filesys.rmdir(path)
+ else
+ return os_remove(path)
+ end
+end
+
+return {
+ copy_command = copy_command,
+ isfile = isfile,
+ isdir = isdir,
+ mkdir_rec = mkdir_rec,
+ remove_rec = remove_rec,
+}
+end
+package.preload["texrunner.option"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+-- options_and_params, i = parseoption(arg, options)
+-- options[i] = {short = "o", long = "option" [, param = true] [, boolean = true] [, allow_single_hyphen = false]}
+-- arg[i], arg[i + 1], ..., arg[#arg] are non-options
+local function parseoption(arg, options)
+ local i = 1
+ local option_and_params = {}
+ while i <= #arg do
+ if arg[i] == "--" then
+ -- Stop handling options
+ i = i + 1
+ break
+ elseif arg[i]:sub(1,2) == "--" then
+ -- Long option
+ local name,param = arg[i]:match("^([^=]+)=(.*)$", 3)
+ name = name or arg[i]:sub(3)
+ local opt = nil
+ for _,o in ipairs(options) do
+ if o.long then
+ if o.long == name then
+ if o.param then
+ if param then
+ -- --option=param
+ else
+ if o.default ~= nil then
+ param = o.default
+ else
+ -- --option param
+ assert(i + 1 <= #arg, "argument missing after " .. arg[i] .. " option")
+ param = arg[i + 1]
+ i = i + 1
+ end
+ end
+ else
+ -- --option
+ param = true
+ end
+ opt = o
+ break
+ elseif o.boolean and name == "no-" .. o.long then
+ -- --no-option
+ opt = o
+ break
+ end
+ end
+ end
+ if opt then
+ table.insert(option_and_params, {opt.long, param})
+ else
+ -- Unknown long option
+ error("unknown long option: " .. arg[i])
+ end
+ elseif arg[i]:sub(1,1) == "-" then
+ local name,param = arg[i]:match("^([^=]+)=(.*)$", 2)
+ name = name or arg[i]:sub(2)
+ local opt = nil
+ for _,o in ipairs(options) do
+ if o.long and o.allow_single_hyphen then
+ if o.long == name then
+ if o.param then
+ if param then
+ -- -option=param
+ else
+ if o.default ~= nil then
+ param = o.default
+ else
+ -- -option param
+ assert(i + 1 <= #arg, "argument missing after " .. arg[i] .. " option")
+ param = arg[i + 1]
+ i = i + 1
+ end
+ end
+ else
+ -- -option
+ param = true
+ end
+ opt = o
+ break
+ elseif o.boolean and name == "no-" .. o.long then
+ -- -no-option
+ opt = o
+ break
+ end
+ elseif o.long and #name >= 2 and (o.long == name or (o.boolean and name == "no-" .. o.long)) then
+ error("You must supply two hyphens (i.e. --" .. name .. ") for long option")
+ end
+ end
+ if opt == nil then
+ -- Short option
+ name = arg[i]:sub(2,2)
+ for _,o in ipairs(options) do
+ if o.short then
+ if o.short == name then
+ if o.param then
+ if #arg[i] > 2 then
+ -- -oparam
+ param = arg[i]:sub(3)
+ else
+ -- -o param
+ assert(i + 1 <= #arg, "argument missing after " .. arg[i] .. " option")
+ param = arg[i + 1]
+ i = i + 1
+ end
+ else
+ -- -o
+ assert(#arg[i] == 2, "combining multiple short options like -abc is not supported")
+ param = true
+ end
+ opt = o
+ break
+ end
+ end
+ end
+ end
+ if opt then
+ table.insert(option_and_params, {opt.long or opt.short, param})
+ else
+ error("unknown short option: " .. arg[i])
+ end
+ else
+ -- arg[i] is not an option
+ break
+ end
+ i = i + 1
+ end
+ return option_and_params, i
+end
+
+return {
+ parseoption = parseoption;
+}
+end
+package.preload["texrunner.tex_engine"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local table = table
+local setmetatable = setmetatable
+local ipairs = ipairs
+
+local shellutil = require "texrunner.shellutil"
+
+--[[
+engine.name: string
+engine.type = "onePass" or "twoPass"
+engine:build_command(inputfile, options)
+ options:
+ halt_on_error: boolean
+ interaction: string
+ file_line_error: boolean
+ synctex: string
+ shell_escape: boolean
+ shell_restricted: boolean
+ jobname: string
+ output_directory: string
+ extraoptions: a list of strings
+ output_format: "pdf" or "dvi"
+ draftmode: boolean (pdfTeX / XeTeX / LuaTeX)
+ fmt: string
+ tex_injection: string
+ lua_initialization_script: string (LuaTeX only)
+engine.executable: string
+engine.supports_pdf_generation: boolean
+engine.dvi_extension: string
+engine.supports_draftmode: boolean
+engine.is_luatex: true or nil
+]]
+
+local engine_meta = {}
+engine_meta.__index = engine_meta
+engine_meta.dvi_extension = "dvi"
+function engine_meta:build_command(inputfile, options)
+ local command = {self.executable, "-recorder"}
+ if options.fmt then
+ table.insert(command, "-fmt=" .. options.fmt)
+ end
+ if options.halt_on_error then
+ table.insert(command, "-halt-on-error")
+ end
+ if options.interaction then
+ table.insert(command, "-interaction=" .. options.interaction)
+ end
+ if options.file_line_error then
+ table.insert(command, "-file-line-error")
+ end
+ if options.synctex then
+ table.insert(command, "-synctex=" .. shellutil.escape(options.synctex))
+ end
+ if options.shell_escape == false then
+ table.insert(command, "-no-shell-escape")
+ elseif options.shell_restricted == true then
+ table.insert(command, "-shell-restricted")
+ elseif options.shell_escape == true then
+ table.insert(command, "-shell-escape")
+ end
+ if options.jobname then
+ table.insert(command, "-jobname=" .. shellutil.escape(options.jobname))
+ end
+ if options.output_directory then
+ table.insert(command, "-output-directory=" .. shellutil.escape(options.output_directory))
+ end
+ if self.handle_additional_options then
+ self:handle_additional_options(command, options)
+ end
+ if options.extraoptions then
+ for _,v in ipairs(options.extraoptions) do
+ table.insert(command, v)
+ end
+ end
+ if type(options.tex_injection) == "string" then
+ table.insert(command, shellutil.escape(options.tex_injection .. "\\input " .. inputfile)) -- TODO: what if filename contains spaces?
+ else
+ table.insert(command, shellutil.escape(inputfile))
+ end
+ return table.concat(command, " ")
+end
+
+local function engine(name, supports_pdf_generation, handle_additional_options)
+ return setmetatable({
+ name = name,
+ executable = name,
+ supports_pdf_generation = supports_pdf_generation,
+ handle_additional_options = handle_additional_options,
+ supports_draftmode = supports_pdf_generation,
+ }, engine_meta)
+end
+
+local function handle_pdftex_options(self, args, options)
+ if options.draftmode then
+ table.insert(args, "-draftmode")
+ elseif options.output_format == "dvi" then
+ table.insert(args, "-output-format=dvi")
+ end
+end
+
+local function handle_xetex_options(self, args, options)
+ if options.output_format == "dvi" or options.draftmode then
+ table.insert(args, "-no-pdf")
+ end
+end
+
+local function handle_luatex_options(self, args, options)
+ if options.lua_initialization_script then
+ table.insert(args, "--lua="..shellutil.escape(options.lua_initialization_script))
+ end
+ handle_pdftex_options(self, args, options)
+end
+
+local function is_luatex(e)
+ e.is_luatex = true
+ return e
+end
+
+local KnownEngines = {
+ ["pdftex"] = engine("pdftex", true, handle_pdftex_options),
+ ["pdflatex"] = engine("pdflatex", true, handle_pdftex_options),
+ ["luatex"] = is_luatex(engine("luatex", true, handle_luatex_options)),
+ ["lualatex"] = is_luatex(engine("lualatex", true, handle_luatex_options)),
+ ["luajittex"] = is_luatex(engine("luajittex", true, handle_luatex_options)),
+ ["xetex"] = engine("xetex", true, handle_xetex_options),
+ ["xelatex"] = engine("xelatex", true, handle_xetex_options),
+ ["tex"] = engine("tex", false),
+ ["etex"] = engine("etex", false),
+ ["latex"] = engine("latex", false),
+ ["ptex"] = engine("ptex", false),
+ ["eptex"] = engine("eptex", false),
+ ["platex"] = engine("platex", false),
+ ["uptex"] = engine("uptex", false),
+ ["euptex"] = engine("euptex", false),
+ ["uplatex"] = engine("uplatex", false),
+}
+
+KnownEngines["xetex"].dvi_extension = "xdv"
+KnownEngines["xelatex"].dvi_extension = "xdv"
+
+return KnownEngines
+end
+package.preload["texrunner.reruncheck"] = function(...)
+--[[
+ Copyright 2016,2018 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local io = io
+local assert = assert
+local filesys = require "lfs"
+local md5 = require "md5"
+local fsutil = require "texrunner.fsutil"
+local pathutil = require "texrunner.pathutil"
+local message = require "texrunner.message"
+
+local function md5sum_file(path)
+ local f = assert(io.open(path, "rb"))
+ local contents = f:read("*a")
+ f:close()
+ return md5.sum(contents)
+end
+
+-- filelist, filemap = parse_recorder_file("jobname.fls", options [, filelist, filemap])
+-- filelist[i] = {path = "...", abspath = "...", kind = "input" or "output" or "auxiliary"}
+local function parse_recorder_file(file, options, filelist, filemap)
+ filelist = filelist or {}
+ filemap = filemap or {}
+ for l in io.lines(file) do
+ local t,path = l:match("^(%w+) (.*)$")
+ if t == "PWD" then
+ -- Ignore
+
+ elseif t == "INPUT" then
+ local abspath = pathutil.abspath(path)
+ local fileinfo = filemap[abspath]
+ if not fileinfo then
+ if fsutil.isfile(path) then
+ local kind = "input"
+ local ext = pathutil.ext(path)
+ if ext == "bbl" then
+ kind = "auxiliary"
+ end
+ fileinfo = {path = path, abspath = abspath, kind = kind}
+ table.insert(filelist, fileinfo)
+ filemap[abspath] = fileinfo
+ else
+ -- Maybe a command execution
+ end
+ else
+ if #path < #fileinfo.path then
+ fileinfo.path = path
+ end
+ if fileinfo.kind == "output" then
+ -- The files listed in both INPUT and OUTPUT are considered to be auxiliary files.
+ fileinfo.kind = "auxiliary"
+ end
+ end
+
+ elseif t == "OUTPUT" then
+ local abspath = pathutil.abspath(path)
+ local fileinfo = filemap[abspath]
+ if not fileinfo then
+ local kind = "output"
+ local ext = pathutil.ext(path)
+ if ext == "out" then
+ -- hyperref bookmarks file
+ kind = "auxiliary"
+ elseif options.makeindex and ext == "idx" then
+ -- Treat .idx files (to be processed by MakeIndex) as auxiliary
+ kind = "auxiliary"
+ -- ...and .ind files
+ elseif ext == "bcf" then -- biber
+ kind = "auxiliary"
+ elseif ext == "glo" then -- makeglossaries
+ kind = "auxiliary"
+ end
+ fileinfo = {path = path, abspath = abspath, kind = kind}
+ table.insert(filelist, fileinfo)
+ filemap[abspath] = fileinfo
+ else
+ if #path < #fileinfo.path then
+ fileinfo.path = path
+ end
+ if fileinfo.kind == "input" then
+ -- The files listed in both INPUT and OUTPUT are considered to be auxiliary files.
+ fileinfo.kind = "auxiliary"
+ end
+ end
+
+ else
+ message.warning("Unrecognized line in recorder file '", file, "': ", l)
+ end
+ end
+ return filelist, filemap
+end
+
+-- auxstatus = collectfileinfo(filelist [, auxstatus])
+local function collectfileinfo(filelist, auxstatus)
+ auxstatus = auxstatus or {}
+ for i,fileinfo in ipairs(filelist) do
+ local path = fileinfo.abspath
+ if fsutil.isfile(path) then
+ local status = auxstatus[path] or {}
+ auxstatus[path] = status
+ if fileinfo.kind == "input" then
+ status.mtime = status.mtime or filesys.attributes(path, "modification")
+ elseif fileinfo.kind == "auxiliary" then
+ status.mtime = status.mtime or filesys.attributes(path, "modification")
+ status.size = status.size or filesys.attributes(path, "size")
+ status.md5sum = status.md5sum or md5sum_file(path)
+ end
+ end
+ end
+ return auxstatus
+end
+
+local function binarytohex(s)
+ return (s:gsub(".", function(c) return string.format("%02x", string.byte(c)) end))
+end
+
+-- should_rerun, newauxstatus = comparefileinfo(auxfiles, auxstatus)
+local function comparefileinfo(filelist, auxstatus)
+ local should_rerun = false
+ local newauxstatus = {}
+ for i,fileinfo in ipairs(filelist) do
+ local path = fileinfo.abspath
+ if fsutil.isfile(path) then
+ if fileinfo.kind == "input" then
+ -- Input file: User might have modified while running TeX.
+ local mtime = filesys.attributes(path, "modification")
+ if auxstatus[path] and auxstatus[path].mtime then
+ if auxstatus[path].mtime < mtime then
+ -- Input file was updated during execution
+ message.info("Input file '", fileinfo.path, "' was modified (by user, or some external commands).")
+ newauxstatus[path] = {mtime = mtime}
+ return true, newauxstatus
+ end
+ else
+ -- New input file
+ end
+
+ elseif fileinfo.kind == "auxiliary" then
+ -- Auxiliary file: Compare file contents.
+ if auxstatus[path] then
+ -- File was touched during execution
+ local really_modified = false
+ local modified_because = nil
+ local size = filesys.attributes(path, "size")
+ if auxstatus[path].size ~= size then
+ really_modified = true
+ if auxstatus[path].size then
+ modified_because = string.format("size: %d -> %d", auxstatus[path].size, size)
+ else
+ modified_because = string.format("size: (N/A) -> %d", size)
+ end
+ newauxstatus[path] = {size = size}
+ else
+ local md5sum = md5sum_file(path)
+ if auxstatus[path].md5sum ~= md5sum then
+ really_modified = true
+ if auxstatus[path].md5sum then
+ modified_because = string.format("md5: %s -> %s", binarytohex(auxstatus[path].md5sum), binarytohex(md5sum))
+ else
+ modified_because = string.format("md5: (N/A) -> %s", binarytohex(md5sum))
+ end
+ end
+ newauxstatus[path] = {size = size, md5sum = md5sum}
+ end
+ if really_modified then
+ message.info("File '", fileinfo.path, "' was modified (", modified_because, ").")
+ should_rerun = true
+ else
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("File '", fileinfo.path, "' unmodified (size and md5sum).")
+ end
+ end
+ else
+ -- New file
+ if path:sub(-4) == ".aux" then
+ local size = filesys.attributes(path, "size")
+ if size == 8 then
+ local auxfile = io.open(path, "rb")
+ local contents = auxfile:read("*a")
+ auxfile:close()
+ if contents == "\\relax \n" then
+ -- The .aux file is new, but it is almost empty
+ else
+ should_rerun = true
+ end
+ newauxstatus[path] = {size = size, md5sum = md5.sum(contents)}
+ else
+ should_rerun = true
+ newauxstatus[path] = {size = size}
+ end
+ else
+ should_rerun = true
+ end
+ if should_rerun then
+ message.info("New auxiliary file '", fileinfo.path, "'.")
+ else
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("Ignoring almost-empty auxiliary file '", fileinfo.path, "'.")
+ end
+ end
+ end
+ if should_rerun then
+ break
+ end
+ end
+ else
+ -- Auxiliary file is not really a file???
+ end
+ end
+ return should_rerun, newauxstatus
+end
+
+-- true if src is newer than dst
+local function comparefiletime(srcpath, dstpath, auxstatus)
+ if not filesys.isfile(dstpath) then
+ return true
+ end
+ local src_info = auxstatus[srcpath]
+ if src_info then
+ local src_mtime = src_info.mtime
+ if src_mtime then
+ local dst_mtime = filesys.attributes(dstpath, "modification")
+ return src_mtime > dst_mtime
+ end
+ end
+ return false
+end
+
+return {
+ parse_recorder_file = parse_recorder_file;
+ collectfileinfo = collectfileinfo;
+ comparefileinfo = comparefileinfo;
+ comparefiletime = comparefiletime;
+}
+end
+package.preload["texrunner.auxfile"] = function(...)
+--[[
+ Copyright 2016 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local string_match = string.match
+local pathutil = require "texrunner.pathutil"
+local filesys = require "lfs"
+local fsutil = require "texrunner.fsutil"
+local message = require "texrunner.message"
+
+-- for LaTeX
+local function parse_aux_file(auxfile, outdir, report, seen)
+ report = report or {}
+ seen = seen or {}
+ seen[auxfile] = true
+ for l in io.lines(auxfile) do
+ local subauxfile = string_match(l, "\\@input{(.+)}")
+ if subauxfile then
+ if fsutil.isfile(subauxfile) then
+ parse_aux_file(pathutil.join(outdir, subauxfile), outdir, report, seen)
+ else
+ local dir = pathutil.join(outdir, pathutil.dirname(subauxfile))
+ if not fsutil.isdir(dir) then
+ assert(fsutil.mkdir_rec(dir))
+ report.made_new_directory = true
+ end
+ end
+ end
+ end
+ return report
+end
+
+-- \citation, \bibdata, \bibstyle and \@input
+local function extract_bibtex_from_aux_file(auxfile, outdir, biblines)
+ biblines = biblines or {}
+ for l in io.lines(auxfile) do
+ local name = string_match(l, "\\([%a@]+)")
+ if name == "citation" or name == "bibdata" or name == "bibstyle" then
+ table.insert(biblines, l)
+ if CLUTTEX_VERBOSITY >= 2 then
+ message.info("BibTeX line: ", l)
+ end
+ elseif name == "@input" then
+ local subauxfile = string_match(l, "\\@input{(.+)}")
+ if subauxfile and fsutil.isfile(subauxfile) then
+ extract_bibtex_from_aux_file(pathutil.join(outdir, subauxfile), outdir, biblines)
+ end
+ end
+ end
+ return biblines
+end
+
+return {
+ parse_aux_file = parse_aux_file,
+ extract_bibtex_from_aux_file = extract_bibtex_from_aux_file,
+}
+end
+package.preload["texrunner.luatexinit"] = function(...)
+local function create_initialization_script(filename, options)
+ local initscript = assert(io.open(filename,"w"))
+ if type(options.file_line_error) == "boolean" then
+ initscript:write(string.format("texconfig.file_line_error = %s\n", options.file_line_error))
+ end
+ if type(options.halt_on_error) == "boolean" then
+ initscript:write(string.format("texconfig.halt_on_error = %s\n", options.halt_on_error))
+ end
+ initscript:write([==[
+local print = print
+local io_open = io.open
+local io_write = io.write
+local os_execute = os.execute
+local texio_write = texio.write
+local texio_write_nl = texio.write_nl
+]==])
+
+ -- Packages coded in Lua doesn't follow -output-directory option and doesn't write command to the log file
+ initscript:write(string.format("local output_directory = %q\n", options.output_directory))
+ initscript:write([==[
+local luawritelog
+local function openluawritelog()
+ if not luawritelog then
+ luawritelog = assert(io_open(output_directory .. "/" .. tex.jobname .. ".cluttex-fls", "w"))
+ end
+ return luawritelog
+end
+io.open = function(fname, mode)
+ -- luatexja-ruby
+ if mode == "w" and fname == tex.jobname .. ".ltjruby" then
+ fname = output_directory .. "/" .. fname
+ end
+ if type(mode) == "string" and string.find(mode, "w") ~= nil then
+ -- write mode
+ openluawritelog():write("OUTPUT " .. fname .. "\n")
+ end
+ return io_open(fname, mode)
+end
+os.execute = function(...)
+ texio_write_nl("log", string.format("CLUTTEX_EXEC %s", ...), "")
+ return os_execute(...)
+end
+]==])
+
+ -- Silence some of the TeX output to the terminal.
+ initscript:write([==[
+local function start_file_cb(category, filename)
+ if category == 1 then -- a normal data file, like a TeX source
+ texio_write_nl("log", "("..filename)
+ elseif category == 2 then -- a font map coupling font names to resources
+ texio_write("log", "{"..filename)
+ elseif category == 3 then -- an image file (png, pdf, etc)
+ texio_write("<"..filename)
+ elseif category == 4 then -- an embedded font subset
+ texio_write("<"..filename)
+ elseif category == 5 then -- a fully embedded font
+ texio_write("<<"..filename)
+ else
+ print("start_file: unknown category", category, filename)
+ end
+end
+callback.register("start_file", start_file_cb)
+local function stop_file_cb(category)
+ if category == 1 then
+ texio_write("log", ")")
+ elseif category == 2 then
+ texio_write("log", "}")
+ elseif category == 3 then
+ texio_write(">")
+ elseif category == 4 then
+ texio_write(">")
+ elseif category == 5 then
+ texio_write(">>")
+ else
+ print("stop_file: unknown category", category)
+ end
+end
+callback.register("stop_file", stop_file_cb)
+texio.write = function(...)
+ if select("#",...) == 1 then
+ -- Suppress luaotfload's message (See src/fontloader/runtime/fontload-reference.lua)
+ local s = ...
+ if string.match(s, "^%(using cache: ")
+ or string.match(s, "^%(using write cache: ")
+ or string.match(s, "^%(using read cache: ")
+ or string.match(s, "^%(load luc: ")
+ or string.match(s, "^%(load cache: ") then
+ return texio_write("log", ...)
+ end
+ end
+ return texio_write(...)
+end
+]==])
+ initscript:close()
+end
+
+return {
+ create_initialization_script = create_initialization_script
+}
+end
+package.preload["texrunner.recovery"] = function(...)
+--[[
+ Copyright 2018 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local io = io
+local string = string
+local parse_aux_file = require "texrunner.auxfile".parse_aux_file
+local pathutil = require "texrunner.pathutil"
+local fsutil = require "texrunner.fsutil"
+local shellutil = require "texrunner.shellutil"
+local message = require "texrunner.message"
+
+local function create_missing_directories(args)
+ if string.find(args.execlog, "I can't write on file", 1, true) then
+ -- There is a possibility that there are some subfiles under subdirectories.
+ -- Directories for sub-auxfiles are not created automatically, so we need to provide them.
+ local report = parse_aux_file(args.auxfile, args.options.output_directory)
+ if report.made_new_directory then
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("Created missing directories.")
+ end
+ return true
+ end
+ end
+ return false
+end
+
+local function run_epstopdf(args)
+ local run = false
+ if args.options.shell_escape ~= false then -- (possibly restricted) \write18 enabled
+ for outfile, infile in string.gmatch(args.execlog, "%(epstopdf%)%s*Command: <r?epstopdf %-%-outfile=([%w%-/]+%.pdf) ([%w%-/]+%.eps)>") do
+ local infile_abs = pathutil.abspath(infile, args.original_wd)
+ if fsutil.isfile(infile_abs) then -- input file exists
+ local outfile_abs = pathutil.abspath(outfile, args.options.output_directory)
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("Running epstopdf on ", infile, ".")
+ end
+ local outdir = pathutil.dirname(outfile_abs)
+ if not fsutil.isdir(outdir) then
+ assert(fsutil.mkdir_rec(outdir))
+ end
+ local command = string.format("epstopdf --outfile=%s %s", shellutil.escape(outfile_abs), shellutil.escape(infile_abs))
+ message.exec(command)
+ local success = os.execute(command)
+ if type(success) == "number" then -- Lua 5.1 or LuaTeX
+ success = success == 0
+ end
+ run = run or success
+ end
+ end
+ end
+ return run
+end
+
+local function check_minted(args)
+ return string.find(args.execlog, "Package minted Error: Missing Pygments output; \\inputminted was") ~= nil
+end
+
+local function try_recovery(args)
+ local recovered = false
+ recovered = create_missing_directories(args)
+ recovered = run_epstopdf(args) or recovered
+ recovered = check_minted(args) or recovered
+ return recovered
+end
+
+return {
+ create_missing_directories = create_missing_directories,
+ run_epstopdf = run_epstopdf,
+ try_recovery = try_recovery,
+}
+end
+package.preload["texrunner.handleoption"] = function(...)
+local COPYRIGHT_NOTICE = [[
+Copyright (C) 2016,2018 ARATA Mizuki
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local pathutil = require "texrunner.pathutil"
+local shellutil = require "texrunner.shellutil"
+local parseoption = require "texrunner.option".parseoption
+local KnownEngines = require "texrunner.tex_engine"
+local message = require "texrunner.message"
+
+local function usage(arg)
+ io.write(string.format([[
+ClutTeX: Process TeX files without cluttering your working directory
+
+Usage:
+ %s [options] [--] FILE.tex
+
+Options:
+ -e, --engine=ENGINE Specify which TeX engine to use.
+ ENGINE is one of the following:
+ pdflatex, pdftex,
+ lualatex, luatex, luajittex,
+ xelatex, xetex, latex, etex, tex,
+ platex, eptex, ptex,
+ uplatex, euptex, uptex,
+ -o, --output=FILE The name of output file.
+ [default: JOBNAME.pdf or JOBNAME.dvi]
+ --fresh Clean intermediate files before running TeX.
+ Cannot be used with --output-directory.
+ --max-iterations=N Maximum number of running TeX to resolve
+ cross-references. [default: 3]
+ --start-with-draft Start with draft mode.
+ --[no-]change-directory Change directory before running TeX.
+ --watch Watch input files for change. Requires fswatch
+ program to be installed.
+ --tex-option=OPTION Pass OPTION to TeX as a single option.
+ --tex-options=OPTIONs Pass OPTIONs to TeX as multiple options.
+ --dvipdfmx-option[s]=OPTION[s] Same for dvipdfmx.
+ --makeindex=COMMAND+OPTIONs Command to generate index, such as
+ `makeindex' or `mendex'.
+ --bibtex=COMMAND+OPTIONs Command for BibTeX, such as
+ `bibtex' or `pbibtex'.
+ --biber[=COMMAND+OPTIONs] Command for Biber.
+ --makeglossaries[=COMMAND+OPTIONs] Command for makeglossaries.
+ -h, --help Print this message and exit.
+ -v, --version Print version information and exit.
+ -V, --verbose Be more verbose.
+ --color=WHEN Make ClutTeX's message colorful. WHEN is one of
+ `always', `auto', or `never'. [default: auto]
+ --includeonly=NAMEs Insert '\includeonly{NAMEs}'.
+
+ --[no-]shell-escape
+ --shell-restricted
+ --synctex=NUMBER
+ --fmt=FMTNAME
+ --[no-]file-line-error [default: yes]
+ --[no-]halt-on-error [default: yes]
+ --interaction=STRING [default: nonstopmode]
+ --jobname=STRING
+ --output-directory=DIR [default: somewhere in the temporary directory]
+ --output-format=FORMAT FORMAT is `pdf' or `dvi'. [default: pdf]
+
+%s
+]], arg[0] or 'texlua cluttex.lua', COPYRIGHT_NOTICE))
+end
+
+local option_spec = {
+ -- Options for ClutTeX
+ {
+ short = "e",
+ long = "engine",
+ param = true,
+ },
+ {
+ short = "o",
+ long = "output",
+ param = true,
+ },
+ {
+ long = "fresh",
+ },
+ {
+ long = "max-iterations",
+ param = true,
+ },
+ {
+ long = "start-with-draft",
+ },
+ {
+ long = "change-directory",
+ boolean = true,
+ },
+ {
+ long = "watch",
+ },
+ {
+ short = "h",
+ long = "help",
+ allow_single_hyphen = true,
+ },
+ {
+ short = "v",
+ long = "version",
+ },
+ {
+ short = "V",
+ long = "verbose",
+ },
+ {
+ long = "color",
+ param = true,
+ default = "always",
+ },
+ {
+ long = "includeonly",
+ param = true,
+ },
+ -- Options for TeX
+ {
+ long = "synctex",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "file-line-error",
+ boolean = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "interaction",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "halt-on-error",
+ boolean = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "shell-escape",
+ boolean = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "shell-restricted",
+ allow_single_hyphen = true,
+ },
+ {
+ long = "jobname",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "fmt",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "output-directory",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "output-format",
+ param = true,
+ allow_single_hyphen = true,
+ },
+ {
+ long = "tex-option",
+ param = true,
+ },
+ {
+ long = "tex-options",
+ param = true,
+ },
+ {
+ long = "dvipdfmx-option",
+ param = true,
+ },
+ {
+ long = "dvipdfmx-options",
+ param = true,
+ },
+ {
+ long = "makeindex",
+ param = true,
+ },
+ {
+ long = "bibtex",
+ param = true,
+ },
+ {
+ long = "biber",
+ param = true,
+ default = "biber",
+ },
+ {
+ long = "makeglossaries",
+ param = true,
+ default = "makeglossaries",
+ },
+}
+
+-- Default values for options
+local function set_default_values(options)
+ if options.max_iterations == nil then
+ options.max_iterations = 3
+ end
+
+ if options.interaction == nil then
+ options.interaction = "nonstopmode"
+ end
+
+ if options.file_line_error == nil then
+ options.file_line_error = true
+ end
+
+ if options.halt_on_error == nil then
+ options.halt_on_error = true
+ end
+end
+
+-- inputfile, engine, options = handle_cluttex_options(arg)
+local function handle_cluttex_options(arg)
+ -- Parse options
+ local option_and_params, non_option_index = parseoption(arg, option_spec)
+
+ -- Handle options
+ local options = {
+ tex_extraoptions = {},
+ dvipdfmx_extraoptions = {},
+ }
+ CLUTTEX_VERBOSITY = 0
+ for _,option in ipairs(option_and_params) do
+ local name = option[1]
+ local param = option[2]
+
+ if name == "engine" then
+ assert(options.engine == nil, "multiple --engine options")
+ options.engine = param
+
+ elseif name == "output" then
+ assert(options.output == nil, "multiple --output options")
+ options.output = param
+
+ elseif name == "fresh" then
+ assert(options.fresh == nil, "multiple --fresh options")
+ options.fresh = true
+
+ elseif name == "max-iterations" then
+ assert(options.max_iterations == nil, "multiple --max-iterations options")
+ options.max_iterations = assert(tonumber(param), "invalid value for --max-iterations option")
+ assert(options.max_iterations >= 1, "invalid value for --max-iterations option")
+
+ elseif name == "start-with-draft" then
+ assert(options.start_with_draft == nil, "multiple --start-with-draft options")
+ options.start_with_draft = true
+
+ elseif name == "watch" then
+ assert(options.watch == nil, "multiple --watch options")
+ options.watch = true
+
+ elseif name == "help" then
+ usage(arg)
+ os.exit(0)
+
+ elseif name == "version" then
+ io.stderr:write("cluttex ",CLUTTEX_VERSION,"\n")
+ os.exit(0)
+
+ elseif name == "verbose" then
+ CLUTTEX_VERBOSITY = CLUTTEX_VERBOSITY + 1
+
+ elseif name == "color" then
+ assert(options.color == nil, "multiple --collor options")
+ options.color = param
+ message.set_colors(options.color)
+
+ elseif name == "change-directory" then
+ assert(options.change_directory == nil, "multiple --change-directory options")
+ options.change_directory = param
+
+ elseif name == "includeonly" then
+ assert(options.includeonly == nil, "multiple --includeonly options")
+ options.includeonly = param
+
+ -- Options for TeX
+ elseif name == "synctex" then
+ assert(options.synctex == nil, "multiple --synctex options")
+ options.synctex = param
+
+ elseif name == "file-line-error" then
+ options.file_line_error = param
+
+ elseif name == "interaction" then
+ assert(options.interaction == nil, "multiple --interaction options")
+ assert(param == "batchmode" or param == "nonstopmode" or param == "scrollmode" or param == "errorstopmode", "invalid argument for --interaction")
+ options.interaction = param
+
+ elseif name == "halt-on-error" then
+ options.halt_on_error = param
+
+ elseif name == "shell-escape" then
+ assert(options.shell_escape == nil and options.shell_restricted == nil, "multiple --(no-)shell-escape or --shell-restricted options")
+ options.shell_escape = param
+
+ elseif name == "shell-restricted" then
+ assert(options.shell_escape == nil and options.shell_restricted == nil, "multiple --(no-)shell-escape or --shell-restricted options")
+ options.shell_restricted = true
+
+ elseif name == "jobname" then
+ assert(options.jobname == nil, "multiple --jobname options")
+ options.jobname = param
+
+ elseif name == "fmt" then
+ assert(options.fmt == nil, "multiple --fmt options")
+ options.fmt = param
+
+ elseif name == "output-directory" then
+ assert(options.output_directory == nil, "multiple --output-directory options")
+ options.output_directory = param
+
+ elseif name == "output-format" then
+ assert(options.output_format == nil, "multiple --output-format options")
+ assert(param == "pdf" or param == "dvi", "invalid argument for --output-format")
+ options.output_format = param
+
+ elseif name == "tex-option" then
+ table.insert(options.tex_extraoptions, shellutil.escape(param))
+
+ elseif name == "tex-options" then
+ table.insert(options.tex_extraoptions, param)
+
+ elseif name == "dvipdfmx-option" then
+ table.insert(options.dvipdfmx_extraoptions, shellutil.escape(param))
+
+ elseif name == "dvipdfmx-options" then
+ table.insert(options.dvipdfmx_extraoptions, param)
+
+ elseif name == "makeindex" then
+ assert(options.makeindex == nil, "multiple --makeindex options")
+ options.makeindex = param
+
+ elseif name == "bibtex" then
+ assert(options.bibtex == nil, "multiple --bibtex options")
+ assert(options.biber == nil, "multiple --bibtex/--biber options")
+ options.bibtex = param
+
+ elseif name == "biber" then
+ assert(options.biber == nil, "multiple --biber options")
+ assert(options.bibtex == nil, "multiple --bibtex/--biber options")
+ options.biber = param
+
+ elseif name == "makeglossaries" then
+ assert(options.makeglossaries == nil, "multiple --makeglossaries options")
+ options.makeglossaries = param
+
+ end
+ end
+
+ if options.color == nil then
+ message.set_colors("auto")
+ end
+
+ -- Handle non-options (i.e. input file)
+ if non_option_index > #arg then
+ -- No input file given
+ usage(arg)
+ os.exit(1)
+ elseif non_option_index < #arg then
+ message.error("Multiple input files are not supported.")
+ os.exit(1)
+ end
+ local inputfile = arg[non_option_index]
+
+ -- If run as 'cllualatex', then the default engine is lualatex
+ if options.engine == nil and type(arg[0]) == "string" then
+ local basename = pathutil.trimext(pathutil.basename(arg[0]))
+ local engine_part = string.match(basename, "^cl(%w+)$")
+ if engine_part and KnownEngines[engine_part] then
+ options.engine = engine_part
+ end
+ end
+
+ if options.engine == nil then
+ message.error("Engine not specified.")
+ os.exit(1)
+ end
+ local engine = KnownEngines[options.engine]
+ if not engine then
+ message.error("Unknown engine name '", options.engine, "'.")
+ os.exit(1)
+ end
+
+ set_default_values(options)
+
+ return inputfile, engine, options
+end
+
+return {
+ usage = usage,
+ handle_cluttex_options = handle_cluttex_options,
+}
+end
+package.preload["texrunner.isatty"] = function(...)
+--[[
+ Copyright 2018 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+if os.type == "unix" then
+ -- Try luaposix
+ local succ, M = pcall(function()
+ local isatty = require "posix.unistd".isatty
+ local fileno = require "posix.stdio".fileno
+ return {
+ isatty = function(file)
+ return isatty(fileno(file)) == 1
+ end,
+ }
+ end)
+ if succ then
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: isatty found via luaposix\n")
+ end
+ return M
+ else
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: luaposix not found: ", M, "\n")
+ end
+ end
+
+ -- Try LuaJIT-like FFI
+ local succ, M = pcall(function()
+ local ffi = require "ffi"
+ ffi.cdef[[
+int isatty(int fd);
+int fileno(void *stream);
+]]
+ local isatty = assert(ffi.C.isatty, "isatty not found")
+ local fileno = assert(ffi.C.fileno, "fileno not found")
+ return {
+ isatty = function(file)
+ -- LuaJIT converts Lua's file handles into FILE* (void*)
+ return isatty(fileno(file)) ~= 0
+ end
+ }
+ end)
+ if succ then
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: isatty found via FFI (Unix)\n")
+ end
+ return M
+ else
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: FFI (Unix) not found: ", M, "\n")
+ end
+ end
+
+else
+ -- Try LuaJIT
+ -- TODO: Try to detect MinTTY using GetFileInformationByHandleEx
+ local succ, M = pcall(function()
+ local ffi = require "ffi"
+ local bitlib = assert(bit32 or bit, "Neither bit32 (Lua 5.2) nor bit (LuaJIT) found") -- Lua 5.2 or LuaJIT
+ ffi.cdef[[
+int _isatty(int fd);
+int _fileno(void *stream);
+void *_get_osfhandle(int fd); // should return intptr_t
+typedef int BOOL;
+typedef uint32_t DWORD;
+typedef int FILE_INFO_BY_HANDLE_CLASS; // ???
+typedef struct _FILE_NAME_INFO {
+DWORD FileNameLength;
+uint16_t FileName[?];
+} FILE_NAME_INFO;
+DWORD GetFileType(void *hFile);
+BOOL GetFileInformationByHandleEx(void *hFile, FILE_INFO_BY_HANDLE_CLASS fic, void *fileinfo, DWORD dwBufferSize);
+BOOL GetConsoleMode(void *hConsoleHandle, DWORD* lpMode);
+BOOL SetConsoleMode(void *hConsoleHandle, DWORD dwMode);
+]]
+ local isatty = assert(ffi.C._isatty, "_isatty not found")
+ local fileno = assert(ffi.C._fileno, "_fileno not found")
+ local get_osfhandle = assert(ffi.C._get_osfhandle, "_get_osfhandle not found")
+ local GetFileType = assert(ffi.C.GetFileType, "GetFileType not found")
+ local GetFileInformationByHandleEx = assert(ffi.C.GetFileInformationByHandleEx, "GetFileInformationByHandleEx not found")
+ local GetConsoleMode = assert(ffi.C.GetConsoleMode, "GetConsoleMode not found")
+ local SetConsoleMode = assert(ffi.C.SetConsoleMode, "SetConsoleMode not found")
+ local function wide_to_narrow(array, length)
+ local t = {}
+ for i = 0, length - 1 do
+ table.insert(t, string.char(math.min(array[i], 0xff)))
+ end
+ return table.concat(t, "")
+ end
+ local function is_mintty(fd)
+ local handle = get_osfhandle(fd)
+ local filetype = GetFileType(handle)
+ if filetype ~= 0x0003 then -- not FILE_TYPE_PIPE (0x0003)
+ -- mintty must be a pipe
+ if CLUTTEX_VERBOSITY >= 4 then
+ io.stderr:write("ClutTeX: not a pipe\n")
+ end
+ return false
+ end
+ local nameinfo = ffi.new("FILE_NAME_INFO", 32768)
+ local FileNameInfo = 2 -- : FILE_INFO_BY_HANDLE_CLASS
+ if GetFileInformationByHandleEx(handle, FileNameInfo, nameinfo, ffi.sizeof("FILE_NAME_INFO", 32768)) ~= 0 then
+ local filename = wide_to_narrow(nameinfo.FileName, math.floor(nameinfo.FileNameLength / 2))
+ -- \(cygwin|msys)-<hex digits>-pty<N>-(from|to)-master
+ if CLUTTEX_VERBOSITY >= 4 then
+ io.stderr:write("ClutTeX: GetFileInformationByHandleEx returned ", filename, "\n")
+ end
+ local a, b = string.match(filename, "^\\(%w+)%-%x+%-pty%d+%-(%w+)%-master$")
+ if (a == "cygwin" or a == "msys") and (b == "from" or b == "to") then
+ return true
+ end
+ else
+ if CLUTTEX_VERBOSITY >= 4 then
+ io.stderr:write("ClutTeX: GetFileInformationByHandleEx failed\n")
+ end
+ end
+ return false
+ end
+ return {
+ isatty = function(file)
+ -- LuaJIT converts Lua's file handles into FILE* (void*)
+ local fd = fileno(file)
+ return isatty(fd) ~= 0 or is_mintty(fd)
+ end,
+ enable_console_colors = function(file)
+ local fd = fileno(file)
+ if isatty(fd) ~= 0 then
+ local handle = get_osfhandle(fd)
+ local modePtr = ffi.new("DWORD[1]")
+ local result = GetConsoleMode(handle, modePtr)
+ if result ~= 0 then
+ local ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
+ result = SetConsoleMode(handle, bitlib.bor(modePtr[0], ENABLE_VIRTUAL_TERMINAL_PROCESSING))
+ if result == 0 then
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: SetConsoleMode failed\n")
+ end
+ end
+ else
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: GetConsoleMode failed\n")
+ end
+ end
+ end
+ end,
+ }
+ end)
+ if succ then
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: isatty found via FFI (Windows)\n")
+ end
+ return M
+ else
+ if CLUTTEX_VERBOSITY >= 3 then
+ io.stderr:write("ClutTeX: FFI (Windows) not found: ", M, "\n")
+ end
+ end
+end
+
+return {
+ isatty = function(file)
+ return false
+ end,
+}
+end
+package.preload["texrunner.message"] = function(...)
+--[[
+ Copyright 2018 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local use_colors = false
+
+local function set_colors(mode)
+ local M
+ if mode == "always" then
+ use_colors = true
+ M = require "texrunner.isatty"
+ if M.enable_console_colors then
+ M.enable_console_colors(io.stderr)
+ end
+ elseif mode == "never" then
+ use_colors = false
+ elseif mode == "auto" then
+ M = require "texrunner.isatty"
+ use_colors = M.isatty(io.stderr)
+ else
+ error "The value of --color option must be one of 'auto', 'always', or 'never'."
+ end
+ if use_colors and M.enable_console_colors then
+ M.enable_console_colors(io.stderr)
+ end
+end
+
+-- ESCAPE: hex 1B = dec 27 = oct 33
+
+local CMD = {
+ reset = "\027[0m",
+ underline = "\027[4m",
+ fg_black = "\027[30m",
+ fg_red = "\027[31m",
+ fg_green = "\027[32m",
+ fg_yellow = "\027[33m",
+ fg_blue = "\027[34m",
+ fg_magenta = "\027[35m",
+ fg_cyan = "\027[36m",
+ fg_white = "\027[37m",
+ fg_reset = "\027[39m",
+ bg_black = "\027[40m",
+ bg_red = "\027[41m",
+ bg_green = "\027[42m",
+ bg_yellow = "\027[43m",
+ bg_blue = "\027[44m",
+ bg_magenta = "\027[45m",
+ bg_cyan = "\027[46m",
+ bg_white = "\027[47m",
+ bg_reset = "\027[49m",
+ fg_x_black = "\027[90m",
+ fg_x_red = "\027[91m",
+ fg_x_green = "\027[92m",
+ fg_x_yellow = "\027[93m",
+ fg_x_blue = "\027[94m",
+ fg_x_magenta = "\027[95m",
+ fg_x_cyan = "\027[96m",
+ fg_x_white = "\027[97m",
+ bg_x_black = "\027[100m",
+ bg_x_red = "\027[101m",
+ bg_x_green = "\027[102m",
+ bg_x_yellow = "\027[103m",
+ bg_x_blue = "\027[104m",
+ bg_x_magenta = "\027[105m",
+ bg_x_cyan = "\027[106m",
+ bg_x_white = "\027[107m",
+}
+
+local function exec_msg(commandline)
+ if use_colors then
+ io.stderr:write(CMD.fg_x_white, CMD.bg_red, "[EXEC]", CMD.reset, " ", CMD.fg_red, commandline, CMD.reset, "\n")
+ else
+ io.stderr:write("[EXEC] ", commandline, "\n")
+ end
+end
+
+local function error_msg(...)
+ local message = table.concat({...}, "")
+ if use_colors then
+ io.stderr:write(CMD.fg_x_white, CMD.bg_red, "[ERROR]", CMD.reset, " ", CMD.fg_red, message, CMD.reset, "\n")
+ else
+ io.stderr:write("[ERROR] ", message, "\n")
+ end
+end
+
+local function warn_msg(...)
+ local message = table.concat({...}, "")
+ if use_colors then
+ io.stderr:write(CMD.fg_x_white, CMD.bg_red, "[WARN]", CMD.reset, " ", CMD.fg_blue, message, CMD.reset, "\n")
+ else
+ io.stderr:write("[WARN] ", message, "\n")
+ end
+end
+
+local function diag_msg(...)
+ local message = table.concat({...}, "")
+ if use_colors then
+ io.stderr:write(CMD.fg_x_white, CMD.bg_red, "[DIAG]", CMD.reset, " ", CMD.fg_blue, message, CMD.reset, "\n")
+ else
+ io.stderr:write("[DIAG] ", message, "\n")
+ end
+end
+
+local function info_msg(...)
+ local message = table.concat({...}, "")
+ if use_colors then
+ io.stderr:write(CMD.fg_x_white, CMD.bg_red, "[INFO]", CMD.reset, " ", CMD.fg_magenta, message, CMD.reset, "\n")
+ else
+ io.stderr:write("[INFO] ", message, "\n")
+ end
+end
+
+return {
+ set_colors = set_colors,
+ exec = exec_msg,
+ error = error_msg,
+ warn = warn_msg,
+ diag = diag_msg,
+ info = info_msg,
+}
+end
+--[[
+ Copyright 2016,2018 ARATA Mizuki
+
+ This file is part of ClutTeX.
+
+ ClutTeX is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ ClutTeX is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+CLUTTEX_VERSION = "v0.1"
+
+-- Standard libraries
+local coroutine = coroutine
+local tostring = tostring
+
+-- External libraries (included in texlua)
+local filesys = require "lfs"
+local md5 = require "md5"
+-- local kpse = require "kpse"
+
+-- My own modules
+local pathutil = require "texrunner.pathutil"
+local fsutil = require "texrunner.fsutil"
+local shellutil = require "texrunner.shellutil"
+local reruncheck = require "texrunner.reruncheck"
+local luatexinit = require "texrunner.luatexinit"
+local recoverylib = require "texrunner.recovery"
+local message = require "texrunner.message"
+local extract_bibtex_from_aux_file = require "texrunner.auxfile".extract_bibtex_from_aux_file
+local handle_cluttex_options = require "texrunner.handleoption".handle_cluttex_options
+
+-- arguments: input file name, jobname, etc...
+local function genOutputDirectory(...)
+ -- The name of the temporary directory is based on the path of input file.
+ local message = table.concat({...}, "\0")
+ local hash = md5.sumhexa(message)
+ local tmpdir = os.getenv("TMPDIR") or os.getenv("TMP") or os.getenv("TEMP")
+ if tmpdir == nil then
+ local home = os.getenv("HOME") or os.getenv("USERPROFILE") or error("environment variable 'TMPDIR' not set!")
+ tmpdir = pathutil.join(home, ".latex-build-temp")
+ end
+ return pathutil.join(tmpdir, 'latex-build-' .. hash)
+end
+
+local inputfile, engine, options = handle_cluttex_options(arg)
+
+local jobname = options.jobname or pathutil.basename(pathutil.trimext(inputfile))
+assert(jobname ~= "", "jobname cannot be empty")
+
+if options.output_format == nil then
+ options.output_format = "pdf"
+end
+local output_extension
+if options.output_format == "dvi" then
+ output_extension = engine.dvi_extension or "dvi"
+else
+ output_extension = "pdf"
+end
+
+if options.output == nil then
+ options.output = jobname .. "." .. output_extension
+end
+
+-- Prepare output directory
+if options.output_directory == nil then
+ local inputfile_abs = pathutil.abspath(inputfile)
+ options.output_directory = genOutputDirectory(inputfile_abs, jobname, options.engine)
+
+ if not fsutil.isdir(options.output_directory) then
+ assert(fsutil.mkdir_rec(options.output_directory))
+
+ elseif options.fresh then
+ -- The output directory exists and --fresh is given:
+ -- Remove all files in the output directory
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("Cleaning '", options.output_directory, "'...")
+ end
+ assert(fsutil.remove_rec(options.output_directory))
+ assert(filesys.mkdir(options.output_directory))
+ end
+
+elseif options.fresh then
+ message.error("--fresh and --output-directory cannot be used together.")
+ os.exit(1)
+end
+
+local pathsep = ":"
+if os.type == "windows" then
+ pathsep = ";"
+end
+
+local original_wd = filesys.currentdir()
+if options.change_directory then
+ local TEXINPUTS = os.getenv("TEXINPUTS") or ""
+ filesys.chdir(options.output_directory)
+ options.output = pathutil.abspath(options.output, original_wd)
+ os.setenv("TEXINPUTS", original_wd .. pathsep .. TEXINPUTS)
+end
+if options.bibtex or options.biber then
+ local BIBINPUTS = os.getenv("BIBINPUTS") or ""
+ options.output = pathutil.abspath(options.output, original_wd)
+ os.setenv("BIBINPUTS", original_wd .. pathsep .. BIBINPUTS)
+end
+
+-- Set `max_print_line' environment variable if not already set.
+if os.getenv("max_print_line") == nil then
+ os.setenv("max_print_line", "65536")
+end
+-- TODO: error_line, half_error_line
+--[[
+ According to texmf.cnf:
+ 45 < error_line < 255,
+ 30 < half_error_line < error_line - 15,
+ 60 <= max_print_line.
+]]
+
+local function path_in_output_directory(ext)
+ return pathutil.join(options.output_directory, jobname .. "." .. ext)
+end
+
+local recorderfile = path_in_output_directory("fls")
+local recorderfile2 = path_in_output_directory("cluttex-fls")
+
+local tex_options = {
+ interaction = options.interaction,
+ file_line_error = options.file_line_error,
+ halt_on_error = options.halt_on_error,
+ synctex = options.synctex,
+ output_directory = options.output_directory,
+ shell_escape = options.shell_escape,
+ shell_restricted = options.shell_restricted,
+ jobname = options.jobname,
+ fmt = options.fmt,
+ extraoptions = options.tex_extraoptions,
+}
+if options.output_format ~= "pdf" and engine.supports_pdf_generation then
+ tex_options.output_format = options.output_format
+end
+
+-- Setup LuaTeX initialization script
+if engine.is_luatex then
+ local initscriptfile = path_in_output_directory("cluttexinit.lua")
+ luatexinit.create_initialization_script(initscriptfile, tex_options)
+ tex_options.lua_initialization_script = initscriptfile
+end
+
+-- Run TeX command (*tex, *latex)
+-- should_rerun, newauxstatus = single_run([auxstatus])
+-- This function should be run in a coroutine.
+local function single_run(auxstatus, iteration)
+ local minted = false
+ local bibtex_aux_hash = nil
+ local mainauxfile = path_in_output_directory("aux")
+ if fsutil.isfile(recorderfile) then
+ -- Recorder file already exists
+ local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options)
+ if engine.is_luatex and fsutil.isfile(recorderfile2) then
+ filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap)
+ end
+ auxstatus = reruncheck.collectfileinfo(filelist, auxstatus)
+ for _,fileinfo in ipairs(filelist) do
+ if string.match(fileinfo.path, "minted/minted%.sty$") then
+ minted = true
+ break
+ end
+ end
+ if options.bibtex then
+ local biblines = extract_bibtex_from_aux_file(mainauxfile, options.output_directory)
+ if #biblines > 0 then
+ bibtex_aux_hash = md5.sum(table.concat(biblines, "\n"))
+ end
+ end
+ else
+ -- This is the first execution
+ if auxstatus ~= nil then
+ message.error("Recorder file was not generated during the execution!")
+ os.exit(1)
+ end
+ auxstatus = {}
+ end
+ --local timestamp = os.time()
+
+ if options.includeonly then
+ tex_options.tex_injection = string.format("%s\\includeonly{%s}", tex_options.tex_injection or "", options.includeonly)
+ end
+
+ if minted and not (tex_options.tex_injection and string.find(tex_options.tex_injection,"minted") == nil) then
+ tex_options.tex_injection = string.format("%s\\PassOptionsToPackage{outputdir=%s}{minted}", tex_options.tex_injection or "", options.output_directory)
+ end
+
+ local current_tex_options, lightweight_mode = tex_options, false
+ if iteration == 1 and options.start_with_draft then
+ current_tex_options = {}
+ for k,v in pairs(tex_options) do
+ current_tex_options[k] = v
+ end
+ if engine.supports_draftmode then
+ current_tex_options.draftmode = true
+ options.start_with_draft = false
+ end
+ current_tex_options.interaction = "batchmode"
+ lightweight_mode = true
+ else
+ current_tex_options.draftmode = false
+ end
+
+ local command = engine:build_command(inputfile, current_tex_options)
+
+ local execlog -- the contents of .log file
+
+ local recovered = false
+ local function recover()
+ -- Check log file
+ if not execlog then
+ local logfile = assert(io.open(path_in_output_directory("log")))
+ execlog = logfile:read("*a")
+ logfile:close()
+ end
+ recovered = recoverylib.try_recovery{
+ execlog = execlog,
+ auxfile = path_in_output_directory("aux"),
+ options = options,
+ original_wd = original_wd,
+ }
+ return recovered
+ end
+ coroutine.yield(command, recover) -- Execute the command
+ if recovered then
+ return true, {}
+ end
+
+ local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options)
+ if engine.is_luatex and fsutil.isfile(recorderfile2) then
+ filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap)
+ end
+
+ if not execlog then
+ local logfile = assert(io.open(path_in_output_directory("log")))
+ execlog = logfile:read("*a")
+ logfile:close()
+ end
+
+ if options.makeindex then
+ -- Look for .idx files and run MakeIndex
+ for _,file in ipairs(filelist) do
+ if pathutil.ext(file.path) == "idx" then
+ -- Run makeindex if the .idx file is new or updated
+ local idxfileinfo = {path = file.path, abspath = file.abspath, kind = "auxiliary"}
+ local output_ind = pathutil.replaceext(file.abspath, "ind")
+ if reruncheck.comparefileinfo({idxfileinfo}, auxstatus) or reruncheck.comparefiletime(file.abspath, output_ind, auxstatus) then
+ local idx_dir = pathutil.dirname(file.abspath)
+ local makeindex_command = {
+ "cd", shellutil.escape(idx_dir), "&&",
+ options.makeindex, -- Do not escape options.makeindex to allow additional options
+ "-o", pathutil.basename(output_ind),
+ pathutil.basename(file.abspath)
+ }
+ coroutine.yield(table.concat(makeindex_command, " "))
+ table.insert(filelist, {path = output_ind, abspath = output_ind, kind = "auxiliary"})
+ else
+ local succ, err = filesys.touch(output_ind)
+ if not succ then
+ message.warn("Failed to touch " .. output_ind .. " (" .. err .. ")")
+ end
+ end
+ end
+ end
+ else
+ -- Check log file
+ if string.find(execlog, "No file [^\n]+%.ind%.") then
+ message.diag("You may want to use --makeindex option.")
+ end
+ end
+
+ if options.makeglossaries then
+ -- Look for .glo files and run makeglossaries
+ for _,file in ipairs(filelist) do
+ if pathutil.ext(file.path) == "glo" then
+ -- Run makeglossaries if the .glo file is new or updated
+ local glofileinfo = {path = file.path, abspath = file.abspath, kind = "auxiliary"}
+ local output_gls = pathutil.replaceext(file.abspath, "gls")
+ if reruncheck.comparefileinfo({glofileinfo}, auxstatus) or reruncheck.comparefiletime(file.abspath, output_gls, auxstatus) then
+ local makeglossaries_command = {
+ options.makeglossaries,
+ "-d", shellutil.escape(options.output_directory),
+ pathutil.trimext(pathutil.basename(file.path))
+ }
+ coroutine.yield(table.concat(makeglossaries_command, " "))
+ table.insert(filelist, {path = output_gls, abspath = output_gls, kind = "auxiliary"})
+ else
+ local succ, err = filesys.touch(output_gls)
+ if not succ then
+ message.warn("Failed to touch " .. output_ind .. " (" .. err .. ")")
+ end
+ end
+ end
+ end
+ else
+ -- Check log file
+ if string.find(execlog, "No file [^\n]+%.gls%.") then
+ message.diag("You may want to use --makeglossaries option.")
+ end
+ end
+
+ if options.bibtex then
+ local biblines2 = extract_bibtex_from_aux_file(mainauxfile, options.output_directory)
+ local bibtex_aux_hash2
+ if #biblines2 > 0 then
+ bibtex_aux_hash2 = md5.sum(table.concat(biblines2, "\n"))
+ end
+ local output_bbl = path_in_output_directory("bbl")
+ if bibtex_aux_hash ~= bibtex_aux_hash2 or reruncheck.comparefiletime(mainauxfile, output_bbl, auxstatus) then
+ -- The input for BibTeX command has changed...
+ local bibtex_command = {
+ "cd", shellutil.escape(options.output_directory), "&&",
+ options.bibtex,
+ pathutil.basename(mainauxfile)
+ }
+ coroutine.yield(table.concat(bibtex_command, " "))
+ else
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("No need to run BibTeX.")
+ end
+ local succ, err = filesys.touch(output_bbl)
+ if not succ then
+ message.warn("Failed to touch " .. output_bbl .. " (" .. err .. ")")
+ end
+ end
+ elseif options.biber then
+ for _,file in ipairs(filelist) do
+ if pathutil.ext(file.path) == "bcf" then
+ -- Run biber if the .bcf file is new or updated
+ local bcffileinfo = {path = file.path, abspath = file.abspath, kind = "auxiliary"}
+ local output_bbl = pathutil.replaceext(file.abspath, "bbl")
+ if reruncheck.comparefileinfo({bcffileinfo}, auxstatus) or reruncheck.comparefiletime(file.abspath, output_bbl, auxstatus) then
+ local bbl_dir = pathutil.dirname(file.abspath)
+ local biber_command = {
+ options.biber, -- Do not escape options.biber to allow additional options
+ "--output-directory", shellutil.escape(options.output_directory),
+ pathutil.basename(file.abspath)
+ }
+ coroutine.yield(table.concat(biber_command, " "))
+ table.insert(filelist, {path = output_bbl, abspath = output_bbl, kind = "auxiliary"})
+ else
+ local succ, err = filesys.touch(output_bbl)
+ if not succ then
+ message.warn("Failed to touch " .. output_bbl .. " (" .. err .. ")")
+ end
+ end
+ end
+ end
+ else
+ -- Check log file
+ if string.find(execlog, "No file [^\n]+%.bbl%.") then
+ message.diag("You may want to use --bibtex or --biber option.")
+ end
+ end
+
+ if string.find(execlog, "No pages of output.") then
+ return "No pages of output."
+ end
+
+ local should_rerun, auxstatus = reruncheck.comparefileinfo(filelist, auxstatus)
+ return should_rerun or lightweight_mode, auxstatus
+end
+
+-- Run (La)TeX (possibly multiple times) and produce a PDF file.
+-- This function should be run in a coroutine.
+local function do_typeset_c()
+ local iteration = 0
+ local should_rerun, auxstatus
+ repeat
+ iteration = iteration + 1
+ should_rerun, auxstatus = single_run(auxstatus, iteration)
+ if should_rerun == "No pages of output." then
+ message.warn("No pages of output.")
+ return
+ end
+ until not should_rerun or iteration >= options.max_iterations
+
+ if should_rerun then
+ message.warn("LaTeX should be run once more.")
+ end
+
+ -- Successful
+ if options.output_format == "dvi" or engine.supports_pdf_generation then
+ -- Output file (DVI/PDF) is generated in the output directory
+ local outfile = path_in_output_directory(output_extension)
+ local oncopyerror
+ if os.type == "windows" then
+ oncopyerror = function()
+ message.error("Failed to copy file. Some applications may be locking the ", string.upper(options.output_format), " file.")
+ return false
+ end
+ end
+ coroutine.yield(fsutil.copy_command(outfile, options.output), oncopyerror)
+ if #options.dvipdfmx_extraoptions > 0 then
+ message.warn("--dvipdfmx-option[s] are ignored.")
+ end
+
+ else
+ -- DVI file is generated, but PDF file is wanted
+ local dvifile = path_in_output_directory("dvi")
+ local dvipdfmx_command = {"dvipdfmx", "-o", shellutil.escape(options.output)}
+ for _,v in ipairs(options.dvipdfmx_extraoptions) do
+ table.insert(dvipdfmx_command, v)
+ end
+ table.insert(dvipdfmx_command, shellutil.escape(dvifile))
+ coroutine.yield(table.concat(dvipdfmx_command, " "))
+ end
+
+ -- Copy SyncTeX file if necessary
+ if options.output_format == "pdf" then
+ local synctex = tonumber(options.synctex or "0")
+ local synctex_ext = nil
+ if synctex > 0 then
+ -- Compressed SyncTeX file (.synctex.gz)
+ synctex_ext = "synctex.gz"
+ elseif synctex < 0 then
+ -- Uncompressed SyncTeX file (.synctex)
+ synctex_ext = "synctex"
+ end
+ if synctex_ext then
+ coroutine.yield(fsutil.copy_command(path_in_output_directory(synctex_ext), pathutil.replaceext(options.output, synctex_ext)))
+ end
+ end
+end
+
+local function do_typeset()
+ -- Execute the command string yielded by do_typeset_c
+ for command, recover in coroutine.wrap(do_typeset_c) do
+ message.exec(command)
+ local success, termination, status_or_signal = os.execute(command)
+ if type(success) == "number" then -- Lua 5.1 or LuaTeX
+ local code = success
+ success = code == 0
+ termination = nil
+ status_or_signal = code
+ end
+ if not success and not (recover and recover()) then
+ if termination == "exit" then
+ message.error("Command exited abnormally: exit status ", tostring(status_or_signal))
+ elseif termination == "signal" then
+ message.error("Command exited abnormally: signal ", tostring(status_or_signal))
+ else
+ message.error("Command exited abnormally: ", tostring(status_or_signal))
+ end
+ return false, termination, status_or_signal
+ end
+ end
+ -- Successful
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.info("Command exited successfully")
+ end
+ return true
+end
+
+if options.watch then
+ -- Watch mode
+ local success, status = do_typeset()
+ local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options)
+ if engine.is_luatex and fsutil.isfile(recorderfile2) then
+ filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap)
+ end
+ local input_files_to_watch = {}
+ for _,fileinfo in ipairs(filelist) do
+ if fileinfo.kind == "input" then
+ table.insert(input_files_to_watch, fileinfo.abspath)
+ end
+ end
+ local fswatch_command = {"fswatch", "--event=Updated", "--"}
+ for _,path in ipairs(input_files_to_watch) do
+ table.insert(fswatch_command, shellutil.escape(path))
+ end
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.exec(table.concat(fswatch_command, " "))
+ end
+ local fswatch = assert(io.popen(table.concat(fswatch_command, " "), "r"))
+ for l in fswatch:lines() do
+ local found = false
+ for _,path in ipairs(input_files_to_watch) do
+ if l == path then
+ found = true
+ break
+ end
+ end
+ if found then
+ local success, status = do_typeset()
+ if not success then
+ -- Not successful
+ end
+ end
+ end
+
+else
+ -- Not in watch mode
+ local success, status = do_typeset()
+ if not success then
+ os.exit(1)
+ end
+end
diff --git a/Master/tlpkg/bin/tlpkg-ctan-check b/Master/tlpkg/bin/tlpkg-ctan-check
index 58854ceace7..8b44455c8d9 100755
--- a/Master/tlpkg/bin/tlpkg-ctan-check
+++ b/Master/tlpkg/bin/tlpkg-ctan-check
@@ -154,7 +154,7 @@ my @TLP_working = qw(
cite citeall citeref cje cjhebrew cjk cjk-gs-integrate cjk-ko cjkpunct
classics classpack classicthesis
cleanthesis clearsans clefval cleveref clipboard
- clock cloze clrdblpg clrscode clrscode3e clrstrip
+ clock cloze clrdblpg clrscode clrscode3e clrstrip cluttex
cm cm-lgc cm-mf-extra-bold cm-super cm-unicode
cmap cmarrows cmbright cmcyr
cmdstring cmdtrack cmexb cmextra cmll cmpica cmpj cmsd cmsrb cmtiup
diff --git a/Master/tlpkg/libexec/ctan2tds b/Master/tlpkg/libexec/ctan2tds
index 689f3d5d961..663daf84076 100755
--- a/Master/tlpkg/libexec/ctan2tds
+++ b/Master/tlpkg/libexec/ctan2tds
@@ -1300,6 +1300,7 @@ chomp ($Build = `cd $Master/../Build/source && pwd`);
'apa6' => '&PREHOOK_apa6',
'cals' => '&PREHOOK_cals',
'chess' => '&PREHOOK_chess',
+ 'cluttex' => '&PREHOOK_cluttex',
'cm' => '&PREHOOK_cm',
'cm-super' => '&PREHOOK_cm_super',
'cmextra' => '&PREHOOK_cmextra',
@@ -2933,6 +2934,7 @@ $standardttf = '\.ttf|\.TTC';
'checkcites' => '\.lua$',
'checklistings' => '\.sh$',
'cjk-gs-integrate' => '\.pl$',
+ 'cluttex' => 'cluttex\.lua$', # moved by prehook.
'convbkmk' => '\.rb$',
'crossrefware' => '\.pl$',
'ctan-o-mat' => 'ctan-o-mat$',
@@ -3812,9 +3814,14 @@ sub doscripts {
# Scripts with special cases.
# xx should merge with bin_links in
# Build/source/texk/texlive/linked_scripts/Makefile.am, sigh.
+ &SYSTEM ("ln -s $linkname $platdir/cllualatex")
+ if $linkname eq "cluttex"; # cluttex->cllualatex
+ &SYSTEM ("ln -s $linkname $platdir/clxelatex")
+ if $linkname eq "cluttex"; # cluttex->clxelatex
+ #
&SYSTEM ("ln -s $linkname $platdir/r$linkname")
if $linkname =~ /^(pdfcrop|epstopdf)$/; # rpdfcrop ->pdfcrop, ...
-
+ #
&SYSTEM ("ln -s $linkname $platdir/latexdef")
if $linkname eq "texdef"; # latexdef->texdef
@@ -3840,20 +3847,30 @@ sub doscripts {
&SYSTEM ("$MV $s.bat.noMiKTeX $scriptsdir");
# best to have wrapper also, maybe?
- } elsif ($s eq "lua2dox_filter" # package lua2dox
- || $s eq "ctan-o-mat") { # package ctan-o-mat
- # handwritten .bat
- &SYSTEM ("$MV $s.bat $platdir/");
+ } elsif ($s eq "cluttex.lua") {
+ # provided .bat
+ &SYSTEM ("$MV bin/cluttex.bat $platdir/");
+ # can't use our wrapper, I think, maybe copies will work.
+ &SYSTEM ("$CP $platdir/cluttex.bat $platdir/cllualatex.bat");
+ &SYSTEM ("$CP $platdir/cluttex.bat $platdir/clxelatex.bat");
+ &SYSTEM ("rmdir bin");
next; # no wrapper
} elsif ($s eq "latexindent.pl") {
# provided .exe (made with par::packer)
&SYSTEM ("$MV latexindent.exe $platdir/");
next; # no wrapper
+
+ } elsif ($s eq "lua2dox_filter" # package lua2dox
+ || $s eq "ctan-o-mat") { # package ctan-o-mat
+ # handwritten .bat
+ &SYSTEM ("$MV $s.bat $platdir/");
+ next; # no wrapper
}
- # xx here too should merge with bin_links in
- # Build/source/texk/texlive/linked_scripts/Makefile.am, sigh.
+ # xx here too, should merge with bin_links in
+ # Build/source/texk/texlive/linked_scripts/Makefile.am,
+ # or at least with above duplicated code.
my $w32_wrapper = "$Build/$build_tldir/w32_wrapper/runscript.exe";
&SYSTEM ("$CP $w32_wrapper $platdir/$linkname.exe");
&SYSTEM ("$CP $w32_wrapper $platdir/r$linkname.exe")
@@ -5316,6 +5333,11 @@ sub PREHOOK_chess {
&SYSTEM ("$RM -rf inputs pkfonts.zip");
}
+sub PREHOOK_cluttex {
+ print "PREHOOK_$package - mv */cluttex for specialscripts\n";
+ &SYSTEM ("$MV bin/cluttex cluttex.lua");
+}
+
sub PREHOOK_cm {
print "PREHOOK_$package - remove cmex9.mf\n";
&SYSTEM ("$RM cmex9.mf"); # because it's really an amsfonts file
diff --git a/Master/tlpkg/tlpsrc/cluttex.tlpsrc b/Master/tlpkg/tlpsrc/cluttex.tlpsrc
new file mode 100644
index 00000000000..e3b8d60755c
--- /dev/null
+++ b/Master/tlpkg/tlpsrc/cluttex.tlpsrc
@@ -0,0 +1,3 @@
+binpattern f bin/${ARCH}/${PKGNAME}
+binpattern f bin/${ARCH}/cllualatex
+binpattern f bin/${ARCH}/clxelatex
diff --git a/Master/tlpkg/tlpsrc/collection-binextra.tlpsrc b/Master/tlpkg/tlpsrc/collection-binextra.tlpsrc
index 2f65fc52adb..be53753ceae 100644
--- a/Master/tlpkg/tlpsrc/collection-binextra.tlpsrc
+++ b/Master/tlpkg/tlpsrc/collection-binextra.tlpsrc
@@ -15,6 +15,7 @@ depend bibtexu
depend bundledoc
depend checklistings
depend chktex
+depend cluttex
depend ctan-o-mat
depend ctan_chk
depend ctanbib