(* This file is part of ClutTeX. *)
structure Main = struct
val CLUTTEX_VERSION = "v0.7.0"

val COPYRIGHT_NOTICE =
"Copyright (C) 2016-2024  ARATA Mizuki\n\
\\n\
\This program is free software: you can redistribute it and/or modify\n\
\it under the terms of the GNU General Public License as published by\n\
\the Free Software Foundation, either version 3 of the License, or\n\
\(at your option) any later version.\n\
\\n\
\This program is distributed in the hope that it will be useful,\n\
\but WITHOUT ANY WARRANTY; without even the implied warranty of\n\
\MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n\
\GNU General Public License for more details.\n\
\\n\
\You should have received a copy of the GNU General Public License\n\
\along with this program.  If not, see <http://www.gnu.org/licenses/>.\n";

exception Abort

(* Workaround for recent Universal CRT *)
val () = Lua.call0 Lua.Lib.os.setlocale #[Lua.fromString "", Lua.fromString "ctype"]

fun getEnvMulti [] = NONE
 | getEnvMulti (name :: xs) = case OS.Process.getEnv name of
                                  SOME x => SOME x
                                | NONE => getEnvMulti xs

fun genOutputDirectory (temporary_directory : string option, xs : string list)
   = let val message = String.concatWith "\000" xs
         val hash = MD5.md5AsLowerHex (Byte.stringToBytes message)
         val tmpdir = case temporary_directory of
                          SOME tmpdir => tmpdir
                        | NONE => case getEnvMulti ["TMPDIR", "TMP", "TEMP"] of
                                      SOME tmpdir => tmpdir
                                    | NONE => case getEnvMulti ["HOME", "USERPROFILE"] of
                                                  SOME home => OS.Path.joinDirFile { dir = home, file = ".latex-build-temp" } (* $XDG_CACHE_HOME/cluttex, $HOME/.cache/cluttex *)
                                                | NONE => raise Fail "environment variable 'TMPDIR' not set!"
     in OS.Path.joinDirFile { dir = tmpdir, file = "cluttex-" ^ hash }
     end

fun showUsage () = let val progName = CommandLine.name ()
                  in TextIO.output (TextIO.stdErr,
"ClutTeX: Process TeX files without cluttering your working directory\n\
\\n\
\Usage:\n\
\  " ^ progName ^ " [options] [--] FILE.tex\n\
\\n\
\Options:\n\
\  -e, --engine=ENGINE          Specify which TeX engine to use.\n\
\                                 ENGINE is one of the following:\n\
\                                     pdflatex, pdftex,\n\
\                                     lualatex, luatex, luajittex,\n\
\                                     xelatex, xetex, latex, etex, tex,\n\
\                                     platex, eptex, ptex,\n\
\                                     uplatex, euptex, uptex,\n\
\      --engine-executable=COMMAND+OPTIONs\n\
\                               The actual TeX command to use.\n\
\                                 [default: ENGINE]\n\
\  -o, --output=FILE            The name of output file.\n\
\                                 [default: JOBNAME.pdf or JOBNAME.dvi]\n\
\      --fresh                  Clean intermediate files before running TeX.\n\
\                                 Cannot be used with --output-directory.\n\
\      --max-iterations=N       Maximum number of running TeX to resolve\n\
\                                 cross-references.  [default: 3]\n\
\      --start-with-draft       Start with draft mode.\n\
\      --[no-]change-directory  Change directory before running TeX.\n\
\      --watch[=ENGINE]         Watch input files for change.  Requires fswatch\n\
\                                 or inotifywait to be installed. ENGINE is one of\n\
\                                 `fswatch', `inotifywait' or `auto' [default: `auto']\n\
\      --tex-option=OPTION      Pass OPTION to TeX as a single option.\n\
\      --tex-options=OPTIONs    Pass OPTIONs to TeX as multiple options.\n\
\      --dvipdfmx-option[s]=OPTION[s]  Same for dvipdfmx.\n\
\      --makeindex=COMMAND+OPTIONs  Command to generate index, such as\n\
\                                     `makeindex' or `mendex'.\n\
\      --bibtex=COMMAND+OPTIONs     Command for BibTeX, such as\n\
\                                     `bibtex' or `pbibtex'.\n\
\      --biber[=COMMAND+OPTIONs]    Command for Biber.\n\
\      --makeglossaries[=COMMAND+OPTIONs]  Command for makeglossaries.\n\
\  -h, --help                   Print this message and exit.\n\
\  -v, --version                Print version information and exit.\n\
\  -V, --verbose                Be more verbose.\n\
\      --color[=WHEN]           Make ClutTeX's message colorful. WHEN is one of\n\
\                                 `always', `auto', or `never'.\n\
\                                 [default: `auto' if --color is omitted,\n\
\                                           `always' if WHEN is omitted]\n\
\      --includeonly=NAMEs      Insert '\\includeonly{NAMEs}'.\n\
\      --make-depends=FILE      Write dependencies as a Makefile rule.\n\
\      --print-output-directory  Print the output directory and exit.\n\
\      --package-support=PKG1[,PKG2,...]\n\
\                               Enable special support for some shell-escaping\n\
\                                 packages.\n\
\                               Currently supported: minted, epstopdf\n\
\      --check-driver=DRIVER    Check that the correct driver file is loaded.\n\
\                               DRIVER is one of `dvipdfmx', `dvips', `dvisvgm'.\n\
\      --source-date-epoch=TIME\n\
\                               Set SOURCE_DATE_EPOCH variable.\n\
\                               TIME is `now' or an integer.\n\
\\n\
\      --[no-]shell-escape\n\
\      --shell-restricted\n\
\      --synctex=NUMBER\n\
\      --fmt=FMTNAME\n\
\      --[no-]file-line-error   [default: yes]\n\
\      --[no-]halt-on-error     [default: yes]\n\
\      --interaction=STRING     [default: nonstopmode]\n\
\      --jobname=STRING\n\
\      --output-directory=DIR   [default: somewhere in the temporary directory]\n\
\      --output-format=FORMAT   FORMAT is `pdf' or `dvi'.  [default: pdf]\n\
\\n" ^ COPYRIGHT_NOTICE)
                   ; OS.Process.exit OS.Process.success
                  end

structure HandleOptions = HandleOptions (fun showMessageAndFail message = (TextIO.output (TextIO.stdErr, message ^ "\n"); OS.Process.exit OS.Process.failure)
                                        val showUsage = showUsage
                                        fun showVersion () = (TextIO.output (TextIO.stdErr, "cluttex " ^ CLUTTEX_VERSION ^ "\n"); OS.Process.exit OS.Process.success)
                                       )

