summaryrefslogtreecommitdiff
path: root/Master/texmf-dist/scripts/cluttex
diff options
context:
space:
mode:
authorKarl Berry <karl@freefriends.org>2019-04-30 22:36:50 +0000
committerKarl Berry <karl@freefriends.org>2019-04-30 22:36:50 +0000
commitb9d1e8848fb4f8e45796a706a0391734b0c88c7c (patch)
tree8e97fb607639f6bc1bf187b6a31cb76a4c53a5c8 /Master/texmf-dist/scripts/cluttex
parentf6aba0a218c3b19afb4ff78799e8f0760e584397 (diff)
cluttex (30apr19)
git-svn-id: svn://tug.org/texlive/trunk@50913 c570f23f-e606-0410-a88d-b1316a301751
Diffstat (limited to 'Master/texmf-dist/scripts/cluttex')
-rwxr-xr-xMaster/texmf-dist/scripts/cluttex/cluttex.lua532
1 files changed, 515 insertions, 17 deletions
diff --git a/Master/texmf-dist/scripts/cluttex/cluttex.lua b/Master/texmf-dist/scripts/cluttex/cluttex.lua
index dc622725bee..dfbd5c5694f 100755
--- a/Master/texmf-dist/scripts/cluttex/cluttex.lua
+++ b/Master/texmf-dist/scripts/cluttex/cluttex.lua
@@ -344,7 +344,7 @@ end
if os.type == "windows" then
package.preload["texrunner.shellutil"] = function(...)
--[[
- Copyright 2016 ARATA Mizuki
+ Copyright 2016,2019 ARATA Mizuki
This file is part of ClutTeX.
@@ -363,6 +363,7 @@ package.preload["texrunner.shellutil"] = function(...)
]]
local string_gsub = string.gsub
+local os_execute = os.execute
-- s: string
local function escape(s)
@@ -370,14 +371,21 @@ local function escape(s)
end
+local function has_command(name)
+ local result = os_execute("where " .. escape(name) .. " > NUL 2>&1")
+ -- Note that os.execute returns a number on Lua 5.1 or LuaTeX
+ return result == 0 or result == true
+end
+
return {
escape = escape,
+ has_command = has_command,
}
end
else
package.preload["texrunner.shellutil"] = function(...)
--[[
- Copyright 2016 ARATA Mizuki
+ Copyright 2016,2019 ARATA Mizuki
This file is part of ClutTeX.
@@ -400,6 +408,7 @@ local string_match = string.match
local table = table
local table_insert = table.insert
local table_concat = table.concat
+local os_execute = os.execute
-- s: string
local function escape(s)
@@ -430,8 +439,15 @@ local function escape(s)
end
+local function has_command(name)
+ local result = os_execute("which " .. escape(name) .. " > /dev/null")
+ -- Note that os.execute returns a number on Lua 5.1 or LuaTeX
+ return result == 0 or result == true
+end
+
return {
escape = escape,
+ has_command = has_command,
}
end
end
@@ -542,6 +558,7 @@ package.preload["texrunner.option"] = function(...)
-- options_and_params, i = parseoption(arg, options)
-- options[i] = {short = "o", long = "option" [, param = true] [, boolean = true] [, allow_single_hyphen = false]}
+-- options_and_params[j] = {"option", "value"}
-- arg[i], arg[i + 1], ..., arg[#arg] are non-options
local function parseoption(arg, options)
local i = 1
@@ -581,6 +598,7 @@ local function parseoption(arg, options)
elseif o.boolean and name == "no-" .. o.long then
-- --no-option
opt = o
+ param = false
break
end
end
@@ -620,6 +638,7 @@ local function parseoption(arg, options)
elseif o.boolean and name == "no-" .. o.long then
-- -no-option
opt = o
+ param = false
break
end
elseif o.long and #name >= 2 and (o.long == name or (o.boolean and name == "no-" .. o.long)) then
@@ -2124,6 +2143,423 @@ return {
info = info_msg,
}
end
+package.preload["texrunner.fswatcher_windows"] = function(...)
+--[[
+ Copyright 2019 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 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[[
+typedef int BOOL;
+typedef unsigned int UINT;
+typedef uint32_t DWORD;
+typedef void *HANDLE;
+typedef uintptr_t ULONG_PTR;
+typedef uint16_t WCHAR;
+typedef struct _OVERLAPPED {
+ ULONG_PTR Internal;
+ ULONG_PTR InternalHigh;
+ union {
+ struct {
+ DWORD Offset;
+ DWORD OffsetHigh;
+ };
+ void *Pointer;
+ };
+ HANDLE hEvent;
+} OVERLAPPED;
+typedef struct _FILE_NOTIFY_INFORMATION {
+ DWORD NextEntryOffset;
+ DWORD Action;
+ DWORD FileNameLength;
+ WCHAR FileName[?];
+} FILE_NOTIFY_INFORMATION;
+typedef void (__stdcall *LPOVERLAPPED_COMPLETION_ROUTINE)(DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered, OVERLAPPED *lpOverlapped);
+DWORD GetLastError();
+BOOL CloseHandle(HANDLE hObject);
+HANDLE CreateFileA(const char *lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, void *lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile);
+HANDLE CreateIoCompletionPort(HANDLE fileHandle, HANDLE existingCompletionPort, ULONG_PTR completionKey, DWORD numberOfConcurrentThreads);
+BOOL ReadDirectoryChangesW(HANDLE hDirectory, void *lpBuffer, DWORD nBufferLength, BOOL bWatchSubtree, DWORD dwNotifyFilter, DWORD *lpBytesReturned, OVERLAPPED *lpOverlapped, LPOVERLAPPED_COMPLETION_ROUTINE lpOverlappedCompletionRoutine);
+BOOL GetQueuedCompletionStatus(HANDLE CompletionPort, DWORD *lpNumberOfBytes, ULONG_PTR *lpCompletionKey, OVERLAPPED **lpOverlapped, DWORD dwMilliseconds);
+int MultiByteToWideChar(UINT CodePage, DWORD dwFlags, const char *lpMultiByteStr, int cbMultiByte, WCHAR *lpWideCharStr, int cchWideChar);
+int WideCharToMultiByte(UINT CodePage, DWORD dwFlags, const WCHAR *lpWideCharStr, int cchWideChar, char *lpMultiByteStr, int cbMultiByte, const char *lpDefaultChar, BOOL *lpUsedDefaultChar);
+DWORD GetFullPathNameA(const char *lpFileName, DWORD nBufferLength, char *lpBuffer, char **lpFilePart);
+uint64_t GetTickCount64();
+]]
+
+-- LuaTeX's FFI does not equate a null pointer with nil.
+-- On LuaJIT, ffi.NULL is just nil.
+local NULL = ffi.NULL
+
+-- GetLastError
+local ERROR_FILE_NOT_FOUND = 0x0002
+local ERROR_PATH_NOT_FOUND = 0x0003
+local ERROR_ACCESS_DENIED = 0x0005
+local ERROR_INVALID_PARAMETER = 0x0057
+local ERROR_INSUFFICIENT_BUFFER = 0x007A
+local WAIT_TIMEOUT = 0x0102
+local ERROR_ABANDONED_WAIT_0 = 0x02DF
+local ERROR_NOACCESS = 0x03E6
+local ERROR_INVALID_FLAGS = 0x03EC
+local ERROR_NOTIFY_ENUM_DIR = 0x03FE
+local ERROR_NO_UNICODE_TRANSLATION = 0x0459
+local KnownErrors = {
+ [ERROR_FILE_NOT_FOUND] = "ERROR_FILE_NOT_FOUND",
+ [ERROR_PATH_NOT_FOUND] = "ERROR_PATH_NOT_FOUND",
+ [ERROR_ACCESS_DENIED] = "ERROR_ACCESS_DENIED",
+ [ERROR_INVALID_PARAMETER] = "ERROR_INVALID_PARAMETER",
+ [ERROR_INSUFFICIENT_BUFFER] = "ERROR_INSUFFICIENT_BUFFER",
+ [ERROR_ABANDONED_WAIT_0] = "ERROR_ABANDONED_WAIT_0",
+ [ERROR_NOACCESS] = "ERROR_NOACCESS",
+ [ERROR_INVALID_FLAGS] = "ERROR_INVALID_FLAGS",
+ [ERROR_NOTIFY_ENUM_DIR] = "ERROR_NOTIFY_ENUM_DIR",
+ [ERROR_NO_UNICODE_TRANSLATION] = "ERROR_NO_UNICODE_TRANSLATION",
+}
+
+-- CreateFile
+local FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
+local FILE_FLAG_OVERLAPPED = 0x40000000
+local OPEN_EXISTING = 3
+local FILE_SHARE_READ = 0x00000001
+local FILE_SHARE_WRITE = 0x00000002
+local FILE_SHARE_DELETE = 0x00000004
+local FILE_LIST_DIRECTORY = 0x1
+local INVALID_HANDLE_VALUE = ffi.cast("void *", -1)
+
+-- ReadDirectoryChangesW / FILE_NOTIFY_INFORMATION
+local FILE_NOTIFY_CHANGE_FILE_NAME = 0x00000001
+local FILE_NOTIFY_CHANGE_DIR_NAME = 0x00000002
+local FILE_NOTIFY_CHANGE_ATTRIBUTES = 0x00000004
+local FILE_NOTIFY_CHANGE_SIZE = 0x00000008
+local FILE_NOTIFY_CHANGE_LAST_WRITE = 0x00000010
+local FILE_NOTIFY_CHANGE_LAST_ACCESS = 0x00000020
+local FILE_NOTIFY_CHANGE_CREATION = 0x00000040
+local FILE_NOTIFY_CHANGE_SECURITY = 0x00000100
+local FILE_ACTION_ADDED = 0x00000001
+local FILE_ACTION_REMOVED = 0x00000002
+local FILE_ACTION_MODIFIED = 0x00000003
+local FILE_ACTION_RENAMED_OLD_NAME = 0x00000004
+local FILE_ACTION_RENAMED_NEW_NAME = 0x00000005
+
+-- WideCharToMultiByte / MultiByteToWideChar
+local CP_ACP = 0
+local CP_UTF8 = 65001
+
+local C = ffi.C
+
+local function format_error(name, lasterror, extra)
+ local errorname = KnownErrors[lasterror] or string.format("error code %d", lasterror)
+ if extra then
+ return string.format("%s failed with %s (0x%04x) [%s]", name, errorname, lasterror, extra)
+ else
+ return string.format("%s failed with %s (0x%04x)", name, errorname, lasterror)
+ end
+end
+local function wcs_to_mbs(wstr, wstrlen, codepage)
+ -- wstr: FFI uint16_t[?]
+ -- wstrlen: length of wstr, or -1 if NUL-terminated
+ if wstrlen == 0 then
+ return ""
+ end
+ codepage = codepage or CP_ACP
+ local dwFlags = 0
+ local result = C.WideCharToMultiByte(codepage, dwFlags, wstr, wstrlen, nil, 0, nil, nil)
+ if result <= 0 then
+ -- Failed
+ local lasterror = C.GetLastError()
+ -- Candidates: ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_FLAGS, ERROR_INVALID_PARAMETER, ERROR_NO_UNICODE_TRANSLATION
+ return nil, format_error("WideCharToMultiByte", lasterror)
+ end
+ local mbsbuf = ffi.new("char[?]", result)
+ result = C.WideCharToMultiByte(codepage, dwFlags, wstr, wstrlen, mbsbuf, result, nil, nil)
+ if result <= 0 then
+ -- Failed
+ local lasterror = C.GetLastError()
+ -- Candidates: ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_FLAGS, ERROR_INVALID_PARAMETER, ERROR_NO_UNICODE_TRANSLATION
+ return nil, format_error("WideCharToMultiByte", lasterror)
+ end
+ return ffi.string(mbsbuf, result)
+end
+local function mbs_to_wcs(str, codepage)
+ -- str: Lua string
+ if str == "" then
+ return ffi.new("WCHAR[0]")
+ end
+ codepage = codepage or CP_ACP
+ local dwFlags = 0
+ local result = C.MultiByteToWideChar(codepage, dwFlags, str, #str, nil, 0)
+ if result <= 0 then
+ local lasterror = C.GetLastError()
+ -- ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_FLAGS, ERROR_INVALID_PARAMETER, ERROR_NO_UNICODE_TRANSLATION
+ return nil, format_error("MultiByteToWideChar", lasterror)
+ end
+ local wcsbuf = ffi.new("WCHAR[?]", result)
+ result = C.MultiByteToWideChar(codepage, dwFlags, str, #str, wcsbuf, result)
+ if result <= 0 then
+ local lasterror = C.GetLastError()
+ return nil, format_error("MultiByteToWideChar", lasterror)
+ end
+ return wcsbuf, result
+end
+
+
+local function get_full_path_name(filename)
+ local bufsize = 1024
+ local buffer
+ local filePartPtr = ffi.new("char*[1]")
+ local result
+ repeat
+ buffer = ffi.new("char[?]", bufsize)
+ result = C.GetFullPathNameA(filename, bufsize, buffer, filePartPtr)
+ if result == 0 then
+ local lasterror = C.GetLastError()
+ return nil, format_error("GetFullPathNameA", lasterror, filename)
+ elseif bufsize < result then
+ -- result: buffer size required to hold the path + terminating NUL
+ bufsize = result
+ end
+ until result < bufsize
+ local fullpath = ffi.string(buffer, result)
+ local filePart = ffi.string(filePartPtr[0])
+ local dirPart = ffi.string(buffer, ffi.cast("intptr_t", filePartPtr[0]) - ffi.cast("intptr_t", buffer)) -- LuaTeX's FFI doesn't support pointer subtraction
+ return fullpath, filePart, dirPart
+end
+
+--[[
+ dirwatche.dirname : string
+ dirwatcher._rawhandle : cdata HANDLE
+ dirwatcher._overlapped : cdata OVERLAPPED
+ dirwatcher._buffer : cdata char[?]
+]]
+local dirwatcher_meta = {}
+dirwatcher_meta.__index = dirwatcher_meta
+function dirwatcher_meta:close()
+ if self._rawhandle ~= nil then
+ C.CloseHandle(ffi.gc(self._rawhandle, nil))
+ self._rawhandle = nil
+ end
+end
+local function open_directory(dirname)
+ local dwShareMode = bitlib.bor(FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_SHARE_DELETE)
+ local dwFlagsAndAttributes = bitlib.bor(FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OVERLAPPED)
+ local handle = C.CreateFileA(dirname, FILE_LIST_DIRECTORY, dwShareMode, nil, OPEN_EXISTING, dwFlagsAndAttributes, nil)
+ if handle == INVALID_HANDLE_VALUE then
+ local lasterror = C.GetLastError()
+ print("Failed to open "..dirname)
+ return nil, format_error("CreateFileA", lasterror, dirname)
+ end
+ return setmetatable({
+ dirname = dirname,
+ _rawhandle = ffi.gc(handle, C.CloseHandle),
+ _overlapped = ffi.new("OVERLAPPED"),
+ _buffer = ffi.new("char[?]", 1024),
+ }, dirwatcher_meta)
+end
+function dirwatcher_meta:start_watch(watchSubtree)
+ local dwNotifyFilter = bitlib.bor(FILE_NOTIFY_CHANGE_FILE_NAME, FILE_NOTIFY_CHANGE_DIR_NAME, FILE_NOTIFY_CHANGE_ATTRIBUTES, FILE_NOTIFY_CHANGE_SIZE, FILE_NOTIFY_CHANGE_LAST_WRITE, FILE_NOTIFY_CHANGE_LAST_ACCESS, FILE_NOTIFY_CHANGE_CREATION, FILE_NOTIFY_CHANGE_SECURITY)
+ local buffer = self._buffer
+ local bufferSize = ffi.sizeof(buffer)
+ local result = C.ReadDirectoryChangesW(self._rawhandle, buffer, bufferSize, watchSubtree, dwNotifyFilter, nil, self._overlapped, nil)
+ if result == 0 then
+ local lasterror = C.GetLastError()
+ return nil, format_error("ReadDirectoryChangesW", lasterror, self.dirname)
+ end
+ return true
+end
+local ActionTable = {
+ [FILE_ACTION_ADDED] = "added",
+ [FILE_ACTION_REMOVED] = "removed",
+ [FILE_ACTION_MODIFIED] = "modified",
+ [FILE_ACTION_RENAMED_OLD_NAME] = "rename_from",
+ [FILE_ACTION_RENAMED_NEW_NAME] = "rename_to",
+}
+function dirwatcher_meta:process(numberOfBytes)
+ -- self._buffer received `numberOfBytes` bytes
+ local buffer = self._buffer
+ numberOfBytes = math.min(numberOfBytes, ffi.sizeof(buffer))
+ local ptr = ffi.cast("char *", buffer)
+ local structSize = ffi.sizeof("FILE_NOTIFY_INFORMATION", 1)
+ local t = {}
+ while numberOfBytes >= structSize do
+ local notifyInfo = ffi.cast("FILE_NOTIFY_INFORMATION*", ptr)
+ local nextEntryOffset = notifyInfo.NextEntryOffset
+ local action = notifyInfo.Action
+ local fileNameLength = notifyInfo.FileNameLength
+ local fileName = notifyInfo.FileName
+ local u = { action = ActionTable[action], filename = wcs_to_mbs(fileName, fileNameLength / 2) }
+ table.insert(t, u)
+ if nextEntryOffset == 0 or numberOfBytes <= nextEntryOffset then
+ break
+ end
+ numberOfBytes = numberOfBytes - nextEntryOffset
+ ptr = ptr + nextEntryOffset
+ end
+ return t
+end
+
+--[[
+ watcher._rawport : cdata HANDLE
+ watcher._pending : array of {
+ action = ..., filename = ...
+ }
+ watcher._directories[dirname] = {
+ dir = directory watcher,
+ dirname = dirname,
+ files = { [filename] = user-supplied path } -- files to watch
+ }
+ watcher[i] = i-th directory (_directories[dirname] for some dirname)
+]]
+
+local fswatcher_meta = {}
+fswatcher_meta.__index = fswatcher_meta
+local function new_watcher()
+ local port = C.CreateIoCompletionPort(INVALID_HANDLE_VALUE, nil, 0, 0)
+ if port == NULL then
+ local lasterror = C.GetLastError()
+ return nil, format_error("CreateIoCompletionPort", lasterror)
+ end
+ return setmetatable({
+ _rawport = ffi.gc(port, C.CloseHandle), -- ?
+ _pending = {},
+ _directories = {},
+ }, fswatcher_meta)
+end
+local function add_directory(self, dirname)
+ local t = self._directories[dirname]
+ if not t then
+ local dirwatcher, err = open_directory(dirname)
+ if not dirwatcher then
+ return dirwatcher, err
+ end
+ t = { dirwatcher = dirwatcher, dirname = dirname, files = {} }
+ table.insert(self, t)
+ local i = #self
+ local result = C.CreateIoCompletionPort(dirwatcher._rawhandle, self._rawport, i, 0)
+ if result == NULL then
+ local lasterror = C.GetLastError()
+ return nil, format_error("CreateIoCompletionPort", lasterror, dirname)
+ end
+ self._directories[dirname] = t
+ local result, err = dirwatcher:start_watch(false)
+ if not result then
+ return result, err
+ end
+ end
+ return t
+end
+function fswatcher_meta:add_file(path, ...)
+ local fullpath, filename, dirname = get_full_path_name(path)
+ local t, err = add_directory(self, dirname)
+ if not t then
+ return t, err
+ end
+ t.files[filename] = path
+ return true
+end
+local INFINITE = 0xFFFFFFFF
+local function get_queued(self, timeout)
+ local startTime = C.GetTickCount64()
+ local timeout_ms
+ if timeout == nil then
+ timeout_ms = INFINITE
+ else
+ timeout_ms = timeout * 1000
+ end
+ local numberOfBytesPtr = ffi.new("DWORD[1]")
+ local completionKeyPtr = ffi.new("ULONG_PTR[1]")
+ local lpOverlapped = ffi.new("OVERLAPPED*[1]")
+ repeat
+ local result = C.GetQueuedCompletionStatus(self._rawport, numberOfBytesPtr, completionKeyPtr, lpOverlapped, timeout_ms)
+ if result == 0 then
+ local lasterror = C.GetLastError()
+ if lasterror == WAIT_TIMEOUT then
+ return nil, "timeout"
+ else
+ return nil, format_error("GetQueuedCompletionStatus", lasterror)
+ end
+ end
+ local numberOfBytes = numberOfBytesPtr[0]
+ local completionKey = tonumber(completionKeyPtr[0])
+ local dir_t = assert(self[completionKey], "invalid completion key: " .. tostring(completionKey))
+ local t = dir_t.dirwatcher:process(numberOfBytes)
+ dir_t.dirwatcher:start_watch(false)
+ local found = false
+ for i,v in ipairs(t) do
+ local path = dir_t.files[v.filename]
+ if path then
+ found = true
+ table.insert(self._pending, {path = path, action = v.action})
+ end
+ end
+ if found then
+ return true
+ end
+ if timeout_ms ~= INFINITE then
+ local tt = C.GetTickCount64()
+ timeout_ms = timeout_ms - (tt - startTime)
+ startTime = tt
+ end
+ until timeout_ms < 0
+ return nil, "timeout"
+end
+function fswatcher_meta:next(timeout)
+ if #self._pending > 0 then
+ local result = table.remove(self._pending, 1)
+ get_queued(self, 0) -- ignore error
+ return result
+ else
+ local result, err = get_queued(self, timeout)
+ if result == nil then
+ return nil, err
+ end
+ return table.remove(self._pending, 1)
+ end
+end
+function fswatcher_meta:close()
+ if self._rawport ~= nil then
+ for i,v in ipairs(self) do
+ v.dirwatcher:close()
+ end
+ C.CloseHandle(ffi.gc(self._rawport, nil))
+ self._rawport = nil
+ end
+end
+--[[
+local watcher = require("fswatcher_windows").new()
+assert(watcher:add_file("rdc-sync.c"))
+assert(watcher:add_file("sub2/hoge"))
+for i = 1, 10 do
+ local result, err = watcher:next(2)
+ if err == "timeout" then
+ print(os.date(), "timeout")
+ else
+ assert(result, err)
+ print(os.date(), result.path, result.action)
+ end
+end
+watcher:close()
+]]
+return {
+ new = new_watcher,
+}
+end
--[[
Copyright 2016,2018-2019 ARATA Mizuki
@@ -2143,7 +2579,7 @@ end
along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
]]
-CLUTTEX_VERSION = "v0.2"
+CLUTTEX_VERSION = "v0.3"
-- Standard libraries
local coroutine = coroutine
@@ -2165,6 +2601,8 @@ 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
+os.setlocale("", "ctype") -- Workaround for recent Universal CRT
+
-- arguments: input file name, jobname, etc...
local function genOutputDirectory(...)
-- The name of the temporary directory is based on the path of input file.
@@ -2611,6 +3049,7 @@ end
if options.watch then
-- Watch mode
local success, status = do_typeset()
+ -- TODO: filenames here can be UTF-8 if command_line_encoding=utf-8
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)
@@ -2621,28 +3060,87 @@ if options.watch 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, " "))
+ local fswatcherlib
+ if os.type == "windows" then
+ -- Windows: Try built-in filesystem watcher
+ local succ, result = pcall(require, "texrunner.fswatcher_windows")
+ if not succ and CLUTTEX_VERBOSITY >= 1 then
+ message.warn("Failed to load texrunner.fswatcher_windows: " .. result)
+ end
+ fswatcherlib = result
end
- local fswatch = assert(io.popen(table.concat(fswatch_command, " "), "r"))
- for l in fswatch:lines() do
- local found = false
+ if fswatcherlib then
+ if CLUTTEX_VERBOSITY >= 2 then
+ message.info("Using built-in filesystem watcher for Windows")
+ end
+ local watcher = assert(fswatcherlib.new())
for _,path in ipairs(input_files_to_watch) do
- if l == path then
- found = true
- break
- end
+ assert(watcher:add_file(path))
end
- if found then
+ while true do
+ local result = assert(watcher:next())
+ if CLUTTEX_VERBOSITY >= 2 then
+ message.info(string.format("%s %s"), result.action, result.path)
+ end
local success, status = do_typeset()
if not success then
-- Not successful
end
end
+ elseif shellutil.has_command("fswatch") then
+ local fswatch_command = {"fswatch", "--event=Updated", "--"}
+ for _,path in ipairs(input_files_to_watch) do
+ table.insert(fswatch_command, shellutil.escape(path))
+ end
+ local fswatch_command_str = table.concat(fswatch_command, " ")
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.exec(fswatch_command_str)
+ end
+ local fswatch = assert(io.popen(fswatch_command_str, "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
+ elseif shellutil.has_command("inotifywait") then
+ local inotifywait_command = {"inotifywait", "--monitor", "--event=modify", "--event=attrib", "--format=%w", "--quiet"}
+ for _,path in ipairs(input_files_to_watch) do
+ table.insert(inotifywait_command, shellutil.escape(path))
+ end
+ local inotifywait_command_str = table.concat(inotifywait_command, " ")
+ if CLUTTEX_VERBOSITY >= 1 then
+ message.exec(inotifywait_command_str)
+ end
+ local inotifywait = assert(io.popen(inotifywait_command_str, "r"))
+ for l in inotifywait: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
+ message.error("Could not watch files because neither `fswatch' nor `inotifywait' was installed.")
+ message.info("See ClutTeX's manual for details.")
+ os.exit(1)
end
else