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