summaryrefslogtreecommitdiff
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
parentf6aba0a218c3b19afb4ff78799e8f0760e584397 (diff)
cluttex (30apr19)
git-svn-id: svn://tug.org/texlive/trunk@50913 c570f23f-e606-0410-a88d-b1316a301751
-rwxr-xr-xBuild/source/texk/texlive/linked_scripts/cluttex/cluttex.lua532
-rw-r--r--Master/texmf-dist/doc/support/cluttex/CHANGELOG.md8
-rw-r--r--Master/texmf-dist/doc/support/cluttex/Makefile1
-rw-r--r--Master/texmf-dist/doc/support/cluttex/README.md2
-rwxr-xr-xMaster/texmf-dist/doc/support/cluttex/bin/cluttex.bat532
-rw-r--r--Master/texmf-dist/doc/support/cluttex/build.lua56
-rw-r--r--Master/texmf-dist/doc/support/cluttex/checkglobal.lua1
-rw-r--r--Master/texmf-dist/doc/support/cluttex/doc/manual-ja.pdfbin460819 -> 462174 bytes
-rw-r--r--Master/texmf-dist/doc/support/cluttex/doc/manual-ja.tex11
-rw-r--r--Master/texmf-dist/doc/support/cluttex/doc/manual.pdfbin164542 -> 166892 bytes
-rw-r--r--Master/texmf-dist/doc/support/cluttex/doc/manual.tex12
-rw-r--r--Master/texmf-dist/doc/support/cluttex/src/cluttex.lua92
-rw-r--r--Master/texmf-dist/doc/support/cluttex/src/texrunner/fswatcher_windows.lua423
-rw-r--r--Master/texmf-dist/doc/support/cluttex/src/texrunner/option.lua3
-rw-r--r--Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_unix.lua10
-rw-r--r--Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_windows.lua10
-rwxr-xr-xMaster/texmf-dist/scripts/cluttex/cluttex.lua532
17 files changed, 2132 insertions, 93 deletions
diff --git a/Build/source/texk/texlive/linked_scripts/cluttex/cluttex.lua b/Build/source/texk/texlive/linked_scripts/cluttex/cluttex.lua
index dc622725bee..dfbd5c5694f 100755
--- a/Build/source/texk/texlive/linked_scripts/cluttex/cluttex.lua
+++ b/Build/source/texk/texlive/linked_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
diff --git a/Master/texmf-dist/doc/support/cluttex/CHANGELOG.md b/Master/texmf-dist/doc/support/cluttex/CHANGELOG.md
index dc1d6b20e4c..c76e6189391 100644
--- a/Master/texmf-dist/doc/support/cluttex/CHANGELOG.md
+++ b/Master/texmf-dist/doc/support/cluttex/CHANGELOG.md
@@ -1,3 +1,11 @@
+Version 0.3 (2019-04-30)
+-----
+
+Changes:
+
+* Support other methods for watching file system: `inotifywait` for Linux and a built-in one for Windows.
+* Fix `--no-*` options.
+
Version 0.2 (2019-02-22)
-----
diff --git a/Master/texmf-dist/doc/support/cluttex/Makefile b/Master/texmf-dist/doc/support/cluttex/Makefile
index 778f0d3da7e..c588900ab23 100644
--- a/Master/texmf-dist/doc/support/cluttex/Makefile
+++ b/Master/texmf-dist/doc/support/cluttex/Makefile
@@ -19,6 +19,7 @@ sources= \
src/texrunner/handleoption.lua \
src/texrunner/isatty.lua \
src/texrunner/message.lua \
+ src/texrunner/fswatcher_windows.lua \
src/cluttex.lua
bin/cluttex: $(sources) build.lua
diff --git a/Master/texmf-dist/doc/support/cluttex/README.md b/Master/texmf-dist/doc/support/cluttex/README.md
index 1e0f792cd9a..67354e40eed 100644
--- a/Master/texmf-dist/doc/support/cluttex/README.md
+++ b/Master/texmf-dist/doc/support/cluttex/README.md
@@ -62,7 +62,7 @@ Command-line Options
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.
+ Requires [fswatch](http://emcrisostomo.github.io/fswatch/) program or `inotifywait` program to be installed on Unix systems.
* `--color[=WHEN]`
Make ClutTeX's message colorful.
`WHEN` is one of `always`, `auto`, or `never`.
diff --git a/Master/texmf-dist/doc/support/cluttex/bin/cluttex.bat b/Master/texmf-dist/doc/support/cluttex/bin/cluttex.bat
index 2e3e8033980..7570923a7e7 100755
--- a/Master/texmf-dist/doc/support/cluttex/bin/cluttex.bat
+++ b/Master/texmf-dist/doc/support/cluttex/bin/cluttex.bat
@@ -347,7 +347,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.
@@ -366,6 +366,7 @@ package.preload["texrunner.shellutil"] = function(...)
]]
local string_gsub = string.gsub
+local os_execute = os.execute
-- s: string
local function escape(s)
@@ -373,14 +374,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.
@@ -403,6 +411,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)
@@ -433,8 +442,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
@@ -545,6 +561,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
@@ -584,6 +601,7 @@ local function parseoption(arg, options)
elseif o.boolean and name == "no-" .. o.long then
-- --no-option
opt = o
+ param = false
break
end
end
@@ -623,6 +641,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
@@ -2127,6 +2146,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
@@ -2146,7 +2582,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
@@ -2168,6 +2604,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.
@@ -2614,6 +3052,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)
@@ -2624,28 +3063,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
diff --git a/Master/texmf-dist/doc/support/cluttex/build.lua b/Master/texmf-dist/doc/support/cluttex/build.lua
index bda6cf637a2..24d0674ffc9 100644
--- a/Master/texmf-dist/doc/support/cluttex/build.lua
+++ b/Master/texmf-dist/doc/support/cluttex/build.lua
@@ -28,6 +28,7 @@ elseif arg[1] == "--windows-batchfile" then
table.remove(arg, 1)
end
local outfile = arg[1]
+local preserve_location_info = false
local modules = {
{
@@ -82,6 +83,10 @@ local modules = {
name = "texrunner.message",
path = "texrunner/message.lua",
},
+ {
+ name = "texrunner.fswatcher_windows",
+ path = "texrunner/fswatcher_windows.lua",
+ },
}
local imported_globals = {"io", "os", "string", "table", "package", "require", "assert", "error", "ipairs", "type", "select", "arg"}
@@ -102,7 +107,13 @@ local function strip_global_imports(code)
end
local function strip_test_code(code)
- return (code:gsub("%-%- TEST CODE\n(.-)%-%- END TEST CODE\n", ""))
+ if preserve_location_info then
+ return (code:gsub("%-%- TEST CODE\n.-%-%- END TEST CODE\n", function(s)
+ return (s:gsub("[^\n]",""))
+ end))
+ else
+ return (code:gsub("%-%- TEST CODE\n(.-)%-%- END TEST CODE\n", ""))
+ end
end
local function load_module_code(path)
@@ -133,8 +144,10 @@ else
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 not preserve_location_info then
+ 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")
+end
if default_os then
table.insert(lines, string.format("os.type = os.type or %q\n", default_os))
@@ -142,19 +155,34 @@ 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)))
+if preserve_location_info then
+ table.insert(lines, "local loadstring = loadstring or load\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] = assert(loadstring(%q, %q))\n", m.name, load_module_code(m.path_windows or m.path), "=" .. (m.path_windows or m.path)))
+ table.insert(lines, 'else\n')
+ table.insert(lines, string.format("package.preload[%q] = assert(loadstring(%q, %q))\n", m.name, load_module_code(m.path_unix or m.path), "=" .. (m.path_unix or m.path)))
+ table.insert(lines, 'end\n')
+ else
+ table.insert(lines, string.format("package.preload[%q] = assert(loadstring(%q, %q))\n", m.name, load_module_code(m.path), "=" .. m.path))
+ end
+ end
+ table.insert(lines, string.format("assert(loadstring(%q, %q))(...)\n", main, "=cluttex.lua"))
+else
+ 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))
end
-table.insert(lines, strip_global_imports(main))
if outfile then
io.output(assert(io.open(outfile, "wb")))
diff --git a/Master/texmf-dist/doc/support/cluttex/checkglobal.lua b/Master/texmf-dist/doc/support/cluttex/checkglobal.lua
index 4b86ef9f6c4..0c765809a02 100644
--- a/Master/texmf-dist/doc/support/cluttex/checkglobal.lua
+++ b/Master/texmf-dist/doc/support/cluttex/checkglobal.lua
@@ -32,6 +32,7 @@ local known_global = {
tostring = true,
type = true,
xpcall = true,
+ loadstring = true, -- Lua 5.1
-- Standard modules
bit32 = true, -- Lua 5.2
diff --git a/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.pdf b/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.pdf
index f5524674724..43292492359 100644
--- a/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.pdf
+++ b/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.pdf
Binary files differ
diff --git a/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.tex b/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.tex
index 9966dbb749d..cd1a47741a2 100644
--- a/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.tex
+++ b/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.tex
@@ -10,8 +10,9 @@
\renewcommand\sectionautorefname{セクション}
\renewcommand\subsectionautorefname{サブセクション}
-\title{\ClutTeX{}マニュアル}
+\title{\ClutTeX{}マニュアル\\(バージョン0.3)}
\author{ARATA Mizuki}
+\date{2019年4月30日}
\begin{document}
\maketitle
@@ -23,7 +24,7 @@
\begin{itemize}
\item 作業ディレクトリを\texttt{.aux}や\texttt{.log}等の「余計な」ファイルで散らかさない
\item (相互参照の解決などで)複数回処理を行う必要がある場合に、必要な回数だけ自動で処理する
-\item 入力ファイルを監視し、変更があった場合に自動で再処理する(\texttt{--watch}オプション\footnote{別途プログラムが必要})
+\item 入力ファイルを監視し、変更があった場合に自動で再処理する(\texttt{--watch}オプション\footnote{Unix系OSでは、別途プログラムが必要。})
\item MakeIndex, \BibTeX, Biber等のコマンドを自動で実行する(\texttt{--makeindex}オプション, \texttt{--bibtex}オプション, \texttt{--biber}オプション)
\item p\TeX 系列の処理系でPDFを生成する場合、別途\texttt{dvipdfmx}を実行する必要がない(自動で\texttt{dvipdfmx}を実行する)
\end{itemize}
@@ -71,7 +72,7 @@
デフォルト:3
\item[\texttt{--watch}]
入力ファイルを監視する。
- 別途、\texttt{fswatch}プログラムが必要となる。
+ 別途、\texttt{fswatch}プログラムまたは\texttt{inotifywait}プログラムが必要となる場合がある。
詳しくは\autoref{sec:watch-mode}を参照。
\item[\texttt{--color[=\metavar{WHEN}]}]
ターミナルへの出力を色付けする。
@@ -145,9 +146,9 @@
\section{監視モード}\label{sec:watch-mode}
\ClutTeX{}に\texttt{--watch}オプションを指定して起動した場合、文書の処理後に\emph{監視モード}に入る。
-\texttt{fswatch}\footnote{\url{http://emcrisostomo.github.io/fswatch/}}プログラムが予めインストールされている必要がある。
-\ClutTeX{}の将来のバージョンでは、外部プログラムに頼らない監視モードを実装するかもしれない。
+Windows上では、\ClutTeX{}単体でファイルシステムの監視を行う。
+一方で、それ以外のOS(Unix系)では、\texttt{fswatch}\footnote{\url{http://emcrisostomo.github.io/fswatch/}}プログラムまたは\texttt{inotifywait}プログラムが予めインストールされている必要がある。
\section{MakeIndexや\BibTeX}
MakeIndexや\BibTeX を使って処理を行う場合は、\texttt{--makeindex}や\texttt{--bibtex}等のオプションを指定する。
diff --git a/Master/texmf-dist/doc/support/cluttex/doc/manual.pdf b/Master/texmf-dist/doc/support/cluttex/doc/manual.pdf
index 40e25bc9b3e..7e3e6dd6f72 100644
--- a/Master/texmf-dist/doc/support/cluttex/doc/manual.pdf
+++ b/Master/texmf-dist/doc/support/cluttex/doc/manual.pdf
Binary files differ
diff --git a/Master/texmf-dist/doc/support/cluttex/doc/manual.tex b/Master/texmf-dist/doc/support/cluttex/doc/manual.tex
index 4ec0464cc67..22761006cf1 100644
--- a/Master/texmf-dist/doc/support/cluttex/doc/manual.tex
+++ b/Master/texmf-dist/doc/support/cluttex/doc/manual.tex
@@ -8,8 +8,10 @@
\newcommand\texpkg[1]{\texttt{#1}}
\newcommand\metavar[1]{\textnormal{\textsf{#1}}}
-\title{\ClutTeX\ manual}
+\title{\ClutTeX\ manual\\(Version 0.3)}
\author{ARATA Mizuki}
+\date{2019-04-30}
+
\begin{document}
\maketitle
\tableofcontents
@@ -20,7 +22,7 @@ Basic features are,
\begin{itemize}
\item Does not clutter your working directory with ``extra'' files, like \texttt{.aux} or \texttt{.log}.
\item If multiple runs are required to generate correct document, do so.
-\item Watch input files, and re-process documents if changes are detected\footnote{needs an external program}.
+\item Watch input files, and re-process documents if changes are detected\footnote{needs an external program if you are on a Unix system}.
\item Run MakeIndex, \BibTeX, Biber, if requested.
\item Produces a PDF, even if the engine (e.g.\ p\TeX) does not suport direct PDF generation.
\end{itemize}
@@ -65,7 +67,7 @@ Basic options:
Default: 3
\item[\texttt{--watch}]
Watch input files for change.
- Requires \texttt{fswatch} command to be available.
+ May need an external program to be available.
See \autoref{sec:watch-mode} for details.
\item[\texttt{--color[=\metavar{WHEN}]}]
Colorize messages.
@@ -138,9 +140,9 @@ This is because Sync\TeX\ cannot find its data file if it's not in the same dire
\section{Watch mode}\label{sec:watch-mode}
If \texttt{--watch} option is given, \ClutTeX\ enters \emph{watch mode} after processing the document.
-An auxiliary program \texttt{fswatch}\footnote{\url{http://emcrisostomo.github.io/fswatch/}} needs to be installed for this mode.
-A future version of \ClutTeX\ may implement a built-in filesystem watcher.
+On Windows, a built-in filesystem watcher is implemented.
+On other platforms, an auxiliary program \texttt{fswatch}\footnote{\url{http://emcrisostomo.github.io/fswatch/}} or \texttt{inotifywait} needs to be installed.
\section{MakeIndex and \BibTeX}
If you want to generate index or bibliography, using MakeIndex or \BibTeX, set \texttt{--makeindex}, \texttt{--bibtex}, or \texttt{--biber} option.
diff --git a/Master/texmf-dist/doc/support/cluttex/src/cluttex.lua b/Master/texmf-dist/doc/support/cluttex/src/cluttex.lua
index 0e56e8bf4bd..92952af58df 100644
--- a/Master/texmf-dist/doc/support/cluttex/src/cluttex.lua
+++ b/Master/texmf-dist/doc/support/cluttex/src/cluttex.lua
@@ -18,7 +18,7 @@
along with ClutTeX. If not, see <http://www.gnu.org/licenses/>.
]]
-CLUTTEX_VERSION = "v0.2"
+CLUTTEX_VERSION = "v0.3"
-- Standard libraries
local table = table
@@ -45,6 +45,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.
@@ -491,6 +493,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)
@@ -501,28 +504,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
diff --git a/Master/texmf-dist/doc/support/cluttex/src/texrunner/fswatcher_windows.lua b/Master/texmf-dist/doc/support/cluttex/src/texrunner/fswatcher_windows.lua
new file mode 100644
index 00000000000..05a244093b8
--- /dev/null
+++ b/Master/texmf-dist/doc/support/cluttex/src/texrunner/fswatcher_windows.lua
@@ -0,0 +1,423 @@
+--[[
+ 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
+
+-- TEST CODE
+do
+ local ws = {0x3042}
+ local resultstr = wcs_to_mbs(ffi.new("WCHAR[1]", ws), 1, CP_UTF8)
+ assert(#resultstr == 3)
+ assert(resultstr == "\xE3\x81\x82") -- \u{XXXX} notation is not available on LuaJIT
+end
+-- END TEST CODE
+
+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,
+}
diff --git a/Master/texmf-dist/doc/support/cluttex/src/texrunner/option.lua b/Master/texmf-dist/doc/support/cluttex/src/texrunner/option.lua
index 839e3a40d67..19598610add 100644
--- a/Master/texmf-dist/doc/support/cluttex/src/texrunner/option.lua
+++ b/Master/texmf-dist/doc/support/cluttex/src/texrunner/option.lua
@@ -19,6 +19,7 @@
-- 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
@@ -58,6 +59,7 @@ local function parseoption(arg, options)
elseif o.boolean and name == "no-" .. o.long then
-- --no-option
opt = o
+ param = false
break
end
end
@@ -97,6 +99,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
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
index c506e26ffba..70bc820e0d8 100644
--- a/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_unix.lua
+++ b/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_unix.lua
@@ -1,5 +1,5 @@
--[[
- Copyright 2016 ARATA Mizuki
+ Copyright 2016,2019 ARATA Mizuki
This file is part of ClutTeX.
@@ -22,6 +22,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)
@@ -57,6 +58,13 @@ assert(escape([[Hello' world!]]) == [['Hello'"'"' world!']])
assert(escape([[Hello' world!"]]) == [['Hello'"'"' world!"']])
-- END TEST CODE
+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,
}
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
index cd0cc11fe9d..a8f4d763aa0 100644
--- a/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_windows.lua
+++ b/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_windows.lua
@@ -1,5 +1,5 @@
--[[
- Copyright 2016 ARATA Mizuki
+ Copyright 2016,2019 ARATA Mizuki
This file is part of ClutTeX.
@@ -18,6 +18,7 @@
]]
local string_gsub = string.gsub
+local os_execute = os.execute
-- s: string
local function escape(s)
@@ -30,6 +31,13 @@ assert(escape([[Hello" world!]]) == [["Hello\" world!"]])
assert(escape([[Hello\" world!"]]) == [["Hello\\\" world!\""]])
-- END TEST CODE
+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,
}
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