mtx-context.lua /size: 75 Kb    last modification: 2024-01-16 09:02
1if not modules then modules = { } end modules['mtx-context'] = {
2    version   = 1.001,
3    comment   = "companion to mtxrun.lua",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- todo: more local functions
10-- todo: pass jobticket/ctxdata table around
11
12local type, next, tostring, tonumber = type, next, tostring, tonumber
13local format, gmatch, match, gsub, find = string.format, string.gmatch, string.match, string.gsub, string.find
14local quote, validstring, splitstring = string.quote, string.valid, string.split
15local sort, concat, insert, sortedhash, tohash = table.sort, table.concat, table.insert, table.sortedhash, table.tohash
16local settings_to_array = utilities.parsers.settings_to_array
17local appendtable = table.append
18local lpegpatterns, lpegmatch, Cs, P = lpeg.patterns, lpeg.match, lpeg.Cs, lpeg.P
19
20local getargument   = environment.getargument or environment.argument
21local setargument   = environment.setargument
22
23local filejoinname  = file.join
24local filebasename  = file.basename
25local filenameonly  = file.nameonly
26local filepathpart  = file.pathpart
27local filesuffix    = file.suffix
28local fileaddsuffix = file.addsuffix
29local filenewsuffix = file.replacesuffix
30local removesuffix  = file.removesuffix
31local validfile     = lfs.isfile
32local removefile    = os.remove
33local renamefile    = os.rename
34local formatters    = string.formatters
35
36local starttiming   = statistics.starttiming
37local stoptiming    = statistics.stoptiming
38local elapsedtime   = statistics.elapsedtime
39
40local application = logs.application {
41    name     = "mtx-context",
42    banner   = "ConTeXt Process Management 1.05",
43 -- helpinfo = helpinfo, -- table with { category_a = text_1, category_b = text_2 } or helpstring or xml_blob
44    helpinfo = "mtx-context.xml",
45}
46
47-- local luatexflags = {
48--     ["8bit"]                        = true,  -- ignored, input is assumed to be in UTF-8 encoding
49--     ["default-translate-file"]      = true,  -- ignored, input is assumed to be in UTF-8 encoding
50--     ["translate-file"]              = true,  -- ignored, input is assumed to be in UTF-8 encoding
51--     ["etex"]                        = true,  -- ignored, the etex extensions are always active
52--     ["parse-first-line"]            = true,  -- ignored, enable parsing of the first line of the input file
53--     ["no-parse-first-line"]         = true,  -- ignored, disable parsing of the first line of the input file
54--
55--     ["credits"]                     = true,  -- display credits and exit
56--     ["debug-format"]                = true,  -- enable format debugging
57--     ["disable-write18"]             = true,  -- disable \write18{SHELL COMMAND}
58--     ["draftmode"]                   = true,  -- switch on draft mode (generates no output PDF)
59--     ["enable-write18"]              = true,  -- enable \write18{SHELL COMMAND}
60--     ["file-line-error"]             = true,  -- enable file:line:error style messages
61--     ["file-line-error-style"]       = true,  -- aliases of --file-line-error
62--     ["no-file-line-error"]          = true,  -- disable file:line:error style messages
63--     ["no-file-line-error-style"]    = true,  -- aliases of --no-file-line-error
64--     ["fmt"]                         = true,  -- load the format file FORMAT
65--     ["halt-on-error"]               = true,  -- stop processing at the first error
66--     ["help"]                        = true,  -- display help and exit
67--     ["ini"]                         = true,  -- be iniluatex, for dumping formats
68--     ["interaction"]                 = true,  -- set interaction mode (STRING=batchmode/nonstopmode/scrollmode/errorstopmode)
69--     ["jobname"]                     = true,  -- set the job name to STRING
70--     ["kpathsea-debug"]              = true,  -- set path searching debugging flags according to the bits of NUMBER
71--     ["lua"]                         = true,  -- load and execute a lua initialization script
72--     ["mktex"]                       = true,  -- enable mktexFMT generation (FMT=tex/tfm)
73--     ["no-mktex"]                    = true,  -- disable mktexFMT generation (FMT=tex/tfm)
74--     ["nosocket"]                    = true,  -- disable the lua socket library
75--     ["output-comment"]              = true,  -- use STRING for DVI file comment instead of date (no effect for PDF)
76--     ["output-directory"]            = true,  -- use existing DIR as the directory to write files in
77--     ["output-format"]               = true,  -- use FORMAT for job output; FORMAT is 'dvi' or 'pdf'
78--     ["progname"]                    = true,  -- set the program name to STRING
79--     ["recorder"]                    = true,  -- enable filename recorder
80--     ["safer"]                       = true,  -- disable easily exploitable lua commands
81--     ["shell-escape"]                = true,  -- enable \write18{SHELL COMMAND}
82--     ["no-shell-escape"]             = true,  -- disable \write18{SHELL COMMAND}
83--     ["shell-restricted"]            = true,  -- restrict \write18 to a list of commands given in texmf.cnf
84--     ["nodates"]                     = true,  -- no production dates in pdf file
85--     ["trailerid"]                   = true,  -- alternative trailer id
86--     ["synctex"]                     = true,  -- enable synctex
87--     ["version"]                     = true,  -- display version and exit
88--     ["luaonly"]                     = true,  -- run a lua file, then exit
89--     ["luaconly"]                    = true,  -- byte-compile a lua file, then exit
90--     ["jiton"]                       = false, -- not supported (makes no sense, slower)
91-- }
92
93local report = application.report
94
95scripts         = scripts         or { }
96scripts.context = scripts.context or { }
97
98-- for the moment here
99
100if jit then -- already luajittex
101    setargument("engine","luajittex")
102    setargument("jit",nil)
103elseif getargument("luatex") then -- relaunch luajittex
104    setargument("engine","luatex")
105elseif getargument("jit") or getargument("luajittex") then -- relaunch luajittex
106    -- bonus shortcut, we assume that --jit also indicates the engine
107    -- although --jit and --engine=luajittex are independent
108    setargument("engine","luajittex")
109end
110
111-- -- The way we use stubs will change in a bit in 2019 (mtxrun and context). We also normalize
112-- -- the platforms to use a similar approach to this.
113
114local engine_new = filenameonly(getargument("engine") or directives.value("system.engine"))
115local engine_old = filenameonly(environment.ownmain) or filenameonly(environment.ownbin)
116
117local function restart(engine_old,engine_new)
118    local generate  = environment.arguments.generate and (engine_new == "luatex" or engine_new == "luajittex")
119    local arguments = generate and  "--generate" or environment.reconstructcommandline()
120    local ownname   = filejoinname(filepathpart(environment.ownname),"mtxrun.lua")
121    local command   = format("%s --luaonly --socket %q %s --redirected",engine_new,ownname,arguments)
122    report(format("redirect %s -> %s: %s",engine_old,engine_new,command))
123    local result = os.execute(command)
124    os.exit(result == 0 and 0 or 1)
125end
126
127-- if getargument("redirected") then
128--     setargument("engine",engine_old) -- later on we need this
129-- elseif engine_new == engine_old then
130--     setargument("engine",engine_new) -- later on we need this
131-- elseif environment.validengines[engine_new] and engine_new ~= environment.basicengines[engine_old] then
132--     restart(engine_old,engine_new)
133-- else
134--     setargument("engine",engine_new) -- later on we need this
135-- end
136
137if environment.validengines[engine_new] and engine_new ~= environment.basicengines[engine_old] then
138    restart(engine_old,engine_new)
139end
140
141-- so far
142
143-- constants
144
145local usedfiles = {
146    nop = "cont-nop.mkiv",
147    yes = "cont-yes.mkiv",
148}
149
150local usedsuffixes = {
151    before = {
152        "tuc"
153    },
154    after = {
155        "pdf", "tuc", "log"
156    },
157    keep = {
158        "log"
159    },
160}
161
162local formatofinterface = {
163    en = "cont-en",
164    uk = "cont-uk",
165    de = "cont-de",
166    fr = "cont-fr",
167    nl = "cont-nl",
168    cs = "cont-cs",
169    it = "cont-it",
170    ro = "cont-ro",
171    pe = "cont-pe",
172}
173
174local defaultformats = {
175    "cont-en",
176 -- "cont-nl",
177}
178
179-- purging files (we should have an mkii and mkiv variants)
180
181local generic_files = {
182    "texexec.tex", "texexec.tui", "texexec.tuo",
183    "texexec.tuc", "texexec.tua",
184    "texexec.ps", "texexec.pdf", "texexec.dvi",
185    "cont-opt.tex", "cont-opt.bak"
186}
187
188local obsolete_results = {
189    "dvi",
190}
191
192local temporary_runfiles = {
193    "tui",                             -- mkii two pass file
194    "tua",                             -- mkiv obsolete
195    "tup", "ted", "tes",               -- texexec
196    "top",                             -- mkii options file
197    "log",                             -- tex log file
198    "tmp",                             -- mkii buffer file
199    "run",                             -- mkii stub
200    "bck",                             -- backup (obsolete)
201    "rlg",                             -- resource log
202    "ctl",                             --
203    "mpt", "mpx", "mpd", "mpo", "mpb", -- metafun
204    "prep",                            -- context preprocessed
205    "pgf",                             -- tikz
206    "aux", "blg",                      -- bibtex
207}
208
209local temporary_suffixes = {
210    "prep",                            -- context preprocessed
211}
212
213local synctex_runfiles = {
214    "synctex", "synctex.gz", "syncctx" -- synctex
215}
216
217local persistent_runfiles = {
218    "tuo", -- mkii two pass file
219    "tub", -- mkii buffer file
220    "top", -- mkii options file
221    "tuc", -- mkiv two pass file
222    "bbl", -- bibtex
223}
224
225local special_runfiles = {
226    "%-mpgraph", "%-mprun", "%-temp%-",
227}
228
229local extra_runfiles = {
230    "^m_k_i_v_.-%.pdf$",
231    "^l_m_t_x_.-%.pdf$",
232}
233
234local function purge_file(dfile,cfile)
235    if cfile and validfile(cfile) then
236        if removefile(dfile) then
237            return filebasename(dfile)
238        end
239    elseif dfile then
240        if removefile(dfile) then
241            return filebasename(dfile)
242        end
243    end
244end
245
246-- process information
247
248local ctxrunner = { } -- namespace will go
249
250local ctx_locations = { '..', '../..' }
251
252function ctxrunner.new()
253    return {
254        ctxname   = "",
255        jobname   = "",
256        flags     = { },
257    }
258end
259
260function ctxrunner.checkfile(ctxdata,ctxname,defaultname)
261
262    if not ctxdata.jobname or ctxdata.jobname == "" or getargument("noctx") then
263        return
264    end
265
266    ctxdata.ctxname = ctxname or removesuffix(ctxdata.jobname) or ""
267
268    if ctxdata.ctxname == "" then
269        return
270    end
271
272    ctxdata.jobname = fileaddsuffix(ctxdata.jobname,'tex')
273    ctxdata.ctxname = fileaddsuffix(ctxdata.ctxname,'ctx')
274
275    report("jobname: %s",ctxdata.jobname)
276    report("ctxname: %s",ctxdata.ctxname)
277
278    -- mtxrun should resolve kpse: and file:
279
280    local usedname = ctxdata.ctxname
281    local found    = validfile(usedname)
282
283    -- no further test if qualified path
284
285    if not found then
286        for _, path in next, ctx_locations do
287            local fullname = filejoinname(path,ctxdata.ctxname)
288            if validfile(fullname) then
289                usedname = fullname
290                found    = true
291                break
292            end
293        end
294    end
295
296    if not found then
297        usedname = resolvers.findfile(ctxdata.ctxname,"tex")
298        found    = usedname ~= ""
299    end
300
301    if not found and defaultname and defaultname ~= "" and validfile(defaultname) then
302        usedname = defaultname
303        found    = true
304    end
305
306    if not found then
307        return
308    end
309
310    local xmldata = xml.load(usedname)
311
312    if not xmldata then
313        return
314    else
315        -- test for valid, can be text file
316    end
317
318    local ctxpaths = table.append({'.', filepathpart(ctxdata.ctxname)}, ctx_locations)
319
320    xml.include(xmldata,'ctx:include','name', ctxpaths)
321
322    local flags = ctxdata.flags
323
324    for e in xml.collected(xmldata,"/ctx:job/ctx:flags/ctx:flag") do
325        local flag = xml.text(e) or ""
326        local key, value = match(flag,"^(.-)=(.+)$")
327        if key and value then
328            flags[key] = value
329        else
330            flags[flag] = true
331        end
332    end
333
334end
335
336function ctxrunner.checkflags(ctxdata)
337    if ctxdata then
338        for k,v in next, ctxdata.flags do
339            if getargument(k) == nil then
340                setargument(k,v)
341            end
342        end
343    end
344end
345
346-- multipass control
347
348local multipass_suffixes   = { ".tuc" }
349local multipass_nofruns    = 9 -- better for tracing oscillation
350local multipass_forcedruns = false
351
352local function multipass_hashfiles(jobname)
353    local hash = { }
354    for i=1,#multipass_suffixes do
355        local suffix = multipass_suffixes[i]
356        local full = jobname .. suffix
357        hash[full] = md5.hex(io.loaddata(full) or "unknown")
358    end
359    return hash
360end
361
362local function multipass_changed(oldhash, newhash)
363    for k,v in next, oldhash do
364        if v ~= newhash[k] then
365            return true
366        end
367    end
368    return false
369end
370
371local f_tempfile_i = formatters["%s-%s-%02d.tmp"]
372local f_tempfile_s = formatters["%s-%s-keep.%s"]
373
374local function backup(jobname,run,kind,filename)
375    if run then
376        if run == 1 then
377            for i=1,10 do
378                local tmpname = f_tempfile_i(jobname,kind,i)
379                if validfile(tmpname) then
380                    removefile(tmpname)
381                    report("removing %a",tmpname)
382                end
383            end
384        end
385        if validfile(filename) then
386            local tmpname = f_tempfile_i(jobname,kind,run or 1)
387            report("copying %a into %a",filename,tmpname)
388            file.copy(filename,tmpname)
389        else
390            report("no file %a, nothing kept",filename)
391        end
392    elseif validfile(filename) then
393        local tmpname = f_tempfile_s(jobname,kind,kind)
394        report("copying %a into %a",filename,tmpname)
395        file.copy(filename,tmpname)
396    else
397        report("no file %a, nothing kept",filename)
398    end
399end
400
401local function multipass_copyluafile(jobname,run)
402    local tuaname, tucname = jobname..".tua", jobname..".tuc"
403    if validfile(tuaname) then
404        if run then
405            backup(jobname,run,"tuc",tucname)
406            report("copying %a into %a",tuaname,tucname)
407            report()
408        end
409        removefile(tucname)
410        renamefile(tuaname,tucname)
411    end
412end
413
414local function multipass_copypdffile(jobname,run)
415    if run then
416        local pdfname = jobname..".pdf"
417        if validfile(pdfname) then
418            backup(jobname,false,"pdf",pdfname)
419            report()
420        end
421    end
422end
423
424local function multipass_copylogfile(jobname,run)
425    if run then
426        local logname = jobname..".log"
427        if validfile(logname) then
428            backup(jobname,run,"log",logname)
429            report()
430        end
431    end
432end
433
434--
435
436local pattern = lpegpatterns.utfbom^-1 * (P("%% ") + P("% ")) * Cs((1-lpegpatterns.newline)^1)
437
438local prefile = nil
439local predata = nil
440
441local function preamble_analyze(filename) -- only files on current path
442    filename = fileaddsuffix(filename,"tex") -- to be sure
443    if predata and prefile == filename then
444        return predata
445    end
446    prefile = filename
447    predata = { }
448    local line = io.loadlines(prefile)
449    if line then
450        local preamble = lpegmatch(pattern,line)
451        if preamble then
452            utilities.parsers.options_to_hash(preamble,predata)
453            predata.type = "tex"
454        elseif find(line,"^<?xml ") then
455            predata.type = "xml"
456        end
457        if predata.nofruns then
458            multipass_nofruns = predata.nofruns
459        end
460        if not predata.engine then
461            predata.engine = environment.basicengines[engine_old] --'luatex'
462        end
463        if predata.engine ~= engine_old then -- hack
464            if environment.validengines[predata.engine] and predata.engine ~= environment.basicengines[engine_old] then
465                restart(engine_old,predata.engine)
466            end
467        end
468    end
469    return predata
470end
471
472-- automatically opening and closing pdf files
473
474local pdfview -- delayed
475
476local function pdf_open(name,method)
477    starttiming("pdfview")
478    pdfview = pdfview or dofile(resolvers.findfile("l-pdfview.lua","tex"))
479    pdfview.setmethod(method)
480    report(pdfview.status())
481    local pdfname = filenewsuffix(name,"pdf")
482    if not lfs.isfile(pdfname) then
483        pdfname = name .. ".pdf" -- agressive
484    end
485    pdfview.open(pdfname)
486    stoptiming("pdfview")
487    report("pdfview overhead: %s seconds",elapsedtime("pdfview"))
488end
489
490local function pdf_close(name,method)
491    starttiming("pdfview")
492    pdfview = pdfview or dofile(resolvers.findfile("l-pdfview.lua","tex"))
493    pdfview.setmethod(method)
494    local pdfname = filenewsuffix(name,"pdf")
495    if lfs.isfile(pdfname) then
496        pdfview.close(pdfname)
497    end
498    pdfname = name .. ".pdf" -- agressive
499    pdfview.close(pdfname)
500    stoptiming("pdfview")
501end
502
503-- result file handling
504
505local function result_push_purge(oldbase,newbase)
506    for _, suffix in next, usedsuffixes.after do
507        local oldname = fileaddsuffix(oldbase,suffix)
508        local newname = fileaddsuffix(newbase,suffix)
509        removefile(newname)
510        removefile(oldname)
511    end
512end
513
514local function result_push_keep(oldbase,newbase)
515    for _, suffix in next, usedsuffixes.before do
516        local oldname = fileaddsuffix(oldbase,suffix)
517        local newname = fileaddsuffix(newbase,suffix)
518        local tmpname = "keep-"..oldname
519        removefile(tmpname)
520        renamefile(oldname,tmpname)
521        removefile(oldname)
522        renamefile(newname,oldname)
523    end
524end
525
526local function result_save_error(oldbase,newbase)
527    for _, suffix in next, usedsuffixes.keep do
528        local oldname = fileaddsuffix(oldbase,suffix)
529        local newname = fileaddsuffix(newbase,suffix)
530        removefile(newname) -- to be sure
531        renamefile(oldname,newname)
532    end
533end
534
535local function result_save_purge(oldbase,newbase)
536    for _, suffix in next, usedsuffixes.after do
537        local oldname = fileaddsuffix(oldbase,suffix)
538        local newname = fileaddsuffix(newbase,suffix)
539        removefile(newname) -- to be sure
540        renamefile(oldname,newname)
541    end
542end
543
544local function result_save_keep(oldbase,newbase)
545    for _, suffix in next, usedsuffixes.after do
546        local oldname = fileaddsuffix(oldbase,suffix)
547        local newname = fileaddsuffix(newbase,suffix)
548        local tmpname = "keep-"..oldname
549        removefile(newname)
550        renamefile(oldname,newname)
551        renamefile(tmpname,oldname)
552    end
553end
554
555-- use mtx-plain instead
556
557local plain_formats = {
558    ["plain"]        = "plain",
559    ["luatex-plain"] = "luatex-plain",
560}
561
562local function plain_format(plainformat)
563    return plainformat and plain_formats[plainformat]
564end
565
566local function run_plain(plainformat,filename)
567    local plainformat = plain_formats[plainformat]
568    if plainformat then
569        local command = format("mtxrun --script --texformat=%s plain %s",plainformat,filename)
570        report("running command: %s\n\n",command)
571        -- todo: load and run
572        local resultname = filenewsuffix(filename,"pdf")
573        local pdfview = getargument("autopdf") or getargument("closepdf")
574        if pdfview then
575            pdf_close(resultname,pdfview)
576            os.execute(command) -- maybe also a proper runner
577            pdf_open(resultname,pdfview)
578        else
579            os.execute(command) -- maybe also a proper runner
580        end
581    end
582end
583
584local function run_texexec(filename,a_purge,a_purgeall)
585    if false then
586        -- we need to write a top etc too and run mp etc so it's not worth the
587        -- trouble, so it will take a while before the next is finished
588        --
589        -- context --extra=texutil --convert myfile
590    else
591        local texexec = resolvers.findfile("texexec.rb") or ""
592        if texexec ~= "" then
593            os.setenv("RUBYOPT","")
594            local options = environment.reconstructcommandline(environment.arguments_after)
595            options = gsub(options,"--purge","")
596            options = gsub(options,"--purgeall","")
597            local command = format("ruby %s %s",texexec,options)
598            report("running command: %s\n\n",command)
599            if a_purge then
600                os.execute(command)
601                scripts.context.purge_job(filename,false,true)
602            elseif a_purgeall then
603                os.execute(command)
604                scripts.context.purge_job(filename,true,true)
605            else
606                os.execute(command) -- we can use os.exec but that doesn't give back timing
607            end
608        end
609    end
610end
611
612-- executing luatex
613
614local function flags_to_string(flags,prefix)
615    -- context flags get prepended by c: ... this will move to the sbx module
616    local t = { }
617    for k, v in table.sortedhash(flags) do
618        if prefix then
619            k = format("c:%s",k)
620        end
621        if not v or v == "" or v == '""' then
622            -- no need to flag false
623        elseif v == true then
624            t[#t+1] = format('--%s',k)
625        elseif type(v) == "string" then
626            t[#t+1] = format('--%s=%s',k,quote(v))
627        else
628            t[#t+1] = format('--%s=%s',k,tostring(v))
629        end
630    end
631    return concat(t," ")
632end
633
634function scripts.context.run(ctxdata,filename)
635    --
636    local verbose  = false
637    --
638    local a_nofile = getargument("nofile")
639    local a_engine = getargument("engine")
640    --
641    local files    = environment.filenames or { }
642    --
643    local filelist, mainfile
644    --
645    if filename then
646        -- the given forced name is processed, the filelist is passed to context
647        mainfile = filename
648        filelist = { filename }
649     -- files    = files
650    elseif a_nofile then
651        -- the list of given files is processed using the dummy file
652        mainfile = usedfiles.nop
653        filelist = { usedfiles.nop }
654     -- files    = { }
655    elseif #files > 0 then
656        -- the list of given files is processed using the stub file
657        mainfile = usedfiles.yes -- this can become "" for luametatex/lmtx
658        filelist = files
659        files    = { }
660    else
661        return
662    end
663    --
664    local interface  = validstring(getargument("interface")) or "en"
665    local formatname = formatofinterface[interface] or "cont-en"
666    local formatfile,
667          scriptfile = resolvers.locateformat(formatname) -- regular engine !
668    if not formatfile or not scriptfile then
669        report("warning: no format found, forcing remake (commandline driven)")
670        scripts.context.make(formatname)
671        formatfile, scriptfile = resolvers.locateformat(formatname) -- variant
672    end
673    if formatfile and scriptfile then
674        -- okay
675    elseif formatname then
676        report("error, no format found with name: %s, aborting",formatname)
677        return
678    else
679        report("error, no format found (provide formatname or interface)")
680        return
681    end
682    --
683    local a_mkii          = getargument("mkii") or getargument("pdftex") or getargument("xetex")
684    local a_purge         = getargument("purge")
685    local a_purgeall      = getargument("purgeall")
686    local a_purgeresult   = getargument("purgeresult")
687    local a_global        = getargument("global")
688    local a_runpath       = getargument("runpath")
689    local a_timing        = getargument("timing")
690    local a_profile       = getargument("profile")
691    local a_batchmode     = getargument("batchmode")
692    local a_nonstopmode   = getargument("nonstopmode")
693    local a_scollmode     = getargument("scrollmode")
694    local a_once          = getargument("once")
695    local a_backend       = getargument("backend")
696    local a_arrange       = getargument("arrange")
697    local a_noarrange     = getargument("noarrange")
698    local a_jithash       = getargument("jithash")
699    local a_permitloadlib = getargument("permitloadlib")
700    local a_texformat     = getargument("texformat")
701    local a_notuc         = getargument("notuc")
702    local a_keeptuc       = getargument("keeptuc")
703    local a_keeplog       = getargument("keeplog")
704    local a_keeppdf       = getargument("keeppdf")
705    local a_export        = getargument("export")
706    local a_nodates       = getargument("nodates")
707    local a_trailerid     = getargument("trailerid")
708    local a_nocompression = getargument("nocompression")
709    --
710    a_batchmode = (a_batchmode and "batchmode") or (a_nonstopmode and "nonstopmode") or (a_scrollmode and "scrollmode") or nil
711    --
712    local changed = { }
713    --
714    for i=1,#filelist do
715        --
716        local filename = filelist[i]
717
718        if filename == "" then
719            report("warning: bad filename")
720            break
721        end
722
723        local basename = filebasename(filename) -- use splitter
724        local pathname = filepathpart(filename)
725        --
726        if filesuffix(filename) == "" then
727            filename = fileaddsuffix(filename,"tex")
728        end
729        --
730        if pathname == "" and not a_global and filename ~= usedfiles.nop then
731            filename = "./" .. filename
732            if not validfile(filename) then
733                report("warning: no (local) file %a, proceeding",filename)
734            end
735        end
736        --
737        local jobname  = removesuffix(basename)
738     -- local jobname  = removesuffix(filename)
739        local ctxname  = ctxdata and ctxdata.ctxname
740        --
741        if changed[jobname] == nil then
742            changed[jobname] = false
743        end
744        --
745        local analysis = preamble_analyze(filename)
746        --
747        if a_mkii or analysis.engine == 'pdftex' or analysis.engine == 'xetex' then
748            run_texexec(filename,a_purge,a_purgeall)
749        elseif plain_format(a_texformat or analysis.texformat) then
750            run_plain(a_texformat or analysis.texformat,filename)
751        else
752            if analysis.interface and analysis.interface ~= interface then
753                formatname = formatofinterface[analysis.interface] or formatname
754                formatfile, scriptfile = resolvers.locateformat(formatname)
755            end
756            --
757            local runpath = a_runpath or analysis.runpath
758            if type(runpath) == "string" and runpath ~= "" then
759                runpath = resolvers.resolve(runpath)
760                local currentdir = dir.current()
761                if not lfs.isdir(runpath) then
762                    if dir.makedirs(runpath) then
763                        report("runpath %a has been created",runpath)
764                    else
765                        report("error: runpath %a cannot be created",runpath)
766                        os.exit()
767                    end
768                end
769                if lfs.chdir(runpath) then
770                    report("changing to runpath %a",runpath)
771                else
772                    report("error: changing to runpath %a is impossible",runpath)
773                    os.exit()
774                end
775                environment.arguments.path    = currentdir
776                environment.arguments.runpath = runpath
777                if filepathpart(filename) == "." then
778                    filename = filebasename(filename)
779                end
780            end
781            --
782            a_jithash       = validstring(a_jithash or analysis.jithash) or nil
783            a_permitloadlib = a_permitloadlib or analysis.permitloadlib or nil
784            --
785            if not formatfile or not scriptfile then
786                report("warning: no format found, forcing remake (source driven)")
787                scripts.context.make(formatname,a_engine)
788                formatfile, scriptfile = resolvers.locateformat(formatname)
789            end
790            --
791            local function combine(key)
792                local flag = validstring(environment[key])
793                local plus = analysis[key]
794                if flag and plus then
795                    return plus .. "," .. flag -- flag wins
796                else
797                    return flag or plus -- flag wins
798                end
799            end
800            ----- a_trackers    = analysis.trackers
801            ----- a_experiments = analysis.experiments
802            local directives    = combine("directives")
803            local trackers      = combine("trackers")
804            local experiments   = combine("experiments")
805            --
806            local ownerpassword = environment.ownerpassword or analysis.ownerpassword
807            local userpassword  = environment.userpassword  or analysis.userpassword
808            local permissions   = environment.permissions   or analysis.permissions
809            --
810            if formatfile and scriptfile then
811                local suffix     = validstring(getargument("suffix"))
812                local resultname = validstring(getargument("result"))
813                if not resultname or resultname == "" then
814                    resultname = validstring(analysis.result)
815                end
816                local resultpath = filepathpart(resultname)
817                if resultpath ~= "" then
818                    resultname  = nil
819                elseif suffix then
820                    resultname = removesuffix(jobname) .. suffix
821                end
822                local oldbase = ""
823                local newbase = ""
824                if resultname then
825                    oldbase = removesuffix(jobname)
826                    newbase = removesuffix(resultname)
827                    if oldbase ~= newbase then
828                        if a_purgeresult then
829                            result_push_purge(oldbase,newbase)
830                        else
831                            result_push_keep(oldbase,newbase)
832                        end
833                    else
834                        resultname = nil
835                    end
836                end
837                --
838                local pdfview = getargument("autopdf") or getargument("closepdf")
839                if pdfview then
840                    pdf_close(filename,pdfview)
841                    if resultname then
842                        pdf_close(resultname,pdfview)
843                    end
844                end
845                --
846                -- we could do this when locating the format and exit from luatex when
847                -- there is a version mismatch .. that way we can use stock luatex
848                -- plus mtxrun to run luajittex instead .. this saves a restart but is
849                -- also cleaner as then mtxrun only has to check for a special return
850                -- code (signaling a make + rerun) .. maybe some day
851                --
852                local okay = statistics.checkfmtstatus(formatfile,a_engine)
853                if okay ~= true then
854                    report("warning: %s, forcing remake",tostring(okay))
855                    scripts.context.make(formatname)
856                end
857                --
858                local oldhash     = multipass_hashfiles(jobname)
859                local newhash     = { }
860                local maxnofruns  = once and 1 or multipass_nofruns
861                local fulljobname = validstring(filename)
862                --
863                local c_flags = {
864                    directives     = directives,   -- gets passed via mtxrun
865                    trackers       = trackers,     -- gets passed via mtxrun
866                    experiments    = experiments,  -- gets passed via mtxrun
867                    --
868                    result         = validstring(resultname),
869                    input          = validstring(getargument("input") or filename), -- alternative input
870                    fulljobname    = fulljobname,
871                    files          = concat(files,","),
872                    ctx            = validstring(ctxname),
873                    export         = a_export and true or nil,
874                    nocompression  = a_nocompression and true or nil,
875                    texmfbinpath   = os.selfdir,
876                    --
877                    ownerpassword  = ownerpassword,
878                    userpassword   = userpassword,
879                    permissions    = permissions,
880                }
881                --
882                for k, v in next, environment.arguments do
883                    -- the raw arguments
884                    if c_flags[k] == nil then
885                        c_flags[k] = v
886                    end
887                end
888                --
889                -- todo: --output-file=... in luatex
890                --
891                local usedname = jobname
892                local engine   = analysis.engine or "luametatex"
893                if engine == "luametatex" and (mainfile == usedfiles.yes or mainfile == usedfiles.nop) and not getargument("redirected") then
894                    mainfile = "" -- we don't need that
895                    usedname = fulljobname
896                end
897                --
898                --
899                local l_flags = {
900                    ["interaction"]           = a_batchmode,
901                 -- ["synctex"]               = false,       -- context has its own way
902                 -- ["no-parse-first-line"]   = true,        -- obsolete
903                 -- ["safer"]                 = a_safer,     -- better use --sandbox
904                 -- ["no-mktex"]              = true,
905                 -- ["file-line-error-style"] = true,
906--                  ["fmt"]                   = formatfile,
907--                  ["lua"]                   = scriptfile,
908--                  ["jobname"]               = jobname,
909                    ["jobname"]               = usedname,
910                    ["jithash"]               = a_jithash,
911                    ["permitloadlib"]         = a_permitloadlib,
912                }
913                --
914                local directives = { }
915                --
916                -- todo: handle these at the tex end
917                --
918                if a_nodates then
919                    directives[#directives+1] = format("backend.date=%s",type(a_nodates) == "string" and a_nodates or "no")
920                end
921                --
922                if type(a_trailerid) == "string" then
923                    directives[#directives+1] = format("backend.trailerid=%s",a_trailerid)
924                end
925                --
926                if a_profile then
927                    directives[#directives+1] = format("system.profile=%s",tonumber(a_profile) or 0)
928                end
929                --
930             -- if a_notuc then
931             --     removefile(fileaddsuffix(jobname,"tuc"))
932             --     directives[#directives+1] = "job.save=no" -- handled at tex end
933             -- end
934                --
935                for i=1,#synctex_runfiles do
936                    removefile(fileaddsuffix(jobname,synctex_runfiles[i]))
937                end
938                --
939                if #directives > 0 then
940                    c_flags.directives = concat(directives,",")
941                end
942                --
943                -- kindofrun: 1:first run, 2:successive run, 3:once, 4:last of maxruns
944                --
945                -- can be used to include pages from a previous run, --keeppdf or "% keeppdf" on first-line
946                --
947                multipass_copypdffile(jobname,a_keeppdf or analysis.keeppdf)
948                --
949                for currentrun=1,maxnofruns do
950                    --
951                    c_flags.final      = false
952                    c_flags.kindofrun  = (a_once and 3) or (currentrun==1 and 1) or (currentrun==maxnofruns and 4) or 2
953                    c_flags.maxnofruns = maxnofruns
954                    c_flags.forcedruns = multipass_forcedruns and multipass_forcedruns > 0 and multipass_forcedruns or nil
955                    c_flags.currentrun = currentrun
956                    c_flags.noarrange  = a_noarrange or a_arrange or nil
957                    c_flags.profile    = a_profile and (tonumber(a_profile) or 0) or nil
958                    --
959                    print("") -- cleaner, else continuation on same line
960                    --
961                    local returncode = environment.run_format(
962                        formatfile,
963                        scriptfile,
964                        mainfile,
965                        flags_to_string(l_flags),
966                        flags_to_string(c_flags,true),
967                        verbose
968                    )
969                    -- todo: remake format when no proper format is found
970                    if not returncode then
971                        report("fatal error: no return code")
972                        if resultname then
973                            result_save_error(oldbase,newbase)
974                        end
975                        os.exit(1)
976                        break
977                    elseif returncode == 0 then
978                        multipass_copyluafile(jobname,a_keeptuc and currentrun)
979                        multipass_copylogfile(jobname,a_keeplog and currentrun)
980                        if not multipass_forcedruns then
981                            newhash = multipass_hashfiles(jobname)
982                            if multipass_changed(oldhash,newhash) then
983                                changed[jobname] = true
984                                oldhash = newhash
985                            else
986                                break
987                            end
988                        elseif currentrun == multipass_forcedruns then
989                            report("quitting after force %i runs",multipass_forcedruns)
990                            break
991                        end
992                    else
993                        report("fatal error: return code: %s",returncode or "?")
994                        if resultname then
995                            result_save_error(oldbase,newbase)
996                        end
997                        os.exit(1) -- (returncode)
998                        break
999                    end
1000                    --
1001                end
1002                --
1003                if environment.arguments["ansilog"] then
1004                    local logfile = filenewsuffix(jobname,"log")
1005                    local logdata = io.loaddata(logfile) or ""
1006                    if logdata ~= "" then
1007                        io.savedata(logfile,(gsub(logdata,"%[.-m","")))
1008                    end
1009                end
1010                --
1011                --
1012                --  this will go away after we update luatex
1013                --
1014                local syncctx = fileaddsuffix(jobname,"syncctx")
1015                if validfile(syncctx) then
1016                    renamefile(syncctx,fileaddsuffix(jobname,"synctex"))
1017                end
1018                --
1019                if a_arrange then
1020                    --
1021                    c_flags.final      = true
1022                    c_flags.kindofrun  = 3
1023                    c_flags.currentrun = c_flags.currentrun + 1
1024                    c_flags.noarrange  = nil
1025                    --
1026                    report("arrange run: %s",command)
1027                    --
1028                    local returncode = environment.run_format(
1029                        formatfile,
1030                        scriptfile,
1031                        mainfile,
1032                        flags_to_string(l_flags),
1033                        flags_to_string(c_flags,true),
1034                        verbose
1035                    )
1036                    --
1037                    if not returncode then
1038                        report("fatal error: no return code, message: %s",errorstring or "?")
1039                        os.exit(1)
1040                    elseif returncode > 0 then
1041                        report("fatal error: return code: %s",returncode or "?")
1042                        os.exit(returncode)
1043                    end
1044                    --
1045                end
1046                --
1047                if a_purge then
1048                    scripts.context.purge_job(jobname,false,false,fulljobname)
1049                elseif a_purgeall then
1050                    scripts.context.purge_job(jobname,true,false,fulljobname)
1051                end
1052                --
1053                if resultname then
1054                    if a_purgeresult then
1055                        -- so, if there is no result then we don't get the old one, but
1056                        -- related files (log etc) are still there for tracing purposes
1057                        result_save_purge(oldbase,newbase)
1058                    else
1059                        result_save_keep(oldbase,newbase)
1060                    end
1061                    report("result renamed to: %s",newbase)
1062                elseif resultpath ~= "" then
1063                    report()
1064                    report("results are to be on the running path, not on %a, ignoring --result",resultpath)
1065                    report()
1066                end
1067                --
1068             -- -- needs checking
1069             --
1070             -- if a_purge then
1071             --     scripts.context.purge_job(resultname)
1072             -- elseif a_purgeall then
1073             --     scripts.context.purge_job(resultname,true)
1074             -- end
1075                --
1076                local pdfview = getargument("autopdf")
1077                if pdfview then
1078                    pdf_open(resultname or jobname,pdfview)
1079                end
1080                --
1081                local epub = analysis.epub
1082                if epub then
1083                    if type(epub) == "string" then
1084                        local t = settings_to_array(epub)
1085                        for i=1,#t do
1086                            t[i] = "--" .. gsub(t[i],"^%-*","")
1087                        end
1088                        epub = concat(t," ")
1089                    else
1090                        epub = "--make"
1091                    end
1092                    local command = "mtxrun --script epub " .. epub .. " " .. jobname
1093                    report()
1094                    report("making epub file: ",command)
1095                    report()
1096                    os.execute(command) -- todo: also a runner
1097                end
1098                --
1099                if a_timing then
1100                    report()
1101                    report("you can process (timing) statistics with:",jobname)
1102                    report()
1103                    report("context --extra=timing '%s'",jobname)
1104                 -- report("mtxrun --script timing --xhtml [--launch --remove] '%s'",jobname)
1105                    report()
1106                end
1107            else
1108                if formatname then
1109                    report("error, no format found with name: %s, skipping",formatname)
1110                else
1111                    report("error, no format found (provide formatname or interface)")
1112                end
1113                break
1114            end
1115        end
1116    end
1117    --
1118    if #filelist > 1 then
1119        local done = false
1120        for k, v in sortedhash(changed) do
1121            if v then
1122                if not done then
1123                    report()
1124                    done = true
1125                end
1126                report("file %a was changed",k)
1127            end
1128        end
1129        if done then
1130            report()
1131        end
1132    end
1133end
1134
1135function scripts.context.pipe() -- still used?
1136    -- context --pipe
1137    -- context --pipe --purge --dummyfile=whatever.tmp
1138    local interface = getargument("interface")
1139    interface = (type(interface) == "string" and interface) or "en"
1140    local formatname = formatofinterface[interface] or "cont-en"
1141    local formatfile, scriptfile = resolvers.locateformat(formatname)
1142    if not formatfile or not scriptfile then
1143        report("warning: no format found, forcing remake (commandline driven)")
1144        scripts.context.make(formatname)
1145        formatfile, scriptfile = resolvers.locateformat(formatname)
1146    end
1147    if formatfile and scriptfile then
1148        local okay = statistics.checkfmtstatus(formatfile)
1149        if okay ~= true then
1150            report("warning: %s, forcing remake",tostring(okay))
1151            scripts.context.make(formatname)
1152        end
1153        local l_flags = {
1154            interaction = "scrollmode",
1155            fmt         = formatfile,
1156            lua         = scriptfile,
1157        }
1158        local c_flags = {
1159            backend     = "pdf",
1160            final       = false,
1161            kindofrun   = 3,
1162            currentrun  = 1,
1163        }
1164        local filename = getargument("dummyfile") or ""
1165        if filename == "" then
1166            filename = "\\relax"
1167            report("entering scrollmode, end job with \\end")
1168        else
1169            filename = fileaddsuffix(filename,"tmp")
1170            io.savedata(filename,"\\relax")
1171            report("entering scrollmode using '%s' with optionfile, end job with \\end",filename)
1172        end
1173        local returncode = environment.run_format(
1174            formatfile,
1175            scriptfile,
1176            filename,
1177            flags_to_string(l_flags),
1178            flags_to_string(c_flags,true),
1179            verbose
1180        )
1181        if getargument("purge") then
1182            scripts.context.purge_job(filename)
1183        elseif getargument("purgeall") then
1184            scripts.context.purge_job(filename,true)
1185            removefile(filename)
1186        end
1187    elseif formatname then
1188        report("error, no format found with name: %s, aborting",formatname)
1189    else
1190        report("error, no format found (provide formatname or interface)")
1191    end
1192end
1193
1194local function make_mkiv_format(name,engine)
1195    environment.make_format(name) -- jit is picked up later
1196end
1197
1198local make_mkii_format
1199
1200do -- more or less copied from mtx-plain.lua:
1201
1202    local function mktexlsr()
1203        if environment.arguments.silent then
1204            local result = os.execute("mktexlsr --quiet > temp.log")
1205            if result ~= 0 then
1206                print("mktexlsr silent run > fatal error") -- we use a basic print
1207            else
1208                print("mktexlsr silent run") -- we use a basic print
1209            end
1210            removefile("temp.log")
1211        else
1212            report("running mktexlsr")
1213            os.execute("mktexlsr")
1214        end
1215    end
1216
1217    local function engine(texengine,texformat)
1218        local command = string.format('%s --ini --etex --8bit %s \\dump',texengine,fileaddsuffix(texformat,"mkii"))
1219        if environment.arguments.silent then
1220            starttiming()
1221            local command = format("%s > temp.log",command)
1222            local result  = os.execute(command)
1223            local runtime = stoptiming()
1224            if result ~= 0 then
1225                print(format("%s silent make > fatal error when making format %q",texengine,texformat)) -- we use a basic print
1226            else
1227                print(format("%s silent make > format %q made in %.3f seconds",texengine,texformat,runtime)) -- we use a basic print
1228            end
1229            removefile("temp.log")
1230        else
1231            report("running command: %s",command)
1232            os.execute(command)
1233        end
1234    end
1235
1236    local function resultof(...)
1237        local command = string.format(...)
1238        report("running command %a",command)
1239        return string.strip(os.resultof(command) or "")
1240    end
1241
1242    local function make(texengine,texformat)
1243        report("generating kpse file database")
1244        mktexlsr()
1245        local fmtpathspec = resultof("kpsewhich --var-value=TEXFORMATS --engine=%s",texengine)
1246        if fmtpathspec ~= "" then
1247            report("using path specification %a",fmtpathspec)
1248            fmtpathspec = resultof('kpsewhich -expand-braces="%s"',fmtpathspec)
1249        end
1250        if fmtpathspec ~= "" then
1251            report("using path expansion %a",fmtpathspec)
1252        else
1253            report("no valid path reported, trying alternative")
1254            fmtpathspec = resultof("kpsewhich --show-path=fmt --engine=%s",texengine)
1255            if fmtpathspec ~= "" then
1256                report("using path expansion %a",fmtpathspec)
1257            else
1258                report("no valid path reported, falling back to current path")
1259                fmtpathspec = "."
1260            end
1261        end
1262        fmtpathspec = string.splitlines(fmtpathspec)[1] or fmtpathspec
1263        fmtpathspec = fmtpathspec and file.splitpath(fmtpathspec)
1264        local fmtpath = nil
1265        if fmtpathspec then
1266            for i=1,#fmtpathspec do
1267                local path = fmtpathspec[i]
1268                if path ~= "." then
1269                    dir.makedirs(path)
1270                    if lfs.isdir(path) and file.is_writable(path) then
1271                        fmtpath = path
1272                        break
1273                    end
1274                end
1275            end
1276        end
1277        if not fmtpath or fmtpath == "" then
1278            fmtpath = "."
1279        else
1280            lfs.chdir(fmtpath)
1281        end
1282        engine(texengine,texformat)
1283        report("generating kpse file database")
1284        mktexlsr()
1285        report("format %a saved on path %a",texformat,fmtpath)
1286    end
1287
1288    local function run(texengine,texformat,filename)
1289        local t = { }
1290        for k, v in next, environment.arguments do
1291            t[#t+1] = string.format("--mtx:%s=%s",k,v)
1292        end
1293        execute('%s --fmt=%s %s "%s"',texengine,removesuffix(texformat),table.concat(t," "),filename)
1294    end
1295
1296    make_mkii_format = function(name,engine)
1297
1298        -- let the binary sort it out
1299
1300        os.setenv('SELFAUTOPARENT', "")
1301        os.setenv('SELFAUTODIR',    "")
1302        os.setenv('SELFAUTOLOC',    "")
1303        os.setenv('TEXROOT',        "")
1304        os.setenv('TEXOS',          "")
1305        os.setenv('TEXMFOS',        "")
1306        os.setenv('TEXMFCNF',       "")
1307
1308        make(engine,name)
1309    end
1310
1311end
1312
1313function scripts.context.generate()
1314    resolvers.renewcache()
1315    trackers.enable("resolvers.locating")
1316    resolvers.load()
1317end
1318
1319function scripts.context.make(name)
1320    if not getargument("fast") then -- as in texexec
1321        scripts.context.generate()
1322    end
1323    local list = (name and { name }) or (environment.filenames[1] and environment.filenames) or defaultformats
1324    local engine = getargument("engine") or (status and status.luatex_engine) or "luatex"
1325    if getargument("jit") then
1326        engine = "luajittex"
1327    end
1328    for i=1,#list do
1329        local name = list[i]
1330        name = formatofinterface[name] or name or ""
1331        if name == "" then
1332            -- nothing
1333        elseif engine == "luametatex" or engine == "luatex" or engine == "luajittex" then
1334            make_mkiv_format(name,engine)
1335        elseif engine == "pdftex" or engine == "xetex" then
1336            make_mkii_format(name,engine)
1337        end
1338    end
1339end
1340
1341function scripts.context.ctx()
1342    local ctxdata = ctxrunner.new()
1343    ctxdata.jobname = environment.filenames[1]
1344    ctxrunner.checkfile(ctxdata,getargument("ctx"))
1345    ctxrunner.checkflags(ctxdata)
1346    scripts.context.run(ctxdata)
1347end
1348
1349function scripts.context.autoctx()
1350    local ctxdata   = nil
1351    local files     = environment.filenames
1352    local firstfile = #files > 0 and files[1]
1353    if firstfile then
1354        local suffix  = filesuffix(firstfile)
1355        local ctxname = nil
1356        if suffix == "xml" then
1357            local chunk = io.loadchunk(firstfile) -- 1024
1358            if chunk then
1359                ctxname = match(chunk,"<%?context%-directive%s+job%s+ctxfile%s+([^ ]-)%s*?>")
1360            end
1361        elseif suffix == "tex" or suffix == "mkiv" or suffix == "mkxl" then
1362            local analysis = preamble_analyze(firstfile)
1363            ctxname = analysis.ctxfile or analysis.ctx
1364        end
1365        if ctxname then
1366            ctxdata = ctxrunner.new()
1367            ctxdata.jobname = firstfile
1368            ctxrunner.checkfile(ctxdata,ctxname)
1369            ctxrunner.checkflags(ctxdata)
1370        end
1371    end
1372    scripts.context.run(ctxdata)
1373end
1374
1375function scripts.context.version()
1376    local list = { "context.mkiv", "context.mkxl" }
1377    for i=1,#list do
1378        local base = list[i]
1379        local name = resolvers.findfile(base)
1380        if name ~= "" then
1381            report("main context file: %s",name)
1382            local data = io.loaddata(name)
1383            if data then
1384                local version = match(data,"\\edef\\contextversion{(.-)}")
1385                if version then
1386                    report("current version: %s",version)
1387                else
1388                    report("context version: unknown, no timestamp found")
1389                end
1390            else
1391                report("context version: unknown, load error")
1392            end
1393        else
1394            report("main context file: unknown, %a not found",base)
1395        end
1396    end
1397end
1398
1399function scripts.context.purge_job(jobname,all,mkiitoo,fulljobname)
1400    if jobname and jobname ~= "" then
1401        jobname = filebasename(jobname)
1402        local filebase = removesuffix(jobname)
1403        if mkiitoo then
1404            scripts.context.purge(all,filebase,true) -- leading "./"
1405        else
1406            local deleted = { }
1407            for i=1,#obsolete_results do
1408                deleted[#deleted+1] = purge_file(fileaddsuffix(filebase,obsolete_results[i]),fileaddsuffix(filebase,"pdf"))
1409            end
1410            for i=1,#temporary_runfiles do
1411                deleted[#deleted+1] = purge_file(fileaddsuffix(filebase,temporary_runfiles[i]))
1412            end
1413            if fulljobname and fulljobname ~= jobname then
1414                for i=1,#temporary_suffixes do
1415                    deleted[#deleted+1] = purge_file(fileaddsuffix(fulljobname,temporary_suffixes[i],true))
1416                end
1417            end
1418            if all then
1419                for i=1,#persistent_runfiles do
1420                    deleted[#deleted+1] = purge_file(fileaddsuffix(filebase,persistent_runfiles[i]))
1421                end
1422            end
1423            if #deleted > 0 then
1424                report("purged files: %s", concat(deleted,", "))
1425            end
1426        end
1427    end
1428end
1429
1430function scripts.context.purge(all,pattern,mkiitoo)
1431    local all        = all or getargument("all")
1432    local pattern    = getargument("pattern") or (pattern and (pattern.."*")) or "*.*"
1433    local files      = dir.glob(pattern)
1434    local obsolete   = tohash(obsolete_results)
1435    local temporary  = tohash(temporary_runfiles)
1436    local synctex    = tohash(synctex_runfiles)
1437    local persistent = tohash(persistent_runfiles)
1438    local generic    = tohash(generic_files)
1439    local deleted    = { }
1440    for i=1,#files do
1441        local name = files[i]
1442        local suffix = filesuffix(name)
1443        local basename = filebasename(name)
1444        if obsolete[suffix] or temporary[suffix] or synctex[suffix] or persistent[suffix] or generic[basename] then
1445            deleted[#deleted+1] = purge_file(name)
1446        elseif mkiitoo then
1447            for i=1,#special_runfiles do
1448                if find(name,special_runfiles[i]) then
1449                    deleted[#deleted+1] = purge_file(name)
1450                end
1451            end
1452        end
1453        for i=1,#extra_runfiles do
1454            if find(basename,extra_runfiles[i]) then
1455                deleted[#deleted+1] = purge_file(name)
1456            end
1457        end
1458    end
1459    if #deleted > 0 then
1460        report("purged files: %s", concat(deleted,", "))
1461    end
1462end
1463
1464-- touching files (signals regeneration of formats)
1465
1466local newversion = false
1467
1468local function touch(path,name,versionpattern,kind,kindpattern)
1469    if path and path ~= "" then
1470        name = filejoinname(path,name)
1471    else
1472        name = resolvers.findfile(name)
1473    end
1474    local olddata = io.loaddata(name)
1475    if olddata then
1476        local oldkind = ""
1477        local newkind = kind or ""
1478        local oldversion = ""
1479        local newdata
1480              newversion = newversion or os.date("%Y.%m.%d %H:%M")
1481        if versionpattern then
1482            newdata = gsub(olddata,versionpattern,function(pre,mid,post)
1483                oldversion = mid
1484                return pre .. newversion .. post
1485            end) or olddata
1486        end
1487        if kind and kindpattern then
1488            newdata = gsub(newdata,kindpattern,function(pre,mid,post)
1489                oldkind = mid
1490                return pre .. newkind .. post
1491            end) or newdata
1492        end
1493        if newdata ~= "" and (oldversion ~= newversion or oldkind ~= newkind or newdata ~= olddata) then
1494            local backup = filenewsuffix(name,"tmp")
1495            removefile(backup)
1496            renamefile(name,backup)
1497            io.savedata(name,newdata)
1498            return name, oldversion, newversion, oldkind, newkind
1499        end
1500    end
1501end
1502
1503local p_contextkind       = "(\\edef\\contextkind%s*{)(.-)(})"
1504local p_contextversion    = "(\\edef\\contextversion%s*{)(.-)(})"
1505local p_newcontextversion = "(\\newcontextversion%s*{)(.-)(})"
1506
1507local function touchfiles(suffix,kind,path)
1508    local foundname, oldversion, newversion, oldkind, newkind = touch(path,fileaddsuffix("context",suffix),p_contextversion,kind,p_contextkind)
1509    if foundname then
1510        report("old version  : %s (%s)",oldversion,oldkind)
1511        report("new version  : %s (%s)",newversion,newkind)
1512        report("touched file : %s",foundname)
1513        local foundname = touch(path,fileaddsuffix("cont-new",suffix),p_newcontextversion)
1514        if foundname then
1515            report("touched file : %s", foundname)
1516        end
1517    else
1518        report("nothing touched")
1519    end
1520end
1521
1522local tobetouched = tohash { "mkii", "mkiv", "mkvi", "mkxl", "mklx" }
1523
1524function scripts.context.touch()
1525    if getargument("expert") then
1526        local touch = getargument("touch")
1527        local kind  = getargument("kind")
1528        local path  = getargument("basepath")
1529        if tobetouched[touch] then -- mkix mkxi ctix ctxi
1530            touchfiles(touch,kind,path)
1531        else
1532            for touch in sortedhash(tobetouched) do
1533                touchfiles(touch,kind,path)
1534            end
1535        end
1536    else
1537        report("touching needs --expert")
1538    end
1539end
1540
1541function scripts.context.pages()
1542    local filename = environment.files[1]
1543    if filename then
1544        local u = table.load(fileaddsuffix(filename,"tuc"))
1545        if u then
1546            local p = u.structures.pages.collected
1547            local l = u.structures.lists.collected
1548            local page = environment.arguments.page
1549            local list = environment.arguments.list
1550            if type(page) == "string" then
1551                page = settings_to_array(page)
1552            end
1553            if type(list) == "string" then
1554                list = settings_to_array(list)
1555            end
1556            if page or list then
1557                if page then
1558                    for i=1,#page do
1559                        page[i] = string.topattern(page[i])
1560                    end
1561                    for i=1,#p do
1562                        local pi = p[i]
1563                        local m = pi.marked
1564                        if m then
1565                            local ml = #m
1566                            for j=1,#page do
1567                                local n = page[j]
1568                                for k=1,ml do
1569                                    if find(m[k],n) then
1570                                        report("page : %04i %s",i,m[k])
1571                                    end
1572                                end
1573                            end
1574                        end
1575                    end
1576                end
1577                if list then
1578                    for i=1,#list do
1579                        list[i] = string.topattern(list[i])
1580                    end
1581                    for i=1,#l do
1582                        local li = l[i]
1583                        local r = li.references
1584                        if r then
1585                            local rr = r.reference
1586                            if rr then
1587                                rr = splitstring(rr,",")
1588                                local rrl = #rr
1589                                for j=1,#list do
1590                                    local n = list[j]
1591                                    for k=1,rrl do
1592                                        if find(rr[k],n) then
1593                                            report("list : %04i %s",r.realpage,rr[k])
1594                                        end
1595                                    end
1596                                end
1597                            end
1598                        end
1599                    end
1600                end
1601            else
1602                for i=1,#p do
1603                    local pi = p[i]
1604                    local m = pi.marked
1605                    if m then
1606                        report("page : %04i % t",i,m)
1607                    end
1608                end
1609            end
1610        end
1611    end
1612end
1613
1614-- modules
1615
1616local labels = { "title", "comment", "status" }
1617local cards  = { "*.mkiv", "*.mkvi",  "*.mkix", "*.mkxi", "*.mkxl", "*.mklx", "*.tex" }
1618local valid  = tohash { "mkiv", "mkvi", "mkix", "mkxi", "mkxl", "mklx", "tex" }
1619
1620function scripts.context.modules(pattern)
1621    local list = { }
1622    local found = resolvers.findfile("context.mkiv")
1623    if not pattern or pattern == "" then
1624        -- official files in the tree
1625        for i=1,#cards do
1626            resolvers.findwildcardfiles(cards[i],list)
1627        end
1628        -- my dev path
1629        for i=1,#cards do
1630            dir.glob(filejoinname(filepathpart(found),cards[i]),list)
1631        end
1632    else
1633        resolvers.findwildcardfiles(pattern,list)
1634        dir.glob(filejoinname(filepathpart(found,pattern)),list)
1635    end
1636    local done = { } -- todo : sort
1637    local none = { x = { }, m = { }, s = { }, t = { } }
1638    for i=1,#list do
1639        local v = list[i]
1640        local base = filebasename(v)
1641        if not done[base] then
1642            done[base] = true
1643            local suffix = filesuffix(base)
1644            if valid[suffix] then
1645                local prefix, rest = match(base,"^([xmst])%-(.*)")
1646                if prefix then
1647                    v = resolvers.findfile(base) -- so that files on my dev path are seen
1648                    local data = io.loaddata(v) or ""
1649                    data = match(data,"%% begin info(.-)%% end info")
1650                    if data then
1651                        local info = { }
1652                        for label, text in gmatch(data,"%% +([^ ]+) *: *(.-)[\n\r]") do
1653                            info[label] = text
1654                        end
1655                        report()
1656                        report("%-7s : %s","module",base)
1657                        report()
1658                        for i=1,#labels do
1659                            local l = labels[i]
1660                            if info[l] then
1661                                report("%-7s : %s",l,info[l])
1662                            end
1663                        end
1664                        report()
1665                    else
1666                        insert(none[prefix],rest)
1667                    end
1668                end
1669            end
1670        end
1671    end
1672
1673    local function show(k,v)
1674        sort(v)
1675        if #v > 0 then
1676            report()
1677            for i=1,#v do
1678                report("%s : %s",k,v[i])
1679            end
1680        end
1681    end
1682    for k, v in sortedhash(none) do
1683        show(k,v)
1684    end
1685end
1686
1687-- extras
1688
1689function scripts.context.extras(pattern)
1690    -- only in base path, i.e. only official ones
1691    if type(pattern) ~= "string" then
1692        pattern = "*"
1693    end
1694    local found = resolvers.findfile("context.mkiv")
1695    if found ~= "" then
1696        pattern = filejoinname(dir.expandname(filepathpart(found)),format("mtx-context-%s.tex",pattern or "*"))
1697        local list = dir.glob(pattern)
1698        for i=1,#list do
1699            local v = list[i]
1700            local data = io.loaddata(v) or ""
1701            data = match(data,"%% begin help(.-)%% end help")
1702            if data then
1703                report()
1704                report("extra: %s (%s)",(gsub(v,"^.*mtx%-context%-(.-)%.tex$","%1")),v)
1705                for s in gmatch(data,"%% *(.-)[\n\r]") do
1706                    report(s)
1707                end
1708                report()
1709            end
1710        end
1711    end
1712end
1713
1714function scripts.context.extra()
1715    local extra = getargument("extra")
1716    if type(extra) ~= "string" then
1717        scripts.context.extras()
1718    elseif getargument("help") then
1719        scripts.context.extras(extra)
1720    else
1721        local fullextra = extra
1722        if not find(fullextra,"mtx%-context%-") then
1723            fullextra = "mtx-context-" .. extra
1724        end
1725        local foundextra = resolvers.findfile(fullextra)
1726        if foundextra == "" then
1727            scripts.context.extras()
1728            return
1729        else
1730            report("processing extra: %s", foundextra)
1731        end
1732        setargument("purgeall",true)
1733        local result = getargument("result") or ""
1734        if result == "" then
1735            setargument("result","context-extra")
1736        end
1737        scripts.context.run(nil,foundextra)
1738    end
1739end
1740
1741-- experiment
1742
1743do
1744
1745    local popen   = io.popen
1746    local close   = io.close
1747    ----- read    = io.read
1748    local gobble  = io.gobble or function(f) f:read("l") end
1749    ----- clock   = os.clock
1750    local ticks   = lua.getpreciseticks
1751    local seconds = lua.getpreciseseconds
1752
1753    local f_runner  = formatters['context %s "%s"']
1754    local f_command = formatters['%s %s']
1755
1756    function scripts.context.parallel()
1757        if getargument("pattern") then
1758            environment.files = dir.glob(getargument("pattern"))
1759        end
1760        local files = environment.files
1761        local total = files and #files or 0
1762        local list  = nil
1763        if getargument("parallellist") then
1764            list = { }
1765            for i=1,#files do
1766                -- could be an lpeg
1767                local name = files[i]
1768                local data = string.splitlines(io.loaddata(name) or "")
1769                if data then
1770                    for i=1,#data do
1771                        local line = data[i]
1772                        if string.find(line,"^context ") then
1773                            list[#list+1] = line
1774                        end
1775                    end
1776                end
1777            end
1778            files = list
1779            total = #list
1780        end
1781        if not list and total == 1 then
1782            -- Beware: "--parallel" and "--terminal" are passed to the single run but this
1783            -- is normally harmless.
1784            scripts.context.autoctx()
1785        elseif total > 0 then
1786            local results  = { }
1787            local start    = starttiming("parallel")
1788            local terminal = environment.argument("terminal")
1789            local runners  = tonumber(environment.argument("parallel")) or 8
1790            local process  = { }
1791            local count    = 0
1792            -- a hack
1793            local passthese = environment.arguments_after
1794            for i=1,#passthese do
1795                local a = passthese[i]
1796                if string.find(a,"^%-%-parallel") or string.find(a,"^%-%-terminal") or not find(a,"^%-%-") then
1797                    passthese[i] = ""
1798                end
1799            end
1800            passthese = table.unique(passthese)
1801            -- end of hack
1802            local arguments = environment.reconstructcommandline(passthese)
1803            local whattodo  = list and "command" or "filename"
1804            while true do
1805                local done = false
1806                for i=1,runners do
1807                    local pi = process[i]
1808                    if pi then
1809                        local s
1810                        if terminal then
1811                            s = pi.handle:read("l")
1812                            if s then
1813                                done = true
1814                                report("%02i : %s",i,s)
1815                                goto done
1816                            end
1817                        else
1818                            s = gobble(pi.handle)
1819                            if s then
1820                                done = true
1821                                goto done
1822                            end
1823                        end
1824                        if not s then
1825                            local r, detail, n = close(pi.handle)
1826                            stoptiming(pi.timer)
1827                            pi.result = (not r or n > 0) and "error" or "done"
1828                            pi.time   = elapsedtime(pi.timer)
1829                            pi.handle = nil
1830                            pi.timer  = nil
1831                            if terminal then
1832                                report()
1833                            end
1834                            report("process %02i, index %02i, %s %a, status %a, runtime %0.3f",i,pi.count,whattodo,pi.filename,pi.result,pi.time)
1835                            if terminal then
1836                                report()
1837                            end
1838                            process[i] = false
1839                            results[pi.count] = pi
1840                        end
1841                    end
1842                    count = count + 1
1843                    if count > total then
1844                        -- we're done
1845                    else
1846                        local timer    = "parallel:" .. i
1847                        local filename = files[count]
1848                        local dirname  = file.dirname(filename)
1849                        local basename = file.basename(filename)
1850                        if dirname ~= "." and dirname ~= "./" then
1851                            dir.push(dirname)
1852                        end
1853                        local command  = list and f_command(basename,arguments) or f_runner(arguments,basename)
1854                        starttiming(timer)
1855                        local result  = popen(command)
1856                        if dirname ~= "." then
1857                            dir.pop()
1858                        end
1859                        local status  = nil
1860                        if result then
1861                            process[i] = {
1862                                handle   = result,
1863                                result   = "start",
1864                                filename = filename,
1865                                count    = count,
1866                                time     = 0,
1867                                timer    = timer,
1868                            }
1869                            status = process[i]
1870                        else
1871                            status = {
1872                                result   = "error",
1873                                count    = count,
1874                                filename = filename,
1875                                time     = 0,
1876                            }
1877                            stoptiming(timer)
1878                        end
1879                        results[count] = status
1880                        if terminal then
1881                            report()
1882                        end
1883                        report("process %02i, index %02i, %s %a, status %a",i,status.count,whattodo,status.filename,status.result)
1884                        if terminal then
1885                            report()
1886                        end
1887                        done = true
1888                    end
1889                  ::done::
1890                end
1891                if not done then
1892                    break
1893                end
1894            end
1895            stoptiming("parallel")
1896            results.runtime = elapsedtime("parallel")
1897            report()
1898            report("files: %i, runtime: %s",total,results.runtime)
1899            report()
1900            local errors = { }
1901            for i=1,total do
1902                local ri = results[i]
1903                local result   = ri.result
1904                local filename = ri.filename
1905                if result == "error" then
1906                    errors[#errors+1] = filename
1907                end
1908                report("index %02i, %s %a, status %a, runtime %0.3f ",ri.count,whattodo,filename,result,ri.time)
1909            end
1910            if #errors > 0 then
1911                report()
1912                report("errors in:")
1913                report()
1914                for i=1,#errors do
1915                    report("  %s",errors[i])
1916                end
1917            end
1918            report()
1919        end
1920    end
1921
1922end
1923
1924-- todo: we need to do a dummy run
1925
1926local function showsetter()
1927    environment.files = { resolvers.findfile("mtx-context-setters.tex") }
1928    multipass_nofruns = 1
1929    setargument("purgeall",true)
1930    scripts.context.run()
1931end
1932
1933scripts.context.trackers    = showsetter
1934scripts.context.directives  = showsetter
1935scripts.context.experiments = showsetter
1936
1937function scripts.context.logcategories()
1938    environment.files = { resolvers.findfile("m-logcategories.mkiv") }
1939    multipass_nofruns = 1
1940    setargument("purgeall",true)
1941    scripts.context.run()
1942end
1943
1944function scripts.context.timed(action)
1945    statistics.timed(action,true)
1946end
1947
1948-- getting it done
1949
1950if getargument("pdftex") then
1951    setargument("engine","pdftex")
1952elseif getargument("xetex") then
1953    setargument("engine","xetex")
1954end
1955
1956if getargument("timedlog") then
1957    logs.settimedlog()
1958end
1959
1960if getargument("nostats") then
1961    setargument("nostatistics",true)
1962    setargument("nostat",nil)
1963end
1964
1965if getargument("batch") then
1966    setargument("batchmode",true)
1967    setargument("batch",nil)
1968end
1969
1970if getargument("nonstop") then
1971    setargument("nonstopmode",true)
1972    setargument("nonstop",nil)
1973end
1974
1975do
1976
1977    local htmlerrorpage = getargument("htmlerrorpage")
1978    if htmlerrorpage == "scite" then
1979        directives.enable("system.showerror=scite")
1980    elseif htmlerrorpage then
1981        directives.enable("system.showerror")
1982    end
1983
1984end
1985
1986do
1987
1988    local silent = getargument("silent")
1989    if type(silent) == "string" then
1990        directives.enable(format("logs.blocked={%s}",silent))
1991    elseif silent then
1992        directives.enable("logs.blocked")
1993    end
1994
1995    local errors = getargument("errors")
1996    if type(errors) == "errors" then
1997        directives.enable(format("logs.errors={%s}",silent))
1998    elseif errors then
1999        directives.enable("logs.errors")
2000    end
2001
2002end
2003
2004if getargument("once") then
2005    multipass_nofruns = 1
2006else
2007    if getargument("runs") then
2008        multipass_nofruns = tonumber(getargument("runs")) or nil
2009    end
2010    multipass_forcedruns = tonumber(getargument("forcedruns")) or nil
2011end
2012
2013if getargument("parallel") or getargument("parallellist") then
2014    scripts.context.timed(scripts.context.parallel)
2015elseif getargument("run") then
2016    scripts.context.timed(scripts.context.autoctx)
2017elseif getargument("make") then
2018    scripts.context.timed(function() scripts.context.make() end)
2019elseif getargument("generate") then
2020    scripts.context.timed(function() scripts.context.generate() end)
2021elseif getargument("ctx") and not getargument("noctx") then
2022    scripts.context.timed(scripts.context.ctx)
2023elseif getargument("version") then
2024    application.identify()
2025    scripts.context.version()
2026elseif getargument("touch") then
2027    scripts.context.touch()
2028elseif getargument("pages") then
2029    scripts.context.pages()
2030elseif getargument("expert") then
2031    application.help("expert", "special")
2032elseif getargument("showmodules") or getargument("modules") then
2033    scripts.context.modules()
2034elseif getargument("showextras") or getargument("extras") then
2035    scripts.context.extras(environment.filenames[1] or getargument("extras"))
2036elseif getargument("extra") then
2037    scripts.context.extra()
2038elseif getargument("exporthelp") then
2039 -- application.export(getargument("exporthelp"),environment.filenames[1])
2040    application.export()
2041elseif getargument("help") then
2042    if environment.filenames[1] == "extras" then
2043        scripts.context.extras()
2044    else
2045        application.help("basic")
2046    end
2047elseif getargument("showtrackers") or getargument("trackers") == true then
2048    scripts.context.trackers()
2049elseif getargument("showdirectives") or getargument("directives") == true then
2050    scripts.context.directives()
2051elseif getargument("showlogcategories") then
2052    scripts.context.logcategories()
2053elseif environment.filenames[1] or getargument("nofile") then
2054 -- --
2055 -- -- How compatible is this ... we might want to resolve the wildcard at the TeX, so
2056 -- -- we just keep this as unsuported feature (but at least we know of this case):
2057 --
2058 -- if not getargument("pattern") and find(environment.filenames[1],"%*") then
2059 --     environment.filenames = dir.glob(environment.filenames[1])
2060 --  -- setargument("pattern",dir.glob(environment.filenames[1]))
2061 -- end
2062 --
2063    scripts.context.timed(scripts.context.autoctx)
2064elseif getargument("pipe") then
2065    scripts.context.timed(scripts.context.pipe)
2066elseif getargument("purge") then
2067    -- only when no filename given, supports --pattern
2068    scripts.context.purge()
2069elseif getargument("purgeall") then
2070    -- only when no filename given, supports --pattern
2071    scripts.context.purge(true,nil,true)
2072elseif getargument("pattern") then
2073    environment.filenames = dir.glob(getargument("pattern"))
2074    scripts.context.timed(scripts.context.autoctx)
2075else
2076    application.help("basic")
2077end
2078
2079-- we can wipe a signal file when done
2080
2081do
2082
2083    if getargument("wipebusy") then
2084        removefile("context-is-busy.tmp")
2085    end
2086
2087end
2088