(*: val pathInOutputDirectory : AppOptions.options * string -> string *)
fun pathInOutputDirectory (options : AppOptions.options, ext) = PathUtil.join2 (#output_directory options, #jobname options ^ "." ^ ext)
(*: val executeCommand : string * (unit -> bool) option -> unit *)
(*
fun executeCommand (command, recover)
   = let val () = Message.exec command
         val status = OS.Process.system command
         val success_or_recoverd = if OS.Process.isSuccess status then
                                       true
                                   else
                                       case recover of
                                           SOME f => f ()
                                         | NONE => false
     in if success_or_recoverd then
            ()
        else
            ( Message.error "Command exited abnormally" (* TODO: show status code: Unix.fromStatus *)
            ; raise Abort
            )
     end
*)
fun executeCommand (command, recover)
   = let val () = Message.exec command
         val (success, termination, status_or_signal) = Lua.call3 Lua.Lib.os.execute #[Lua.fromString command]
         val (success, termination, status_or_signal) : bool * string option * Lua.value
             = if Lua.typeof success = "number" then (* Lua 5.1 or LuaTeX *)
                   (Lua.== (success, Lua.fromInt 0), NONE, success)
               else
                   (Lua.unsafeFromValue success, SOME (Lua.unsafeFromValue termination), status_or_signal)
         val success_or_recovered = success orelse (case recover of
                                                        SOME f => f ()
                                                      | NONE => false
                                                   )
     in if success_or_recovered then
            ()
        else
            ( case termination of
                  SOME "exit" => Message.error ("Command exited abnormally: exit status " ^ Lua.unsafeFromValue (Lua.call1 Lua.Lib.tostring #[status_or_signal]))
                | SOME "signal" => Message.error ("Command exited abnormally: signal " ^ Lua.unsafeFromValue (Lua.call1 Lua.Lib.tostring #[status_or_signal]))
                | _ => Message.error ("Command exited abnormally: " ^ Lua.unsafeFromValue (Lua.call1 Lua.Lib.tostring #[status_or_signal]))
            ; raise Abort
            )
     end

(* The value to be used for SOURCE_DATE_EPOCH *)
fun getTimeSinceEpoch () : string
 = Lua.unsafeFromValue (Lua.call1 Lua.Lib.tostring #[Lua.call1 Lua.Lib.os.time #[]])

type run_params = { options : AppOptions.options
                 , inputfile : string
                 , engine : TeXEngine.engine
                 , tex_options : TeXEngine.run_options
                 , recorderfile : string
                 , recorderfile2 : string
                 , original_wd : string
                 , output_extension : string
                 , source_date_epoch_info : { time_since_epoch : string, time : Time.time } ref option
                 }
datatype single_run_result = SHOULD_RERUN of Reruncheck.aux_status StringMap.map
                          | NO_NEED_TO_RERUN
                          | NO_PAGES_OF_OUTPUT

(* Run TeX command ( *tex, *latex) *)
(*: val singleRun : run_params * Reruncheck.aux_status StringMap.map * int -> single_run_result *)
fun singleRun ({ options, inputfile, engine, tex_options, recorderfile, recorderfile2, original_wd, source_date_epoch_info, ... } : run_params, auxstatus, iteration)
   = let val mainauxfile = pathInOutputDirectory (options, "aux")
         val { filelist, auxstatus, minted, epstopdf, pdfx, bibtex_aux_hash }
             = if FSUtil.isFile recorderfile then
                   (* Recorder file already exists *)
                   let val recorded = Reruncheck.parseRecorderFile { file = recorderfile, options = options }
                       val recorded = if TeXEngine.isLuaTeX engine andalso FSUtil.isFile recorderfile2 then
                                          Reruncheck.parseRecorderFileContinued { file = recorderfile2, options = options, previousResult = recorded }
                                      else
                                          recorded
                       val (filelist, filemap) = Reruncheck.getFileInfo recorded
                       val auxstatus = Reruncheck.collectFileInfo (filelist, auxstatus)
                       val { minted, epstopdf, pdfx } =
                         List.foldl (fn ({ path, ... }, { minted, epstopdf, pdfx }) =>
                                        { minted = minted orelse String.isSuffix "minted/minted.sty" path
                                        , epstopdf = epstopdf orelse String.isSuffix "epstopdf.sty" path
                                        , pdfx = pdfx orelse String.isSuffix "pdfx.sty" path
                                        }
                                    ) { minted = false, epstopdf = false, pdfx = false } filelist
                       val bibtex_aux_hash = case #bibtex_or_biber options of
                                                 SOME (AppOptions.BIBTEX _) =>
                                                 let val biblines = AuxFile.extractBibTeXLines { auxfile = mainauxfile, outdir = #output_directory options }
                                                 in SOME (MD5.compute (Byte.stringToBytes (String.concatWith "\n" biblines)))
                                                 end
                                               | _ => NONE
                   in { filelist, auxstatus, minted, epstopdf, pdfx, bibtex_aux_hash }
                   end
               else
                   (* This is the first execution *)
                   if StringMap.isEmpty auxstatus then
                       { filelist = [], auxstatus = StringMap.empty, minted = false, epstopdf = false, pdfx = false, bibtex_aux_hash = NONE }
                   else
                       ( Message.error "Recorder file was not generated during the execution!"
                       ; raise Abort
                       )

         (*
          * Set SOURCE_DATE_EPOCH if
          *   * --source-date-epoch=now is set, or
          *   * --source-date-epoch is not set but `pdfx' package is used and SOURCE_DATE_EPOCH is not already set.
          * The value will be the newer of these:
          *   * The time when the program started (see main()).
          *   * The time we are processing after one of the input files was modified.
          *)
         val () = case source_date_epoch_info of
             NONE => () (* already set in main () *)
           | SOME r =>
             let val should_set_source_date_epoch = case #source_date_epoch options of
                     SOME AppOptions.SourceDateEpoch.NOW => true
                   | SOME (AppOptions.SourceDateEpoch.RAW _) => false (* should not occur *)
                   | NONE => pdfx orelse #pdfx (#package_support options)
             in if should_set_source_date_epoch then
                    let val input_time = List.foldl (fn ({ abspath, kind = Reruncheck.INPUT, ... }, acc) =>
                                                        (case StringMap.find (auxstatus, abspath) of
                                                             SOME { mtime, ... } =>
                                                                 (case (mtime, acc) of
                                                                      (SOME mtime', SOME t) =>
                                                                        if Time.< (t, mtime') then
                                                                            mtime
                                                                        else
                                                                            acc
                                                                    | (NONE, _) => acc
                                                                    | (_, NONE) => mtime
                                                                 )
                                                           | NONE => acc
                                                         )
                                                      | (_, acc) => acc
                                                    ) NONE filelist
                        val info = case input_time of
                                       SOME input_time => if Time.< (#time (!r), input_time) then (* input file was changed since the last run *)
                                                              let val new_info = { time_since_epoch = getTimeSinceEpoch (), time = input_time }
                                                              in if Message.getVerbosity () >= 1 then
                                                                     Message.info "Input file was modified; Updating SOURCE_DATE_EPOCH..."
                                                                 else
                                                                     ()
                                                                     ; r := new_info
                                                               ; new_info
                                                              end
                                                          else
                                                              !r
                                     | NONE => !r
                    in if Message.getVerbosity () >= 1 then
                           Message.info ("Setting SOURCE_DATE_EPOCH to " ^ #time_since_epoch info)
                       else
                           ()
                     ; OSUtil.setEnv ("SOURCE_DATE_EPOCH", #time_since_epoch info)
                    end
                else
                    ()
             end

         val tex_injection = case #includeonly options of
                                 SOME io => "\\includeonly{" ^ io ^ "}"
                               | NONE => ""
         val tex_injection = if minted orelse #minted (#package_support options) then
                                 let val () = if not (#minted (#package_support options)) then
                                                  Message.diag "You may want to use --package-support=minted option."
                                              else
                                                  ()
                                     val outdir = #output_directory options
                                     val outdir = if OSUtil.isWindows then
                                                      String.map (fn #"\\" => #"/" | c => c) outdir (* Use forward slashes *)
                                                  else
                                                      outdir
                                 in tex_injection ^ "\\PassOptionsToPackage{outputdir=" ^ outdir ^ "}{minted}"
                                 end
                             else
                                 tex_injection
         val tex_injection = if epstopdf orelse #epstopdf (#package_support options) then
                                 let val () = if not (#epstopdf (#package_support options)) then
                                                  Message.diag "You may want to use --package-support=epstopdf option."
                                              else
                                                  ()
                                     val outdir = #output_directory options
                                     val outdir = if OSUtil.isWindows then
                                                      String.map (fn #"\\" => #"/" | c => c) outdir (* Use forward slashes *)
                                                  else
                                                      outdir
                                     val outdir = if String.isSuffix "/" outdir then
                                                      outdir
                                                  else
                                                      outdir ^ "/" (* Must end with a directory separator *)
                                 in tex_injection ^ "\\PassOptionsToPackage{outdir=" ^ outdir ^ "}{epstopdf}"
                                 end
                             else
                                 tex_injection
         val inputline = tex_injection ^ SafeName.safeInput { name = inputfile, isPdfTeX = TeXEngine.isPdfTeX engine }
         val (current_tex_options, lightweight_mode)
             = if iteration = 1 andalso #start_with_draft options then
                   if #supports_draftmode engine then
                       ({ tex_options where draftmode = true, interaction = SOME InteractionMode.BATCHMODE }, true)
                   else
                       ({ tex_options where interaction = SOME InteractionMode.BATCHMODE }, true)
               else
                   ({ tex_options where draftmode = false }, false)
         val command = TeXEngine.buildCommand (engine, inputline, current_tex_options)
         val execlogCache = ref NONE
         fun getExecLog () = case !execlogCache of
                                 NONE => let val ins = TextIO.openIn (pathInOutputDirectory (options, "log"))
                                             val log = TextIO.inputAll ins
                                             val () = TextIO.closeIn ins
                                         in execlogCache := SOME log
                                          ; log
                                         end
                               | SOME log => log
         val recovered = ref false
         fun recover () = let val execlog = getExecLog ()
                              val r = Recovery.tryRecovery { options = options, execlog = execlog, auxfile = pathInOutputDirectory (options, "aux"), originalWorkingDirectory = original_wd }
                          in recovered := true
                           ; r
                          end
         val () = executeCommand (command, SOME recover)
     in if !recovered then
            SHOULD_RERUN StringMap.empty
        else
            let val recorded = Reruncheck.parseRecorderFile { file = recorderfile, options = options }
                val recorded = if TeXEngine.isLuaTeX engine andalso FSUtil.isFile recorderfile2 then
                                   Reruncheck.parseRecorderFileContinued { file = recorderfile2, options = options, previousResult = recorded }
                               else
                                   recorded
                val (filelist, filemap) = Reruncheck.getFileInfo recorded
                val execlog = getExecLog ()

                (* Check driver *)
                val () = case #check_driver options of
                             NONE => ()
                           | SOME driver => CheckDriver.checkDriver (driver, List.map (fn { path, abspath, kind } => { path = path, kind = case kind of Reruncheck.INPUT => "input" | Reruncheck.OUTPUT => "output" | Reruncheck.AUXILIARY => "auxiliary"}) filelist)

                (* makeindex *)
                val filelist = case #makeindex options of
                                   NONE => (* Check log file *)
                                   ( if Lua.isFalsy (Lua.call1 Lua.Lib.string.find #[Lua.fromString execlog, Lua.fromString "No file [^\n]+%.ind%."]) then
                                         ()
                                     else
                                         Message.diag "You may want to use --makeindex option."
                                   ; filelist
                                   )
                                 | SOME makeindex =>
                                   let fun go (file, filelist_acc) (* Look for .idx files and run MakeIndex *)
                                           = if PathUtil.ext (#path file) = "idx" then
                                                 (* Run makeindex if the .idx file is new or updated *)
                                                 let val idxfileinfo = { path = #path file, abspath = #abspath file, kind = Reruncheck.AUXILIARY }
                                                     val output_ind = PathUtil.replaceext { path = #abspath file, newext = "ind" }
                                                 in if #1 (Reruncheck.compareFileInfo ([idxfileinfo], auxstatus)) orelse Reruncheck.compareFileTime { srcAbs = #abspath file, dst = output_ind, auxstatus = auxstatus } then
                                                        let val idx_dir = PathUtil.dirname (#abspath file)
                                                            val makeindex_command = [
                                                                "cd", ShellUtil.escape idx_dir, "&&",
                                                                makeindex, (* Do not escape `makeindex` to allow additional options *)
                                                                "-o", PathUtil.basename output_ind,
                                                                PathUtil.basename (#abspath file)
                                                            ]
                                                        in executeCommand (String.concatWith " " makeindex_command, NONE)
                                                         ; { path = output_ind, abspath = output_ind, kind = Reruncheck.AUXILIARY } :: filelist_acc
                                                        end
                                                    else
                                                        ( FSUtil.touch output_ind handle Lua.Error err => Message.warn ("Failed to touch " ^ output_ind ^ " (" ^ Lua.unsafeFromValue err ^ ")")
                                                        ; filelist_acc
                                                        )
                                                 end
                                             else
                                                 filelist_acc
                                   in List.foldl go filelist filelist
                                   end

                (* makeglossaries *)
                val filelist = case #makeglossaries options of
                                   NONE => (* Check log file *)
                                   ( if Lua.isFalsy (Lua.call1 Lua.Lib.string.find #[Lua.fromString execlog, Lua.fromString "No file [^\n]+%.gls%."]) then
                                         ()
                                     else
                                         Message.diag "You may want to use --makeglossaries option."
                                   ; filelist
                                   )
                                 | SOME makeglossaries =>
                                   let fun go (file, filelist_acc) (* Look for .glo files and run makeglossaries *)
                                           = if PathUtil.ext (#path file) = "glo" then
                                                 (* Run makeglossaries if the .glo file is new or updated *)
                                                 let val glofileinfo = { path = #path file, abspath = #abspath file, kind = Reruncheck.AUXILIARY }
                                                     val output_gls = PathUtil.replaceext { path = #abspath file, newext = "gls" }
                                                 in if #1 (Reruncheck.compareFileInfo ([glofileinfo], auxstatus)) orelse Reruncheck.compareFileTime { srcAbs = #abspath file, dst = output_gls, auxstatus = auxstatus } then
                                                        let val makeglossaries_command = [
                                                                makeglossaries,
                                                                "-d", ShellUtil.escape (#output_directory options),
                                                                PathUtil.trimext (PathUtil.basename (#path file))
                                                            ]
                                                        in executeCommand (String.concatWith " " makeglossaries_command, NONE)
                                                         ; { path = output_gls, abspath = output_gls, kind = Reruncheck.AUXILIARY } :: filelist_acc
                                                        end
                                                    else
                                                        ( FSUtil.touch output_gls handle Lua.Error err => Message.warn ("Failed to touch " ^ output_gls ^ " (" ^ Lua.unsafeFromValue err ^ ")")
                                                        ; filelist_acc
                                                        )
                                                 end
                                             else
                                                 filelist_acc
                                   in List.foldl go filelist filelist
                                   end

                (* bibtex/biber *)
                val filelist = case #bibtex_or_biber options of
                                   NONE => ( if Lua.isFalsy (Lua.call1 Lua.Lib.string.find #[Lua.fromString execlog, Lua.fromString "No file [^\n]+%.bbl%."]) then
                                                 ()
                                             else
                                                 Message.diag "You may want to use --bibtex or biber option."
                                           ; filelist
                                           )
                                 | SOME (AppOptions.BIBTEX bibtex) =>
                                   let val biblines2 = AuxFile.extractBibTeXLines { auxfile = mainauxfile, outdir = #output_directory options }
                                       val bibtex_aux_hash2 = if List.null biblines2 then
                                                                  NONE
                                                              else
                                                                  SOME (MD5.compute (Byte.stringToBytes (String.concatWith "\n" biblines2)))
                                       val output_bbl = pathInOutputDirectory (options, "bbl")
                                   in if bibtex_aux_hash <> bibtex_aux_hash2 orelse Reruncheck.compareFileTime { srcAbs = PathUtil.abspath { path = mainauxfile, cwd = NONE }, dst = output_bbl, auxstatus = auxstatus } then
                                          (* The input for BibTeX command has changed... *)
                                          let val bibtex_command = [
                                                  "cd", ShellUtil.escape (#output_directory options), "&&",
                                                  bibtex,
                                                  PathUtil.basename mainauxfile
                                              ]
                                          in executeCommand (String.concatWith " " bibtex_command, NONE)
                                          end
                                      else
                                          ( if Message.getVerbosity () >= 1 then
                                                Message.info "No need to run BibTeX."
                                            else
                                                ()
                                          ; FSUtil.touch output_bbl handle Lua.Error err => Message.warn ("Failed to touch " ^ output_bbl ^ " (" ^ Lua.unsafeFromValue err ^ ")")
                                          )
                                    ; filelist
                                   end
                           | SOME (AppOptions.BIBER biber) =>
                             let fun go (file, filelist_acc)
                                     (* Usual compilation with biber
                                      * tex     -> pdflatex tex -> aux,bcf,pdf,run.xml
                                      * bcf     -> biber bcf    -> bbl
                                      * tex,bbl -> pdflatex tex -> aux,bcf,pdf,run.xml
                                      *)
                                     = if PathUtil.ext (#path file) = "bcf" then
                                           (* Run biber if the .bcf file is new or updated *)
                                           let val bcffileinfo = { path = #path file, abspath = #abspath file, kind = Reruncheck.AUXILIARY }
                                               val output_bbl = PathUtil.replaceext { path = #abspath file, newext = "bbl" }
                                               fun check_bib_update abspath
                                                   = let val ins = TextIO.openIn abspath
                                                         fun go updated_dot_bib
                                                             = case TextIO.inputLine ins of
                                                                   NONE => updated_dot_bib
                                                                 | SOME l =>
                                                                   let val bib = Lua.call1 Lua.Lib.string.match #[Lua.fromString l, Lua.fromString "<bcf:datasource .*>(.*)</bcf:datasource>"]
                                                                   in if Lua.isFalsy bib then
                                                                          go updated_dot_bib (* continue *)
                                                                      else
                                                                          let val bib = Lua.unsafeFromValue bib : string
                                                                              val bibfile = PathUtil.join2 (original_wd, bib)
                                                                              val updated_dot_bib = if FSUtil.isFile bibfile then
                                                                                                        let val updated_dot_bib_tmp = not (Reruncheck.compareFileTime { srcAbs = PathUtil.abspath { path = mainauxfile, cwd = NONE }, dst = bibfile, auxstatus = auxstatus })
                                                                                                        in if updated_dot_bib_tmp then
                                                                                                               Message.info (bibfile ^ " is newer than aux")
                                                                                                           else
                                                                                                               ()
                                                                                                         ; updated_dot_bib orelse updated_dot_bib_tmp
                                                                                                        end
                                                                                                    else
                                                                                                        ( Message.warn (bibfile ^ " is not accessible")
                                                                                                        ; updated_dot_bib
                                                                                                        )
                                                                          in go updated_dot_bib
                                                                          end
                                                                   end
                                                     in go false before TextIO.closeIn ins
                                                     end
                                               val updated_dot_bib = check_bib_update (#abspath file)
                                           in if updated_dot_bib orelse #1 (Reruncheck.compareFileInfo ([bcffileinfo], auxstatus)) orelse Reruncheck.compareFileTime { srcAbs = #abspath file, dst = output_bbl, auxstatus = auxstatus } then
                                                  let val biber_command = [
                                                          biber, (* Do not escape `biber` to allow additional options *)
                                                          "--output-directory", ShellUtil.escape (#output_directory options),
                                                          PathUtil.basename (#abspath file)
                                                      ]
                                                  in executeCommand (String.concatWith " " biber_command, NONE)
                                                   ; { path = output_bbl, abspath = output_bbl, kind = Reruncheck.AUXILIARY } :: filelist
                                                  end
                                              else
                                                  ( FSUtil.touch output_bbl handle Lua.Error err => Message.warn ("Failed to touch " ^ output_bbl ^ " (" ^ Lua.unsafeFromValue err ^ ")")
                                                  ; filelist_acc
                                                  )
                                           end
                                       else
                                           filelist_acc
                             in List.foldl go filelist filelist
                             end

            in if String.isSubstring "No pages of output." execlog then
                   NO_PAGES_OF_OUTPUT
               else
                   let val (should_rerun, auxstatus) = Reruncheck.compareFileInfo (filelist, auxstatus)
                   in if should_rerun orelse lightweight_mode then
                          SHOULD_RERUN auxstatus
                      else
                          NO_NEED_TO_RERUN
                   end
            end
     end

(* Run (La)TeX (possibly multiple times) and produce a PDF/DVI file. *)
(*: val doTypeset : run_params -> unit *)
fun doTypeset (run_params as { options, engine, output_extension, recorderfile, recorderfile2, source_date_epoch_info, ... } : run_params)
   = let fun loop (iteration, auxstatus)
             = let val iteration = iteration + 1
               in case singleRun (run_params, auxstatus, iteration) of
                      NO_PAGES_OF_OUTPUT => ( Message.warn "No pages of output."
                                            ; false
                                            )
                    | NO_NEED_TO_RERUN => true
                    | SHOULD_RERUN auxstatus => if iteration >= #max_iterations options then
                                                    ( Message.warn "LaTeX should be run once more."
                                                    ; true
                                                    )
                                                else
                                                    loop (iteration, auxstatus)
               end
     in if loop (0, StringMap.empty) then
            (* Successful *)
            ( if #output_format options = OutputFormat.DVI orelse #supports_pdf_generation engine then
                  (* Output file (DVI/PDF) is generated in the output directory *)
                  let val outfile = pathInOutputDirectory (options, output_extension)
                      val onCopyError = if OSUtil.isWindows then
                                            SOME (fn () => let val output_format = case #output_format options of
                                                                                       OutputFormat.DVI => "DVI"
                                                                                     | OutputFormat.PDF => "PDF"
                                                           in Message.error ("Failed to copy file.  Some applications may be locking the " ^ output_format ^ " file.")
                                                            ; false
                                                           end
                                                 )
                                        else
                                            NONE
                  in executeCommand (FSUtil.copyCommand { from = outfile, to = #output options }, onCopyError)
                   ; if List.null (#dvipdfmx_extraoptions options) then
                         ()
                     else
                         Message.warn "--dvipdfmx-option[s] are ignored."
                  end
              else
                  (* DVI file is generated, but PDF file is wanted *)
                  let val dvifile = pathInOutputDirectory (options, "dvi")
                      val dvipdfmx_command = "dvipdfmx" :: "-o" :: ShellUtil.escape (#output options) :: #dvipdfmx_extraoptions options @ [ShellUtil.escape dvifile]
                  in executeCommand (String.concatWith " " dvipdfmx_command, NONE)
                  end
            ; (* Copy SyncTeX file if necessary *)
              if #output_format options = OutputFormat.PDF then
                  let val synctex = Lua.unsafeFromValue (Lua.call1 Lua.Lib.tonumber #[Lua.fromString (Option.getOpt (#synctex options, "0"))]) : int
                      val synctex_ext = if synctex > 0 then
                                            (* Compressed SyncTeX file (.synctex.gz) *)
                                            SOME "synctex.gz"
                                        else if synctex < 0 then
                                            (* Uncompressed SyncTeX file (.synctex) *)
                                            SOME "synctex"
                                        else
                                            NONE
                  in case synctex_ext of
                         SOME ext => executeCommand (FSUtil.copyCommand { from = pathInOutputDirectory (options, ext), to = PathUtil.replaceext { path = #output options, newext = ext } }, NONE)
                       | NONE => ()
                  end
              else
                  ()
            ; (* Write dependencies file *)
              case #make_depends options of
                  SOME make_depends =>
                  let val recorded = Reruncheck.parseRecorderFile { file = recorderfile, options = options }
                      val recorded = if TeXEngine.isLuaTeX engine andalso FSUtil.isFile recorderfile2 then
                                         Reruncheck.parseRecorderFileContinued { file = recorderfile2, options = options, previousResult = recorded }
                                     else
                                         recorded
                      val (filelist, _) = Reruncheck.getFileInfo recorded
                      val outs = TextIO.openOut make_depends
                  in TextIO.output (outs, #output options ^ ":") (* TODO: quote *)
                   ; List.app (fn { path, abspath = _, kind = Reruncheck.INPUT } => TextIO.output (outs, " " ^ path) (* TODO: quote *)
                              | _ => ()) filelist
                   ; TextIO.output (outs, "\n")
                   ; TextIO.closeOut outs
                  end
                | NONE => ()
            ; (* Successful *)
              if Message.getVerbosity () >= 1 then
                  Message.info "Command exited successfully"
              else
                  ()
            )
        else
            (* No pages of output. *)
            ()
     end

(*: val doWatchWindows : Lua.value -> string list -> bool *)
fun doWatchWindows fswatcherlib files
   = let val watcher = Lua.call1 Lua.Lib.assert (Lua.call (Lua.field (fswatcherlib, "new")) #[])
         val () = List.app (fn file => Lua.call0 Lua.Lib.assert (Lua.method (watcher, "add") #[Lua.fromString file])) files
         val result = Lua.call1 Lua.Lib.assert (Lua.method (watcher, "next") #[])
         val () = if Message.getVerbosity () >= 2 then
                      Message.info (Lua.unsafeFromValue (Lua.field (result, "action")) ^ " " ^ Lua.unsafeFromValue (Lua.field (result, "path")))
                  else
                      ()
         val () = Lua.method0 (watcher, "close") #[]
     in true
     end

(*: val doWatchFswatch : string list -> bool *)
fun doWatchFswatch files
   = let val fswatch_command = "fswatch" :: "--one-event" :: "--event=Updated" :: "--" :: List.map ShellUtil.escape files
         val fswatch_command_str = String.concatWith " " fswatch_command
         val () = if Message.getVerbosity () >= 1 then
                      Message.exec fswatch_command_str
                  else
                      ()
         val fswatch = Lua.call1 Lua.Lib.assert (Lua.call Lua.Lib.io.popen #[Lua.fromString fswatch_command_str, Lua.fromString "r"])
         val readLine = Lua.method1 (fswatch, "lines") #[]
         fun go () = let val l = Lua.call1 readLine #[]
                     in if Lua.isFalsy l then
                            false
                        else if List.exists (fn path => Lua.unsafeFromValue l = path) files then
                            true
                        else
                            go ()
                     end
     in go () before Lua.method0 (fswatch, "close") #[]
     end

(*: val doWatchInotifywait : string list -> bool *)
fun doWatchInotifywait files
   = let val inotifywait_command = "inotifywait" :: "--event=modify" :: "--event=attrib" :: "--format=%w" :: "--quiet" :: List.map ShellUtil.escape files
         val inotifywait_command_str = String.concatWith " " inotifywait_command
         val () = if Message.getVerbosity () >= 1 then
                      Message.exec inotifywait_command_str
                  else
                      ()
         val inotifywait = Lua.call1 Lua.Lib.assert (Lua.call Lua.Lib.io.popen #[Lua.fromString inotifywait_command_str, Lua.fromString "r"])
         val readLine = Lua.method1 (inotifywait, "lines") #[]
         fun go () = let val l = Lua.call1 readLine #[]
                     in if Lua.isFalsy l then
                            false
                        else if List.exists (fn path => Lua.unsafeFromValue l = path) files then
                            true
                        else
                            go ()
                     end
     in go () before Lua.method0 (inotifywait, "close") #[]
     end

(*: val runWatchMode : AppOptions.WatchEngine.engine * run_params -> unit *)
fun runWatchMode (watch_engine, run_params as { options, engine, recorderfile, recorderfile2, ... } : run_params)
   = let val fswatcherlib = if OSUtil.isWindows then
                                (* Windows: Try built-in filesystem watcher *)
                                let val (succ, result) = Lua.call2 Lua.Lib.pcall #[Lua.Lib.require, Lua.fromString "texrunner.fswatcher_windows"]
                                in if Lua.isFalsy succ then
                                       ( if Message.getVerbosity () >= 1 then
                                             Message.warn ("Failed to load texrunner.fswatcher_windows: " ^ Lua.unsafeFromValue result)
                                         else
                                             ()
                                       ; NONE
                                       )
                                   else
                                       SOME result
                                end
                            else
                                NONE
         val doWatch = case fswatcherlib of
                           SOME fswatcherlib =>
                           ( if Message.getVerbosity () >= 2 then
                                 Message.info "Using built-in filesystem watcher for Windows"
                             else
                                 ()
                           ; doWatchWindows fswatcherlib
                           )
                         | NONE => if ShellUtil.hasCommand "fswatch" andalso (watch_engine = AppOptions.WatchEngine.AUTO orelse watch_engine = AppOptions.WatchEngine.AUTO) then
                                       ( if Message.getVerbosity () >= 2 then
                                             Message.info "Using `fswatch' command"
                                         else
                                             ()
                                       ; doWatchFswatch
                                       )
                                   else if ShellUtil.hasCommand "inotifywait" andalso (watch_engine = AppOptions.WatchEngine.AUTO orelse watch_engine = AppOptions.WatchEngine.INOTIFYWAIT) then
                                       ( if Message.getVerbosity () >= 2 then
                                             Message.info "Using `inotifywait' command"
                                         else
                                             ()
                                       ; doWatchInotifywait
                                       )
                                   else
                                       ( case watch_engine of
                                             AppOptions.WatchEngine.AUTO => Message.error "Could not watch files because neither `fswatch' nor `inotifywait' was installed."
                                           | AppOptions.WatchEngine.FSWATCH => Message.error "Could not watch files because your selected engine `fswatch' was not installed."
                                           | AppOptions.WatchEngine.INOTIFYWAIT => Message.error "Could not watch files because your selected engine `inotifywait' was not installed."
                                       ; Message.info "See ClutTeX's manual for details."
                                       ; OS.Process.exit OS.Process.failure
                                       )

         val _ = (doTypeset run_params; true) handle Abort => false
         (* TODO: filenames here can be UTF-8 if command_line_encoding=utf-8 *)
         val recorded = Reruncheck.parseRecorderFile { file = recorderfile, options = options }
         val recorded = if TeXEngine.isLuaTeX engine andalso FSUtil.isFile recorderfile2 then
                            Reruncheck.parseRecorderFileContinued { file = recorderfile2, options = options, previousResult = recorded }
                        else
                            recorded
         val (filelist, _) = Reruncheck.getFileInfo recorded
         val inputFilesToWatch = List.mapPartial (fn { path = _, abspath, kind = Reruncheck.INPUT } => SOME abspath | _ => NONE) filelist
         fun loop inputFilesToWatch
             = if doWatch inputFilesToWatch then
                   let val success = (doTypeset run_params; true) handle Abort => false
                   in if success then
                          let val recorded = Reruncheck.parseRecorderFile { file = recorderfile, options = options }
                              val recorded = if TeXEngine.isLuaTeX engine andalso FSUtil.isFile recorderfile2 then
                                                 Reruncheck.parseRecorderFileContinued { file = recorderfile2, options = options, previousResult = recorded }
                                             else
                                                 recorded
                              val (filelist, _) = Reruncheck.getFileInfo recorded
                              val inputFilesToWatch = List.mapPartial (fn { path = _, abspath, kind = Reruncheck.INPUT } => SOME abspath | _ => NONE) filelist
                          in loop inputFilesToWatch
                          end
                      else
                          loop inputFilesToWatch (* error; watch the same files again *)
                   end
               else
                   () (* exit *)
     in loop inputFilesToWatch
     end

fun getConfigFilePath (SOME configFilePath) = SOME configFilePath
 | getConfigFilePath NONE = case OS.Process.getEnv "CLUTTEX_CONFIG_FILE" of
                                SOME f => SOME f
                              | NONE => if OSUtil.isWindows then
                                           case OS.Process.getEnv "APPDATA" of
                                               SOME appData => SOME (appData ^ "\\cluttex\\config.toml")
                                             | NONE => NONE
                                        else
                                           case OS.Process.getEnv "XDG_CONFIG_HOME" of
                                               SOME xdgConfigHome => SOME (xdgConfigHome ^ "/cluttex/config.toml")
                                             | NONE => case OS.Process.getEnv "HOME" of
                                                           SOME home => SOME (home ^ "/.config/cluttex/config.toml")
                                                         | NONE => NONE

fun loadConfig configFileOpt = case getConfigFilePath configFileOpt of
                                  NONE => ConfigFile.defaultConfig
                                | SOME path => (ConfigFile.loadConfig path handle IO.Io _ => ConfigFile.defaultConfig
                                                                                | ValidateUtf8.InvalidUtf8 => (Message.error ("Config file " ^ path ^ " is not UTF-8 encoded."); ConfigFile.defaultConfig)
                                                                                | TomlParseError.ParseError e => (Message.error ("Config file " ^ path ^ " is not a valid TOML file: " ^ TomlParseError.toString e); ConfigFile.defaultConfig)
                                               )

fun main () = let val (options, rest) = HandleOptions.parse (AppOptions.init, CommandLine.arguments ())
                 val config = loadConfig (#config_file options)

                 (* Apply colors *)
                 val () = Option.app Message.setTypeStyle (#type_ (#color config))
                 val () = Option.app Message.setExecuteStyle (#execute (#color config))
                 val () = Option.app Message.setErrorStyle (#error (#color config))
                 val () = Option.app Message.setWarningStyle (#warning (#color config))
                 val () = Option.app Message.setDiagnosticStyle (#diagnostic (#color config))
                 val () = Option.app Message.setInformationStyle (#information (#color config))

                 val watch = #watch options
                 val () = case #color options of
                              NONE => Message.setColors Message.AUTO
                            | _ => ()
                 val inputfile = case rest of
                                     [] => showUsage () (* No input file given *)
                                   | [input] => input
                                   | _ => ( Message.error "Multiple input files are not supported."
                                          ; OS.Process.exit OS.Process.failure
                                          )
                 val engine = case #engine options of
                                  SOME name => (case TeXEngine.get name of
                                                    SOME engine => engine
                                                  | NONE => ( Message.error ("Unknown engine name '" ^ name ^ "'.")
                                                            ; OS.Process.exit OS.Process.failure
                                                            )
                                               )
                                | NONE => let val name = CommandLine.name ()
                                              val basename = PathUtil.trimext (PathUtil.basename name)
                                              (* If run as 'cl<engine name>' (e.g. 'cllualatex'), then the default engine is <engine name>. *)
                                              fun notSpecified () = ( Message.error "Engine not specified."
                                                                    ; OS.Process.exit OS.Process.failure
                                                                    )
                                          in if String.isPrefix "cl" basename andalso CharVector.all Char.isAlphaNum basename then
                                                 case TeXEngine.get (String.extract (basename, 2, NONE)) of
                                                     NONE => notSpecified ()
                                                   | SOME engine => engine
                                             else
                                                 notSpecified ()
                                          end
                 val output_format = Option.getOpt (#output_format options, OutputFormat.PDF)
                 val check_driver = case output_format of
                                        OutputFormat.PDF =>
                                        ( case #check_driver options of
                                              NONE => ()
                                            | SOME _ => ( Message.error ("--check-driver can only be used when the output format is DVI.")
                                                        ; OS.Process.exit OS.Process.failure
                                                        )
                                        ; if #supports_pdf_generation engine then
                                              if TeXEngine.isLuaTeX engine then
                                                  SOME CheckDriver.LUATEX
                                              else if TeXEngine.isXeTeX engine then
                                                  SOME CheckDriver.XETEX
                                              else if TeXEngine.isPdfTeX engine then
                                                  SOME CheckDriver.PDFTEX
                                              else
                                                  ( Message.warn ("Unknown engine: " ^ #name engine)
                                                  ; Message.warn "Driver check will not work."
                                                  ; NONE
                                                  )
                                          else
                                              (* ClutTeX uses dvipdfmx to generate PDF from DVI output *)
                                              SOME CheckDriver.DVIPDFMX
                                        )
                                      | OutputFormat.DVI =>
                                        case #check_driver options of
                                            SOME AppOptions.DviDriver.DVIPDFMX => SOME CheckDriver.DVIPDFMX
                                          | SOME AppOptions.DviDriver.DVIPS => SOME CheckDriver.DVIPS
                                          | SOME AppOptions.DviDriver.DVISVGM => SOME CheckDriver.DVISVGM
                                          | NONE => NONE
                 val (jobname, jobname_for_output) = case #jobname options of
                                                         SOME jobname => (jobname, jobname)
                                                       | NONE => let val basename = PathUtil.basename (PathUtil.trimext inputfile)
                                                                 in (SafeName.escapeJobname basename, basename)
                                                                 end
                 val output_extension = case output_format of
                                            OutputFormat.DVI => #dvi_extension engine (* "dvi" or "xdv" *)
                                          | OutputFormat.PDF => "pdf"
                 val output_from_original_wd = case #output options of
                                                   NONE => jobname_for_output ^ "." ^ output_extension
                                                 | SOME output => output
                 val output_directory_from_original_wd
                     = case #output_directory options of
                           SOME dir => if #fresh options then
                                           ( Message.error "--fresh and --output-directory cannot be used together."
                                           ; OS.Process.exit OS.Process.failure
                                           )
                                       else
                                           dir
                         | NONE => let val inputfile_abs = PathUtil.abspath { path = inputfile, cwd = NONE }
                                       val output_directory = genOutputDirectory (#temporary_directory config, [inputfile_abs, jobname, Option.getOpt (#engine_executable options, #executable engine)])
                                   in if not (FSUtil.isDirectory output_directory) then
                                          FSUtil.mkDirRec output_directory
                                      else if #fresh options then
                                          ( if Message.getVerbosity () >= 1 then
                                                Message.info ("Cleaning '" ^ output_directory ^ "'...")
                                            else
                                                ()
                                          ; FSUtil.removeRec output_directory
                                          ; OS.FileSys.mkDir output_directory
                                          )
                                      else
                                          ()
                                    ; output_directory
                                   end

                 val () = if #print_output_directory options then
                              ( print (output_directory_from_original_wd ^ "\n")
                              ; OS.Process.exit OS.Process.success
                              )
                          else
                              ()

                 val pathsep = if OSUtil.isWindows then
                                   ";"
                               else
                                   ":"

                 val original_wd = OS.FileSys.getDir ()
                 val (output, output_directory, tex_output_directory)
                     = if Option.getOpt (#change_directory options, false) then
                           let val TEXINPUTS = Option.getOpt (OS.Process.getEnv "TEXINPUTS", "")
                               val LUAINPUTS = Option.getOpt (OS.Process.getEnv "LUAINPUTS", "")
                               val () = OS.FileSys.chDir output_directory_from_original_wd
                               val () = OSUtil.setEnv ("TEXINPUTS", original_wd ^ pathsep ^ TEXINPUTS)
                               val () = OSUtil.setEnv ("LUAINPUTS", original_wd ^ pathsep ^ LUAINPUTS)
                           in (PathUtil.abspath { path = output_from_original_wd, cwd = SOME original_wd }, ".", NONE)
                           end
                       else
                           (output_from_original_wd, output_directory_from_original_wd, SOME output_directory_from_original_wd)
                 val output = case #bibtex_or_biber options of
                                  SOME _ => let val BIBINPUTS = Option.getOpt (OS.Process.getEnv "BIBINPUTS", "")
                                                val () = OSUtil.setEnv ("BIBINPUTS", original_wd ^ pathsep ^ BIBINPUTS)
                                            in PathUtil.abspath { path = output_from_original_wd, cwd = SOME original_wd } (* Is this needed? *)
                                            end
                                | NONE => output

                 (*
                  * Set `max_print_line' environment variable if not already set.
                  *
                  * According to texmf.cnf:
                  *   45 < error_line < 255,
                  *   30 < half_error_line < error_line - 15,
                  *   60 <= max_print_line.
                  *
                  * On TeX Live 2023, (u)(p)bibtex fails if max_print_line >= 20000.
                  *)
                 val () = case OS.Process.getEnv "max_print_line" of
                              NONE => OSUtil.setEnv ("max_print_line", "16384")
                            | SOME _ => ()

                 fun pathInOutputDirectory ext = PathUtil.join2 (output_directory, jobname ^ "." ^ ext)

                 val recorderfile = pathInOutputDirectory "fls"
                 val recorderfile2 = pathInOutputDirectory "cluttex-fls"

                 val tex_output_format = case output_format of
                                             OutputFormat.DVI => OutputFormat.DVI
                                           | OutputFormat.PDF => if #supports_pdf_generation engine then
                                                                     OutputFormat.PDF
                                                                 else
                                                                     OutputFormat.DVI

                 (* Setup LuaTeX initialization script *)
                 val lua_initialization_script
                     = if TeXEngine.isLuaTeX engine then
                           let val initscriptfile = pathInOutputDirectory "cluttexinit.lua"
                           in LuaTeXInit.createInitializationScript (initscriptfile, { file_line_error = #file_line_error options, halt_on_error = #halt_on_error options, output_directory = output_directory, jobname = jobname })
                            ; SOME initscriptfile
                           end
                       else
                           NONE

                 (* Set SOURCE_DATE_EPOCH if --source-date-epoch=<timestamp> is set *)
                 val source_date_epoch_info = case #source_date_epoch options of
                     SOME (AppOptions.SourceDateEpoch.RAW raw) =>
                     (OSUtil.setEnv ("SOURCE_DATE_EPOCH", raw); NONE)
                   | _ => if #source_date_epoch options = SOME AppOptions.SourceDateEpoch.NOW orelse OS.Process.getEnv "SOURCE_DATE_EPOCH" = NONE then
                              SOME (ref { time_since_epoch = getTimeSinceEpoch (), time = Time.now () })
                          else
                              NONE

                 val tex_options : TeXEngine.run_options
                     = { engine_executable = #engine_executable options
                       , interaction = SOME (Option.getOpt (#interaction options, InteractionMode.NONSTOPMODE))
                       , file_line_error = #file_line_error options
                       , halt_on_error = #halt_on_error options
                       , synctex = #synctex options
                       , output_directory = tex_output_directory
                       , shell_escape = #shell_escape options
                       , jobname = SOME jobname
                       , fmt = #fmt options
                       , extra_options = #tex_extraoptions options
                       , output_format = tex_output_format
                       , draftmode = false
                       , lua_initialization_script = lua_initialization_script
                       }
                 val options : AppOptions.options
                     = { engine = engine
                       , engine_executable = #engine_executable options
                       , output = output
                       , fresh = #fresh options
                       , max_iterations = Option.getOpt (#max_iterations options, 4)
                       , start_with_draft = #start_with_draft options
                       , watch = #watch options
                       , change_directory = Option.getOpt (#change_directory options, false)
                       , includeonly = #includeonly options
                       , make_depends = #make_depends options
                       , print_output_directory = #print_output_directory options
                       , package_support = #package_support options
                       , check_driver = check_driver
                       , source_date_epoch = #source_date_epoch options
                       , synctex = #synctex options
                       , file_line_error = #file_line_error options
                       , interaction = Option.getOpt (#interaction options, InteractionMode.NONSTOPMODE)
                       , halt_on_error = #halt_on_error options
                       , shell_escape = #shell_escape options
                       , jobname = jobname
                       , fmt = #fmt options
                       , output_directory = output_directory
                       , output_format = output_format
                       , tex_extraoptions = #tex_extraoptions options
                       , dvipdfmx_extraoptions = #dvipdfmx_extraoptions options
                       , makeindex = #makeindex options
                       , bibtex_or_biber = #bibtex_or_biber options
                       , makeglossaries = #makeglossaries options
                       }
                 val run_params = { options = options
                                  , inputfile = inputfile
                                  , engine = engine
                                  , tex_options = tex_options
                                  , recorderfile = recorderfile
                                  , recorderfile2 = recorderfile2
                                  , original_wd = original_wd
                                  , output_extension = output_extension
                                  , source_date_epoch_info = source_date_epoch_info
                                  }
             in case watch of
                    NONE => (doTypeset run_params handle Abort => OS.Process.exit OS.Process.failure)
                  | SOME watch_engine => runWatchMode (watch_engine, run_params)
             end
end;
val () = Main.main ();