if not modules then modules = { } end modules['mtx-context'] = { version = 1.001, comment = "companion to mtxrun.lua", author = "Hans Hagen, PRAGMA-ADE, Hasselt NL", copyright = "PRAGMA ADE / ConTeXt Development Team", license = "see context related readme files" } -- todo: more local functions -- todo: pass jobticket/ctxdata table around local type, next, tostring, tonumber = type, next, tostring, tonumber local format, gmatch, match, gsub, find = string.format, string.gmatch, string.match, string.gsub, string.find local quote, validstring, splitstring = string.quote, string.valid, string.split local sort, concat, insert, sortedhash, tohash = table.sort, table.concat, table.insert, table.sortedhash, table.tohash local settings_to_array = utilities.parsers.settings_to_array local appendtable = table.append local lpegpatterns, lpegmatch, Cs, P = lpeg.patterns, lpeg.match, lpeg.Cs, lpeg.P local getargument = environment.getargument or environment.argument local setargument = environment.setargument local filejoinname = file.join local filebasename = file.basename local filenameonly = file.nameonly local filepathpart = file.pathpart local filesuffix = file.suffix local fileaddsuffix = file.addsuffix local filenewsuffix = file.replacesuffix local removesuffix = file.removesuffix local validfile = lfs.isfile local removefile = os.remove local renamefile = os.rename local formatters = string.formatters local starttiming = statistics.starttiming local stoptiming = statistics.stoptiming local elapsedtime = statistics.elapsedtime local application = logs.application { name = "mtx-context", banner = "ConTeXt Process Management 1.05", -- helpinfo = helpinfo, -- table with { category_a = text_1, category_b = text_2 } or helpstring or xml_blob helpinfo = "mtx-context.xml", } -- local luatexflags = { -- ["8bit"] = true, -- ignored, input is assumed to be in UTF-8 encoding -- ["default-translate-file"] = true, -- ignored, input is assumed to be in UTF-8 encoding -- ["translate-file"] = true, -- ignored, input is assumed to be in UTF-8 encoding -- ["etex"] = true, -- ignored, the etex extensions are always active -- ["parse-first-line"] = true, -- ignored, enable parsing of the first line of the input file -- ["no-parse-first-line"] = true, -- ignored, disable parsing of the first line of the input file -- -- ["credits"] = true, -- display credits and exit -- ["debug-format"] = true, -- enable format debugging -- ["disable-write18"] = true, -- disable \write18{SHELL COMMAND} -- ["draftmode"] = true, -- switch on draft mode (generates no output PDF) -- ["enable-write18"] = true, -- enable \write18{SHELL COMMAND} -- ["file-line-error"] = true, -- enable file:line:error style messages -- ["file-line-error-style"] = true, -- aliases of --file-line-error -- ["no-file-line-error"] = true, -- disable file:line:error style messages -- ["no-file-line-error-style"] = true, -- aliases of --no-file-line-error -- ["fmt"] = true, -- load the format file FORMAT -- ["halt-on-error"] = true, -- stop processing at the first error -- ["help"] = true, -- display help and exit -- ["ini"] = true, -- be iniluatex, for dumping formats -- ["interaction"] = true, -- set interaction mode (STRING=batchmode/nonstopmode/scrollmode/errorstopmode) -- ["jobname"] = true, -- set the job name to STRING -- ["kpathsea-debug"] = true, -- set path searching debugging flags according to the bits of NUMBER -- ["lua"] = true, -- load and execute a lua initialization script -- ["mktex"] = true, -- enable mktexFMT generation (FMT=tex/tfm) -- ["no-mktex"] = true, -- disable mktexFMT generation (FMT=tex/tfm) -- ["nosocket"] = true, -- disable the lua socket library -- ["output-comment"] = true, -- use STRING for DVI file comment instead of date (no effect for PDF) -- ["output-directory"] = true, -- use existing DIR as the directory to write files in -- ["output-format"] = true, -- use FORMAT for job output; FORMAT is 'dvi' or 'pdf' -- ["progname"] = true, -- set the program name to STRING -- ["recorder"] = true, -- enable filename recorder -- ["safer"] = true, -- disable easily exploitable lua commands -- ["shell-escape"] = true, -- enable \write18{SHELL COMMAND} -- ["no-shell-escape"] = true, -- disable \write18{SHELL COMMAND} -- ["shell-restricted"] = true, -- restrict \write18 to a list of commands given in texmf.cnf -- ["nodates"] = true, -- no production dates in pdf file -- ["trailerid"] = true, -- alternative trailer id -- ["synctex"] = true, -- enable synctex -- ["version"] = true, -- display version and exit -- ["luaonly"] = true, -- run a lua file, then exit -- ["luaconly"] = true, -- byte-compile a lua file, then exit -- ["jiton"] = false, -- not supported (makes no sense, slower) -- } local report = application.report scripts = scripts or { } scripts.context = scripts.context or { } -- for the moment here if jit then -- already luajittex setargument("engine","luajittex") setargument("jit",nil) elseif getargument("luatex") then -- relaunch luajittex setargument("engine","luatex") elseif getargument("jit") or getargument("luajittex") then -- relaunch luajittex -- bonus shortcut, we assume that --jit also indicates the engine -- although --jit and --engine=luajittex are independent setargument("engine","luajittex") end -- -- The way we use stubs will change in a bit in 2019 (mtxrun and context). We also normalize -- -- the platforms to use a similar approach to this. local engine_new = filenameonly(getargument("engine") or directives.value("system.engine")) local engine_old = filenameonly(environment.ownmain) or filenameonly(environment.ownbin) local function restart(engine_old,engine_new) local generate = environment.arguments.generate and (engine_new == "luatex" or engine_new == "luajittex") local arguments = generate and "--generate" or environment.reconstructcommandline() local ownname = filejoinname(filepathpart(environment.ownname),"mtxrun.lua") local command = format("%s --luaonly --socket %q %s --redirected",engine_new,ownname,arguments) report(format("redirect %s -> %s: %s",engine_old,engine_new,command)) local result = os.execute(command) os.exit(result == 0 and 0 or 1) end -- if getargument("redirected") then -- setargument("engine",engine_old) -- later on we need this -- elseif engine_new == engine_old then -- setargument("engine",engine_new) -- later on we need this -- elseif environment.validengines[engine_new] and engine_new ~= environment.basicengines[engine_old] then -- restart(engine_old,engine_new) -- else -- setargument("engine",engine_new) -- later on we need this -- end if environment.validengines[engine_new] and engine_new ~= environment.basicengines[engine_old] then restart(engine_old,engine_new) end -- so far -- constants local usedfiles = { nop = "cont-nop.mkiv", yes = "cont-yes.mkiv", } local usedsuffixes = { before = { "tuc" }, after = { "pdf", "tuc", "log" }, keep = { "log" }, } local formatofinterface = { en = "cont-en", uk = "cont-uk", de = "cont-de", fr = "cont-fr", nl = "cont-nl", cs = "cont-cs", it = "cont-it", ro = "cont-ro", pe = "cont-pe", } local defaultformats = { "cont-en", -- "cont-nl", } -- purging files (we should have an mkii and mkiv variants) local generic_files = { "texexec.tex", "texexec.tui", "texexec.tuo", "texexec.tuc", "texexec.tua", "texexec.ps", "texexec.pdf", "texexec.dvi", "cont-opt.tex", "cont-opt.bak" } local obsolete_results = { "dvi", } local temporary_runfiles = { "tui", -- mkii two pass file "tua", -- mkiv obsolete "tup", "ted", "tes", -- texexec "top", -- mkii options file "log", -- tex log file "tmp", -- mkii buffer file "run", -- mkii stub "bck", -- backup (obsolete) "rlg", -- resource log "ctl", -- "mpt", "mpx", "mpd", "mpo", "mpb", -- metafun "prep", -- context preprocessed "pgf", -- tikz "aux", "blg", -- bibtex } local temporary_suffixes = { "prep", -- context preprocessed } local synctex_runfiles = { "synctex", "synctex.gz", "syncctx" -- synctex } local persistent_runfiles = { "tuo", -- mkii two pass file "tub", -- mkii buffer file "top", -- mkii options file "tuc", -- mkiv two pass file "bbl", -- bibtex } local special_runfiles = { "%-mpgraph", "%-mprun", "%-temp%-", } local extra_runfiles = { "^m_k_i_v_.-%.pdf$", "^l_m_t_x_.-%.pdf$", } local function purge_file(dfile,cfile) if cfile and validfile(cfile) then if removefile(dfile) then return filebasename(dfile) end elseif dfile then if removefile(dfile) then return filebasename(dfile) end end end -- process information local ctxrunner = { } -- namespace will go local ctx_locations = { '..', '../..' } function ctxrunner.new() return { ctxname = "", jobname = "", flags = { }, } end function ctxrunner.checkfile(ctxdata,ctxname,defaultname) if not ctxdata.jobname or ctxdata.jobname == "" or getargument("noctx") then return end ctxdata.ctxname = ctxname or removesuffix(ctxdata.jobname) or "" if ctxdata.ctxname == "" then return end ctxdata.jobname = fileaddsuffix(ctxdata.jobname,'tex') ctxdata.ctxname = fileaddsuffix(ctxdata.ctxname,'ctx') report("jobname: %s",ctxdata.jobname) report("ctxname: %s",ctxdata.ctxname) -- mtxrun should resolve kpse: and file: local usedname = ctxdata.ctxname local found = validfile(usedname) -- no further test if qualified path if not found then for _, path in next, ctx_locations do local fullname = filejoinname(path,ctxdata.ctxname) if validfile(fullname) then usedname = fullname found = true break end end end if not found then usedname = resolvers.findfile(ctxdata.ctxname,"tex") found = usedname ~= "" end if not found and defaultname and defaultname ~= "" and validfile(defaultname) then usedname = defaultname found = true end if not found then return end local xmldata = xml.load(usedname) if not xmldata then return else -- test for valid, can be text file end local ctxpaths = table.append({'.', filepathpart(ctxdata.ctxname)}, ctx_locations) xml.include(xmldata,'ctx:include','name', ctxpaths) local flags = ctxdata.flags for e in xml.collected(xmldata,"/ctx:job/ctx:flags/ctx:flag") do local flag = xml.text(e) or "" local key, value = match(flag,"^(.-)=(.+)$") if key and value then flags[key] = value else flags[flag] = true end end end function ctxrunner.checkflags(ctxdata) if ctxdata then for k,v in next, ctxdata.flags do if getargument(k) == nil then setargument(k,v) end end end end -- multipass control local multipass_suffixes = { ".tuc" } local multipass_nofruns = 9 -- better for tracing oscillation local multipass_forcedruns = false local function multipass_hashfiles(jobname) local hash = { } for i=1,#multipass_suffixes do local suffix = multipass_suffixes[i] local full = jobname .. suffix hash[full] = md5.hex(io.loaddata(full) or "unknown") end return hash end local function multipass_changed(oldhash, newhash) for k,v in next, oldhash do if v ~= newhash[k] then return true end end return false end local f_tempfile_i = formatters["%s-%s-%02d.tmp"] local f_tempfile_s = formatters["%s-%s-keep.%s"] local function backup(jobname,run,kind,filename) if run then if run == 1 then for i=1,10 do local tmpname = f_tempfile_i(jobname,kind,i) if validfile(tmpname) then removefile(tmpname) report("removing %a",tmpname) end end end if validfile(filename) then local tmpname = f_tempfile_i(jobname,kind,run or 1) report("copying %a into %a",filename,tmpname) file.copy(filename,tmpname) else report("no file %a, nothing kept",filename) end elseif validfile(filename) then local tmpname = f_tempfile_s(jobname,kind,kind) report("copying %a into %a",filename,tmpname) file.copy(filename,tmpname) else report("no file %a, nothing kept",filename) end end local function multipass_copyluafile(jobname,run) local tuaname, tucname = jobname..".tua", jobname..".tuc" if validfile(tuaname) then if run then backup(jobname,run,"tuc",tucname) report("copying %a into %a",tuaname,tucname) report() end removefile(tucname) renamefile(tuaname,tucname) end end local function multipass_copypdffile(jobname,run) if run then local pdfname = jobname..".pdf" if validfile(pdfname) then backup(jobname,false,"pdf",pdfname) report() end end end local function multipass_copylogfile(jobname,run) if run then local logname = jobname..".log" if validfile(logname) then backup(jobname,run,"log",logname) report() end end end -- local pattern = lpegpatterns.utfbom^-1 * (P("%% ") + P("% ")) * Cs((1-lpegpatterns.newline)^1) local prefile = nil local predata = nil local function preamble_analyze(filename) -- only files on current path filename = fileaddsuffix(filename,"tex") -- to be sure if predata and prefile == filename then return predata end prefile = filename predata = { } local line = io.loadlines(prefile) if line then local preamble = lpegmatch(pattern,line) if preamble then utilities.parsers.options_to_hash(preamble,predata) predata.type = "tex" elseif find(line,"^ 0 then -- the list of given files is processed using the stub file mainfile = usedfiles.yes -- this can become "" for luametatex/lmtx filelist = files files = { } else return end -- local interface = validstring(getargument("interface")) or "en" local formatname = formatofinterface[interface] or "cont-en" local formatfile, scriptfile = resolvers.locateformat(formatname) -- regular engine ! if not formatfile or not scriptfile then report("warning: no format found, forcing remake (commandline driven)") scripts.context.make(formatname) formatfile, scriptfile = resolvers.locateformat(formatname) -- variant end if formatfile and scriptfile then -- okay elseif formatname then report("error, no format found with name: %s, aborting",formatname) return else report("error, no format found (provide formatname or interface)") return end -- local a_mkii = getargument("mkii") or getargument("pdftex") or getargument("xetex") local a_purge = getargument("purge") local a_purgeall = getargument("purgeall") local a_purgeresult = getargument("purgeresult") local a_global = getargument("global") local a_runpath = getargument("runpath") local a_timing = getargument("timing") local a_profile = getargument("profile") local a_batchmode = getargument("batchmode") local a_nonstopmode = getargument("nonstopmode") local a_scollmode = getargument("scrollmode") local a_once = getargument("once") local a_backend = getargument("backend") local a_arrange = getargument("arrange") local a_noarrange = getargument("noarrange") local a_jithash = getargument("jithash") local a_permitloadlib = getargument("permitloadlib") local a_texformat = getargument("texformat") local a_notuc = getargument("notuc") local a_keeptuc = getargument("keeptuc") local a_keeplog = getargument("keeplog") local a_keeppdf = getargument("keeppdf") local a_export = getargument("export") local a_nodates = getargument("nodates") local a_trailerid = getargument("trailerid") local a_nocompression = getargument("nocompression") -- a_batchmode = (a_batchmode and "batchmode") or (a_nonstopmode and "nonstopmode") or (a_scrollmode and "scrollmode") or nil -- local changed = { } -- for i=1,#filelist do -- local filename = filelist[i] if filename == "" then report("warning: bad filename") break end local basename = filebasename(filename) -- use splitter local pathname = filepathpart(filename) -- if filesuffix(filename) == "" then filename = fileaddsuffix(filename,"tex") end -- if pathname == "" and not a_global and filename ~= usedfiles.nop then filename = "./" .. filename if not validfile(filename) then report("warning: no (local) file %a, proceeding",filename) end end -- local jobname = removesuffix(basename) -- local jobname = removesuffix(filename) local ctxname = ctxdata and ctxdata.ctxname -- if changed[jobname] == nil then changed[jobname] = false end -- local analysis = preamble_analyze(filename) -- if a_mkii or analysis.engine == 'pdftex' or analysis.engine == 'xetex' then run_texexec(filename,a_purge,a_purgeall) elseif plain_format(a_texformat or analysis.texformat) then run_plain(a_texformat or analysis.texformat,filename) else if analysis.interface and analysis.interface ~= interface then formatname = formatofinterface[analysis.interface] or formatname formatfile, scriptfile = resolvers.locateformat(formatname) end -- local runpath = a_runpath or analysis.runpath if type(runpath) == "string" and runpath ~= "" then runpath = resolvers.resolve(runpath) local currentdir = dir.current() if not lfs.isdir(runpath) then if dir.makedirs(runpath) then report("runpath %a has been created",runpath) else report("error: runpath %a cannot be created",runpath) os.exit() end end if lfs.chdir(runpath) then report("changing to runpath %a",runpath) else report("error: changing to runpath %a is impossible",runpath) os.exit() end environment.arguments.path = currentdir environment.arguments.runpath = runpath if filepathpart(filename) == "." then filename = filebasename(filename) end end -- a_jithash = validstring(a_jithash or analysis.jithash) or nil a_permitloadlib = a_permitloadlib or analysis.permitloadlib or nil -- if not formatfile or not scriptfile then report("warning: no format found, forcing remake (source driven)") scripts.context.make(formatname,a_engine) formatfile, scriptfile = resolvers.locateformat(formatname) end -- local function combine(key) local flag = validstring(environment[key]) local plus = analysis[key] if flag and plus then return plus .. "," .. flag -- flag wins else return flag or plus -- flag wins end end ----- a_trackers = analysis.trackers ----- a_experiments = analysis.experiments local directives = combine("directives") local trackers = combine("trackers") local experiments = combine("experiments") -- local ownerpassword = environment.ownerpassword or analysis.ownerpassword local userpassword = environment.userpassword or analysis.userpassword local permissions = environment.permissions or analysis.permissions -- if formatfile and scriptfile then local suffix = validstring(getargument("suffix")) local resultname = validstring(getargument("result")) if not resultname or resultname == "" then resultname = validstring(analysis.result) end local resultpath = filepathpart(resultname) if resultpath ~= "" then resultname = nil elseif suffix then resultname = removesuffix(jobname) .. suffix end local oldbase = "" local newbase = "" if resultname then oldbase = removesuffix(jobname) newbase = removesuffix(resultname) if oldbase ~= newbase then if a_purgeresult then result_push_purge(oldbase,newbase) else result_push_keep(oldbase,newbase) end else resultname = nil end end -- local pdfview = getargument("autopdf") or getargument("closepdf") if pdfview then pdf_close(filename,pdfview) if resultname then pdf_close(resultname,pdfview) end end -- -- we could do this when locating the format and exit from luatex when -- there is a version mismatch .. that way we can use stock luatex -- plus mtxrun to run luajittex instead .. this saves a restart but is -- also cleaner as then mtxrun only has to check for a special return -- code (signaling a make + rerun) .. maybe some day -- local okay = statistics.checkfmtstatus(formatfile,a_engine) if okay ~= true then report("warning: %s, forcing remake",tostring(okay)) scripts.context.make(formatname) end -- local oldhash = multipass_hashfiles(jobname) local newhash = { } local maxnofruns = once and 1 or multipass_nofruns local fulljobname = validstring(filename) -- local c_flags = { directives = directives, -- gets passed via mtxrun trackers = trackers, -- gets passed via mtxrun experiments = experiments, -- gets passed via mtxrun -- result = validstring(resultname), input = validstring(getargument("input") or filename), -- alternative input fulljobname = fulljobname, files = concat(files,","), ctx = validstring(ctxname), export = a_export and true or nil, nocompression = a_nocompression and true or nil, texmfbinpath = os.selfdir, -- ownerpassword = ownerpassword, userpassword = userpassword, permissions = permissions, } -- for k, v in next, environment.arguments do -- the raw arguments if c_flags[k] == nil then c_flags[k] = v end end -- -- todo: --output-file=... in luatex -- local usedname = jobname local engine = analysis.engine or "luametatex" if engine == "luametatex" and (mainfile == usedfiles.yes or mainfile == usedfiles.nop) and not getargument("redirected") then mainfile = "" -- we don't need that usedname = fulljobname end -- -- local l_flags = { ["interaction"] = a_batchmode, -- ["synctex"] = false, -- context has its own way -- ["no-parse-first-line"] = true, -- obsolete -- ["safer"] = a_safer, -- better use --sandbox -- ["no-mktex"] = true, -- ["file-line-error-style"] = true, -- ["fmt"] = formatfile, -- ["lua"] = scriptfile, -- ["jobname"] = jobname, ["jobname"] = usedname, ["jithash"] = a_jithash, ["permitloadlib"] = a_permitloadlib, } -- local directives = { } -- -- todo: handle these at the tex end -- if a_nodates then directives[#directives+1] = format("backend.date=%s",type(a_nodates) == "string" and a_nodates or "no") end -- if type(a_trailerid) == "string" then directives[#directives+1] = format("backend.trailerid=%s",a_trailerid) end -- if a_profile then directives[#directives+1] = format("system.profile=%s",tonumber(a_profile) or 0) end -- -- if a_notuc then -- removefile(fileaddsuffix(jobname,"tuc")) -- directives[#directives+1] = "job.save=no" -- handled at tex end -- end -- for i=1,#synctex_runfiles do removefile(fileaddsuffix(jobname,synctex_runfiles[i])) end -- if #directives > 0 then c_flags.directives = concat(directives,",") end -- -- kindofrun: 1:first run, 2:successive run, 3:once, 4:last of maxruns -- -- can be used to include pages from a previous run, --keeppdf or "% keeppdf" on first-line -- multipass_copypdffile(jobname,a_keeppdf or analysis.keeppdf) -- for currentrun=1,maxnofruns do -- c_flags.final = false c_flags.kindofrun = (a_once and 3) or (currentrun==1 and 1) or (currentrun==maxnofruns and 4) or 2 c_flags.maxnofruns = maxnofruns c_flags.forcedruns = multipass_forcedruns and multipass_forcedruns > 0 and multipass_forcedruns or nil c_flags.currentrun = currentrun c_flags.noarrange = a_noarrange or a_arrange or nil c_flags.profile = a_profile and (tonumber(a_profile) or 0) or nil -- print("") -- cleaner, else continuation on same line -- local returncode = environment.run_format( formatfile, scriptfile, mainfile, flags_to_string(l_flags), flags_to_string(c_flags,true), verbose ) -- todo: remake format when no proper format is found if not returncode then report("fatal error: no return code") if resultname then result_save_error(oldbase,newbase) end os.exit(1) break elseif returncode == 0 then multipass_copyluafile(jobname,a_keeptuc and currentrun) multipass_copylogfile(jobname,a_keeplog and currentrun) if not multipass_forcedruns then newhash = multipass_hashfiles(jobname) if multipass_changed(oldhash,newhash) then changed[jobname] = true oldhash = newhash else break end elseif currentrun == multipass_forcedruns then report("quitting after force %i runs",multipass_forcedruns) break end else report("fatal error: return code: %s",returncode or "?") if resultname then result_save_error(oldbase,newbase) end os.exit(1) -- (returncode) break end -- end -- if environment.arguments["ansilog"] then local logfile = filenewsuffix(jobname,"log") local logdata = io.loaddata(logfile) or "" if logdata ~= "" then io.savedata(logfile,(gsub(logdata,"%[.-m",""))) end end -- -- -- this will go away after we update luatex -- local syncctx = fileaddsuffix(jobname,"syncctx") if validfile(syncctx) then renamefile(syncctx,fileaddsuffix(jobname,"synctex")) end -- if a_arrange then -- c_flags.final = true c_flags.kindofrun = 3 c_flags.currentrun = c_flags.currentrun + 1 c_flags.noarrange = nil -- report("arrange run: %s",command) -- local returncode = environment.run_format( formatfile, scriptfile, mainfile, flags_to_string(l_flags), flags_to_string(c_flags,true), verbose ) -- if not returncode then report("fatal error: no return code, message: %s",errorstring or "?") os.exit(1) elseif returncode > 0 then report("fatal error: return code: %s",returncode or "?") os.exit(returncode) end -- end -- if a_purge then scripts.context.purge_job(jobname,false,false,fulljobname) elseif a_purgeall then scripts.context.purge_job(jobname,true,false,fulljobname) end -- if resultname then if a_purgeresult then -- so, if there is no result then we don't get the old one, but -- related files (log etc) are still there for tracing purposes result_save_purge(oldbase,newbase) else result_save_keep(oldbase,newbase) end report("result renamed to: %s",newbase) elseif resultpath ~= "" then report() report("results are to be on the running path, not on %a, ignoring --result",resultpath) report() end -- -- -- needs checking -- -- if a_purge then -- scripts.context.purge_job(resultname) -- elseif a_purgeall then -- scripts.context.purge_job(resultname,true) -- end -- local pdfview = getargument("autopdf") if pdfview then pdf_open(resultname or jobname,pdfview) end -- local epub = analysis.epub if epub then if type(epub) == "string" then local t = settings_to_array(epub) for i=1,#t do t[i] = "--" .. gsub(t[i],"^%-*","") end epub = concat(t," ") else epub = "--make" end local command = "mtxrun --script epub " .. epub .. " " .. jobname report() report("making epub file: ",command) report() os.execute(command) -- todo: also a runner end -- if a_timing then report() report("you can process (timing) statistics with:",jobname) report() report("context --extra=timing '%s'",jobname) -- report("mtxrun --script timing --xhtml [--launch --remove] '%s'",jobname) report() end else if formatname then report("error, no format found with name: %s, skipping",formatname) else report("error, no format found (provide formatname or interface)") end break end end end -- if #filelist > 1 then local done = false for k, v in sortedhash(changed) do if v then if not done then report() done = true end report("file %a was changed",k) end end if done then report() end end end function scripts.context.pipe() -- still used? -- context --pipe -- context --pipe --purge --dummyfile=whatever.tmp local interface = getargument("interface") interface = (type(interface) == "string" and interface) or "en" local formatname = formatofinterface[interface] or "cont-en" local formatfile, scriptfile = resolvers.locateformat(formatname) if not formatfile or not scriptfile then report("warning: no format found, forcing remake (commandline driven)") scripts.context.make(formatname) formatfile, scriptfile = resolvers.locateformat(formatname) end if formatfile and scriptfile then local okay = statistics.checkfmtstatus(formatfile) if okay ~= true then report("warning: %s, forcing remake",tostring(okay)) scripts.context.make(formatname) end local l_flags = { interaction = "scrollmode", fmt = formatfile, lua = scriptfile, } local c_flags = { backend = "pdf", final = false, kindofrun = 3, currentrun = 1, } local filename = getargument("dummyfile") or "" if filename == "" then filename = "\\relax" report("entering scrollmode, end job with \\end") else filename = fileaddsuffix(filename,"tmp") io.savedata(filename,"\\relax") report("entering scrollmode using '%s' with optionfile, end job with \\end",filename) end local returncode = environment.run_format( formatfile, scriptfile, filename, flags_to_string(l_flags), flags_to_string(c_flags,true), verbose ) if getargument("purge") then scripts.context.purge_job(filename) elseif getargument("purgeall") then scripts.context.purge_job(filename,true) removefile(filename) end elseif formatname then report("error, no format found with name: %s, aborting",formatname) else report("error, no format found (provide formatname or interface)") end end local function make_mkiv_format(name,engine) environment.make_format(name) -- jit is picked up later end local make_mkii_format do -- more or less copied from mtx-plain.lua: local function mktexlsr() if environment.arguments.silent then local result = os.execute("mktexlsr --quiet > temp.log") if result ~= 0 then print("mktexlsr silent run > fatal error") -- we use a basic print else print("mktexlsr silent run") -- we use a basic print end removefile("temp.log") else report("running mktexlsr") os.execute("mktexlsr") end end local function engine(texengine,texformat) local command = string.format('%s --ini --etex --8bit %s \\dump',texengine,fileaddsuffix(texformat,"mkii")) if environment.arguments.silent then starttiming() local command = format("%s > temp.log",command) local result = os.execute(command) local runtime = stoptiming() if result ~= 0 then print(format("%s silent make > fatal error when making format %q",texengine,texformat)) -- we use a basic print else print(format("%s silent make > format %q made in %.3f seconds",texengine,texformat,runtime)) -- we use a basic print end removefile("temp.log") else report("running command: %s",command) os.execute(command) end end local function resultof(...) local command = string.format(...) report("running command %a",command) return string.strip(os.resultof(command) or "") end local function make(texengine,texformat) report("generating kpse file database") mktexlsr() local fmtpathspec = resultof("kpsewhich --var-value=TEXFORMATS --engine=%s",texengine) if fmtpathspec ~= "" then report("using path specification %a",fmtpathspec) fmtpathspec = resultof('kpsewhich -expand-braces="%s"',fmtpathspec) end if fmtpathspec ~= "" then report("using path expansion %a",fmtpathspec) else report("no valid path reported, trying alternative") fmtpathspec = resultof("kpsewhich --show-path=fmt --engine=%s",texengine) if fmtpathspec ~= "" then report("using path expansion %a",fmtpathspec) else report("no valid path reported, falling back to current path") fmtpathspec = "." end end fmtpathspec = string.splitlines(fmtpathspec)[1] or fmtpathspec fmtpathspec = fmtpathspec and file.splitpath(fmtpathspec) local fmtpath = nil if fmtpathspec then for i=1,#fmtpathspec do local path = fmtpathspec[i] if path ~= "." then dir.makedirs(path) if lfs.isdir(path) and file.is_writable(path) then fmtpath = path break end end end end if not fmtpath or fmtpath == "" then fmtpath = "." else lfs.chdir(fmtpath) end engine(texengine,texformat) report("generating kpse file database") mktexlsr() report("format %a saved on path %a",texformat,fmtpath) end local function run(texengine,texformat,filename) local t = { } for k, v in next, environment.arguments do t[#t+1] = string.format("--mtx:%s=%s",k,v) end execute('%s --fmt=%s %s "%s"',texengine,removesuffix(texformat),table.concat(t," "),filename) end make_mkii_format = function(name,engine) -- let the binary sort it out os.setenv('SELFAUTOPARENT', "") os.setenv('SELFAUTODIR', "") os.setenv('SELFAUTOLOC', "") os.setenv('TEXROOT', "") os.setenv('TEXOS', "") os.setenv('TEXMFOS', "") os.setenv('TEXMFCNF', "") make(engine,name) end end function scripts.context.generate() resolvers.renewcache() trackers.enable("resolvers.locating") resolvers.load() end function scripts.context.make(name) if not getargument("fast") then -- as in texexec scripts.context.generate() end local list = (name and { name }) or (environment.filenames[1] and environment.filenames) or defaultformats local engine = getargument("engine") or (status and status.luatex_engine) or "luatex" if getargument("jit") then engine = "luajittex" end for i=1,#list do local name = list[i] name = formatofinterface[name] or name or "" if name == "" then -- nothing elseif engine == "luametatex" or engine == "luatex" or engine == "luajittex" then make_mkiv_format(name,engine) elseif engine == "pdftex" or engine == "xetex" then make_mkii_format(name,engine) end end end function scripts.context.ctx() local ctxdata = ctxrunner.new() ctxdata.jobname = environment.filenames[1] ctxrunner.checkfile(ctxdata,getargument("ctx")) ctxrunner.checkflags(ctxdata) scripts.context.run(ctxdata) end function scripts.context.autoctx() local ctxdata = nil local files = environment.filenames local firstfile = #files > 0 and files[1] if firstfile then local suffix = filesuffix(firstfile) local ctxname = nil if suffix == "xml" then local chunk = io.loadchunk(firstfile) -- 1024 if chunk then ctxname = match(chunk,"<%?context%-directive%s+job%s+ctxfile%s+([^ ]-)%s*?>") end elseif suffix == "tex" or suffix == "mkiv" or suffix == "mkxl" then local analysis = preamble_analyze(firstfile) ctxname = analysis.ctxfile or analysis.ctx end if ctxname then ctxdata = ctxrunner.new() ctxdata.jobname = firstfile ctxrunner.checkfile(ctxdata,ctxname) ctxrunner.checkflags(ctxdata) end end scripts.context.run(ctxdata) end function scripts.context.version() local list = { "context.mkiv", "context.mkxl" } for i=1,#list do local base = list[i] local name = resolvers.findfile(base) if name ~= "" then report("main context file: %s",name) local data = io.loaddata(name) if data then local version = match(data,"\\edef\\contextversion{(.-)}") if version then report("current version: %s",version) else report("context version: unknown, no timestamp found") end else report("context version: unknown, load error") end else report("main context file: unknown, %a not found",base) end end end function scripts.context.purge_job(jobname,all,mkiitoo,fulljobname) if jobname and jobname ~= "" then jobname = filebasename(jobname) local filebase = removesuffix(jobname) if mkiitoo then scripts.context.purge(all,filebase,true) -- leading "./" else local deleted = { } for i=1,#obsolete_results do deleted[#deleted+1] = purge_file(fileaddsuffix(filebase,obsolete_results[i]),fileaddsuffix(filebase,"pdf")) end for i=1,#temporary_runfiles do deleted[#deleted+1] = purge_file(fileaddsuffix(filebase,temporary_runfiles[i])) end if fulljobname and fulljobname ~= jobname then for i=1,#temporary_suffixes do deleted[#deleted+1] = purge_file(fileaddsuffix(fulljobname,temporary_suffixes[i],true)) end end if all then for i=1,#persistent_runfiles do deleted[#deleted+1] = purge_file(fileaddsuffix(filebase,persistent_runfiles[i])) end end if #deleted > 0 then report("purged files: %s", concat(deleted,", ")) end end end end function scripts.context.purge(all,pattern,mkiitoo) local all = all or getargument("all") local pattern = getargument("pattern") or (pattern and (pattern.."*")) or "*.*" local files = dir.glob(pattern) local obsolete = tohash(obsolete_results) local temporary = tohash(temporary_runfiles) local synctex = tohash(synctex_runfiles) local persistent = tohash(persistent_runfiles) local generic = tohash(generic_files) local deleted = { } for i=1,#files do local name = files[i] local suffix = filesuffix(name) local basename = filebasename(name) if obsolete[suffix] or temporary[suffix] or synctex[suffix] or persistent[suffix] or generic[basename] then deleted[#deleted+1] = purge_file(name) elseif mkiitoo then for i=1,#special_runfiles do if find(name,special_runfiles[i]) then deleted[#deleted+1] = purge_file(name) end end end for i=1,#extra_runfiles do if find(basename,extra_runfiles[i]) then deleted[#deleted+1] = purge_file(name) end end end if #deleted > 0 then report("purged files: %s", concat(deleted,", ")) end end -- touching files (signals regeneration of formats) local newversion = false local function touch(path,name,versionpattern,kind,kindpattern) if path and path ~= "" then name = filejoinname(path,name) else name = resolvers.findfile(name) end local olddata = io.loaddata(name) if olddata then local oldkind = "" local newkind = kind or "" local oldversion = "" local newdata newversion = newversion or os.date("%Y.%m.%d %H:%M") if versionpattern then newdata = gsub(olddata,versionpattern,function(pre,mid,post) oldversion = mid return pre .. newversion .. post end) or olddata end if kind and kindpattern then newdata = gsub(newdata,kindpattern,function(pre,mid,post) oldkind = mid return pre .. newkind .. post end) or newdata end if newdata ~= "" and (oldversion ~= newversion or oldkind ~= newkind or newdata ~= olddata) then local backup = filenewsuffix(name,"tmp") removefile(backup) renamefile(name,backup) io.savedata(name,newdata) return name, oldversion, newversion, oldkind, newkind end end end local p_contextkind = "(\\edef\\contextkind%s*{)(.-)(})" local p_contextversion = "(\\edef\\contextversion%s*{)(.-)(})" local p_newcontextversion = "(\\newcontextversion%s*{)(.-)(})" local function touchfiles(suffix,kind,path) local foundname, oldversion, newversion, oldkind, newkind = touch(path,fileaddsuffix("context",suffix),p_contextversion,kind,p_contextkind) if foundname then report("old version : %s (%s)",oldversion,oldkind) report("new version : %s (%s)",newversion,newkind) report("touched file : %s",foundname) local foundname = touch(path,fileaddsuffix("cont-new",suffix),p_newcontextversion) if foundname then report("touched file : %s", foundname) end else report("nothing touched") end end local tobetouched = tohash { "mkii", "mkiv", "mkvi", "mkxl", "mklx" } function scripts.context.touch() if getargument("expert") then local touch = getargument("touch") local kind = getargument("kind") local path = getargument("basepath") if tobetouched[touch] then -- mkix mkxi ctix ctxi touchfiles(touch,kind,path) else for touch in sortedhash(tobetouched) do touchfiles(touch,kind,path) end end else report("touching needs --expert") end end function scripts.context.pages() local filename = environment.files[1] if filename then local u = table.load(fileaddsuffix(filename,"tuc")) if u then local p = u.structures.pages.collected local l = u.structures.lists.collected local page = environment.arguments.page local list = environment.arguments.list if type(page) == "string" then page = settings_to_array(page) end if type(list) == "string" then list = settings_to_array(list) end if page or list then if page then for i=1,#page do page[i] = string.topattern(page[i]) end for i=1,#p do local pi = p[i] local m = pi.marked if m then local ml = #m for j=1,#page do local n = page[j] for k=1,ml do if find(m[k],n) then report("page : %04i %s",i,m[k]) end end end end end end if list then for i=1,#list do list[i] = string.topattern(list[i]) end for i=1,#l do local li = l[i] local r = li.references if r then local rr = r.reference if rr then rr = splitstring(rr,",") local rrl = #rr for j=1,#list do local n = list[j] for k=1,rrl do if find(rr[k],n) then report("list : %04i %s",r.realpage,rr[k]) end end end end end end end else for i=1,#p do local pi = p[i] local m = pi.marked if m then report("page : %04i % t",i,m) end end end end end end -- modules local labels = { "title", "comment", "status" } local cards = { "*.mkiv", "*.mkvi", "*.mkix", "*.mkxi", "*.mkxl", "*.mklx", "*.tex" } local valid = tohash { "mkiv", "mkvi", "mkix", "mkxi", "mkxl", "mklx", "tex" } function scripts.context.modules(pattern) local list = { } local found = resolvers.findfile("context.mkiv") if not pattern or pattern == "" then -- official files in the tree for i=1,#cards do resolvers.findwildcardfiles(cards[i],list) end -- my dev path for i=1,#cards do dir.glob(filejoinname(filepathpart(found),cards[i]),list) end else resolvers.findwildcardfiles(pattern,list) dir.glob(filejoinname(filepathpart(found,pattern)),list) end local done = { } -- todo : sort local none = { x = { }, m = { }, s = { }, t = { } } for i=1,#list do local v = list[i] local base = filebasename(v) if not done[base] then done[base] = true local suffix = filesuffix(base) if valid[suffix] then local prefix, rest = match(base,"^([xmst])%-(.*)") if prefix then v = resolvers.findfile(base) -- so that files on my dev path are seen local data = io.loaddata(v) or "" data = match(data,"%% begin info(.-)%% end info") if data then local info = { } for label, text in gmatch(data,"%% +([^ ]+) *: *(.-)[\n\r]") do info[label] = text end report() report("%-7s : %s","module",base) report() for i=1,#labels do local l = labels[i] if info[l] then report("%-7s : %s",l,info[l]) end end report() else insert(none[prefix],rest) end end end end end local function show(k,v) sort(v) if #v > 0 then report() for i=1,#v do report("%s : %s",k,v[i]) end end end for k, v in sortedhash(none) do show(k,v) end end -- extras function scripts.context.extras(pattern) -- only in base path, i.e. only official ones if type(pattern) ~= "string" then pattern = "*" end local found = resolvers.findfile("context.mkiv") if found ~= "" then pattern = filejoinname(dir.expandname(filepathpart(found)),format("mtx-context-%s.tex",pattern or "*")) local list = dir.glob(pattern) for i=1,#list do local v = list[i] local data = io.loaddata(v) or "" data = match(data,"%% begin help(.-)%% end help") if data then report() report("extra: %s (%s)",(gsub(v,"^.*mtx%-context%-(.-)%.tex$","%1")),v) for s in gmatch(data,"%% *(.-)[\n\r]") do report(s) end report() end end end end function scripts.context.extra() local extra = getargument("extra") if type(extra) ~= "string" then scripts.context.extras() elseif getargument("help") then scripts.context.extras(extra) else local fullextra = extra if not find(fullextra,"mtx%-context%-") then fullextra = "mtx-context-" .. extra end local foundextra = resolvers.findfile(fullextra) if foundextra == "" then scripts.context.extras() return else report("processing extra: %s", foundextra) end setargument("purgeall",true) local result = getargument("result") or "" if result == "" then setargument("result","context-extra") end scripts.context.run(nil,foundextra) end end -- experiment do local popen = io.popen local close = io.close ----- read = io.read local gobble = io.gobble or function(f) f:read("l") end ----- clock = os.clock local ticks = lua.getpreciseticks local seconds = lua.getpreciseseconds local f_runner = formatters['context %s "%s"'] local f_command = formatters['%s %s'] function scripts.context.parallel() if getargument("pattern") then environment.files = dir.glob(getargument("pattern")) end local files = environment.files local total = files and #files or 0 local list = nil if getargument("parallellist") then list = { } for i=1,#files do -- could be an lpeg local name = files[i] local data = string.splitlines(io.loaddata(name) or "") if data then for i=1,#data do local line = data[i] if string.find(line,"^context ") then list[#list+1] = line end end end end files = list total = #list end if not list and total == 1 then -- Beware: "--parallel" and "--terminal" are passed to the single run but this -- is normally harmless. scripts.context.autoctx() elseif total > 0 then local results = { } local start = starttiming("parallel") local terminal = environment.argument("terminal") local runners = tonumber(environment.argument("parallel")) or 8 local process = { } local count = 0 -- a hack local passthese = environment.arguments_after for i=1,#passthese do local a = passthese[i] if string.find(a,"^%-%-parallel") or string.find(a,"^%-%-terminal") or not find(a,"^%-%-") then passthese[i] = "" end end passthese = table.unique(passthese) -- end of hack local arguments = environment.reconstructcommandline(passthese) local whattodo = list and "command" or "filename" while true do local done = false for i=1,runners do local pi = process[i] if pi then local s if terminal then s = pi.handle:read("l") if s then done = true report("%02i : %s",i,s) goto done end else s = gobble(pi.handle) if s then done = true goto done end end if not s then local r, detail, n = close(pi.handle) stoptiming(pi.timer) pi.result = (not r or n > 0) and "error" or "done" pi.time = elapsedtime(pi.timer) pi.handle = nil pi.timer = nil if terminal then report() end report("process %02i, index %02i, %s %a, status %a, runtime %0.3f",i,pi.count,whattodo,pi.filename,pi.result,pi.time) if terminal then report() end process[i] = false results[pi.count] = pi end end count = count + 1 if count > total then -- we're done else local timer = "parallel:" .. i local filename = files[count] local dirname = file.dirname(filename) local basename = file.basename(filename) if dirname ~= "." and dirname ~= "./" then dir.push(dirname) end local command = list and f_command(basename,arguments) or f_runner(arguments,basename) starttiming(timer) local result = popen(command) if dirname ~= "." then dir.pop() end local status = nil if result then process[i] = { handle = result, result = "start", filename = filename, count = count, time = 0, timer = timer, } status = process[i] else status = { result = "error", count = count, filename = filename, time = 0, } stoptiming(timer) end results[count] = status if terminal then report() end report("process %02i, index %02i, %s %a, status %a",i,status.count,whattodo,status.filename,status.result) if terminal then report() end done = true end ::done:: end if not done then break end end stoptiming("parallel") results.runtime = elapsedtime("parallel") report() report("files: %i, runtime: %s",total,results.runtime) report() local errors = { } for i=1,total do local ri = results[i] local result = ri.result local filename = ri.filename if result == "error" then errors[#errors+1] = filename end report("index %02i, %s %a, status %a, runtime %0.3f ",ri.count,whattodo,filename,result,ri.time) end if #errors > 0 then report() report("errors in:") report() for i=1,#errors do report(" %s",errors[i]) end end report() end end end -- todo: we need to do a dummy run local function showsetter() environment.files = { resolvers.findfile("mtx-context-setters.tex") } multipass_nofruns = 1 setargument("purgeall",true) scripts.context.run() end scripts.context.trackers = showsetter scripts.context.directives = showsetter scripts.context.experiments = showsetter function scripts.context.logcategories() environment.files = { resolvers.findfile("m-logcategories.mkiv") } multipass_nofruns = 1 setargument("purgeall",true) scripts.context.run() end function scripts.context.timed(action) statistics.timed(action,true) end -- getting it done if getargument("pdftex") then setargument("engine","pdftex") elseif getargument("xetex") then setargument("engine","xetex") end if getargument("timedlog") then logs.settimedlog() end if getargument("nostats") then setargument("nostatistics",true) setargument("nostat",nil) end if getargument("batch") then setargument("batchmode",true) setargument("batch",nil) end if getargument("nonstop") then setargument("nonstopmode",true) setargument("nonstop",nil) end do local htmlerrorpage = getargument("htmlerrorpage") if htmlerrorpage == "scite" then directives.enable("system.showerror=scite") elseif htmlerrorpage then directives.enable("system.showerror") end end do local silent = getargument("silent") if type(silent) == "string" then directives.enable(format("logs.blocked={%s}",silent)) elseif silent then directives.enable("logs.blocked") end local errors = getargument("errors") if type(errors) == "errors" then directives.enable(format("logs.errors={%s}",silent)) elseif errors then directives.enable("logs.errors") end end if getargument("once") then multipass_nofruns = 1 else if getargument("runs") then multipass_nofruns = tonumber(getargument("runs")) or nil end multipass_forcedruns = tonumber(getargument("forcedruns")) or nil end if getargument("parallel") or getargument("parallellist") then scripts.context.timed(scripts.context.parallel) elseif getargument("run") then scripts.context.timed(scripts.context.autoctx) elseif getargument("make") then scripts.context.timed(function() scripts.context.make() end) elseif getargument("generate") then scripts.context.timed(function() scripts.context.generate() end) elseif getargument("ctx") and not getargument("noctx") then scripts.context.timed(scripts.context.ctx) elseif getargument("version") then application.identify() scripts.context.version() elseif getargument("touch") then scripts.context.touch() elseif getargument("pages") then scripts.context.pages() elseif getargument("expert") then application.help("expert", "special") elseif getargument("showmodules") or getargument("modules") then scripts.context.modules() elseif getargument("showextras") or getargument("extras") then scripts.context.extras(environment.filenames[1] or getargument("extras")) elseif getargument("extra") then scripts.context.extra() elseif getargument("exporthelp") then -- application.export(getargument("exporthelp"),environment.filenames[1]) application.export() elseif getargument("help") then if environment.filenames[1] == "extras" then scripts.context.extras() else application.help("basic") end elseif getargument("showtrackers") or getargument("trackers") == true then scripts.context.trackers() elseif getargument("showdirectives") or getargument("directives") == true then scripts.context.directives() elseif getargument("showlogcategories") then scripts.context.logcategories() elseif environment.filenames[1] or getargument("nofile") then -- -- -- -- How compatible is this ... we might want to resolve the wildcard at the TeX, so -- -- we just keep this as unsuported feature (but at least we know of this case): -- -- if not getargument("pattern") and find(environment.filenames[1],"%*") then -- environment.filenames = dir.glob(environment.filenames[1]) -- -- setargument("pattern",dir.glob(environment.filenames[1])) -- end -- scripts.context.timed(scripts.context.autoctx) elseif getargument("pipe") then scripts.context.timed(scripts.context.pipe) elseif getargument("purge") then -- only when no filename given, supports --pattern scripts.context.purge() elseif getargument("purgeall") then -- only when no filename given, supports --pattern scripts.context.purge(true,nil,true) elseif getargument("pattern") then environment.filenames = dir.glob(getargument("pattern")) scripts.context.timed(scripts.context.autoctx) else application.help("basic") end -- we can wipe a signal file when done do if getargument("wipebusy") then removefile("context-is-busy.tmp") end end