mtx-install.lua /size: 19 Kb    last modification: 2024-01-16 09:02
1if not modules then modules = { } end modules ['mtx-install'] = {
2    version   = 1.002,
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: initial install from zip
10
11local helpinfo = [[
12<?xml version="1.0"?>
13<application>
14 <metadata>
15  <entry name="name">mtx-install</entry>
16  <entry name="detail">ConTeXt Installer</entry>
17  <entry name="version">2.01</entry>
18 </metadata>
19 <flags>
20  <category name="basic">
21   <subcategory>
22    <flag name="platform" value="string"><short>platform</short></flag>
23    <flag name="server" value="string"><short>repository url (rsync://contextgarden.net)</short></flag>
24    <flag name="modules" value="string"><short>extra modules (can be list or 'all')</short></flag>
25    <flag name="fonts" value="string"><short>additional fonts (can be list or 'all')</short></flag>
26    <flag name="goodies" value="string"><short>extra binaries (like scite and texworks)</short></flag>
27    <flag name="install"><short>install context</short></flag>
28    <flag name="update"><short>update context</short></flag>
29    <flag name="erase"><short>wipe the cache</short></flag>
30    <flag name="identify"><short>create list of files</short></flag>
31    <flag name="secure"><short>use curl for https</short></flag>
32   </subcategory>
33  </category>
34 </flags>
35</application>
36]]
37
38local type, tonumber = type, tonumber
39local gsub, find, escapedpattern = string.gsub, string.find, string.escapedpattern
40local round = math.round
41local savetable, loadtable, sortedhash = table.save, table.load, table.sortedhash
42local copyfile, joinfile, filesize, dirname, addsuffix, basename = file.copy, file.join, file.size, file.dirname, file.addsuffix, file.basename
43local isdir, isfile, walkdir, pushdir, popdir, currentdir = lfs.isdir, lfs.isfile, lfs.dir, dir.push, dir.pop, dir.current
44local mkdirs, globdir = dir.mkdirs, dir.glob
45local osremove, osexecute, ostype, resultof = os.remove, os.execute, os.type, os.resultof
46local savedata, loaddata = io.savedata, io.loaddata
47local formatters = string.formatters
48local httprequest = socket.http.request
49
50local usecurl  = false
51local protocol = "http"
52
53local function checkcurl()
54    local s = resultof("curl --version")
55    return type(s) == "string" and find(s,"libcurl") and find(s,"rotocols")
56end
57
58local function fetch(url)
59    local data   = nil
60    local detail = nil
61    if usecurl and find(url,"^https") then
62        data = resultof("curl " .. url)
63    else
64        data, detail = httprequest(url)
65    end
66    if type(data) ~= "string" then
67        data   = false
68        detail = "download failed"
69    elseif #data == 0 then
70        data   = false
71        detail = "download failed, zero length"
72    elseif #data < 2048 then
73        local n, t = find(data,"<head>%s*<title>%s*(%d+)%s(.-)</title>")
74        if tonumber(n) then
75            data   = false
76            detail = n .. " " .. t
77        end
78    end
79    return data, detail
80end
81
82local application = logs.application {
83    name     = "mtx-install",
84    banner   = "ConTeXt Installer 2.01",
85    helpinfo = helpinfo,
86}
87
88local report = application.report
89
90scripts         = scripts         or { }
91scripts.install = scripts.install or { }
92local install   = scripts.install
93
94local texformats = {
95    "cont-en",
96    "cont-nl",
97    "cont-cz",
98    "cont-de",
99    "cont-fa",
100    "cont-it",
101    "cont-ro",
102    "cont-uk",
103    "cont-pe",
104}
105
106local platforms = {
107 -- ["mswin"]          = "mswin",
108 -- ["windows"]        = "mswin",
109 -- ["win32"]          = "mswin",
110 -- ["win"]            = "mswin",
111    --
112    ["windows"]        = "win64",
113    ["windows-64"]     = "win64",
114    ["win64"]          = "win64",
115    --
116 -- ["linux"]          = "linux",
117 -- ["linux-32"]       = "linux",
118 -- ["linux32"]        = "linux",
119    --
120    ["linux"]          = "linux-64",
121    ["linux-64"]       = "linux-64",
122    ["linux64"]        = "linux-64",
123    --
124    ["linuxmusl"]      = "linuxmusl",
125    ["linuxmusl-64"]   = "linuxmusl-64",
126    --
127 -- ["linux-armhf"]    = "linux-armhf",
128    --
129    ["openbsd"]        = "openbsd-amd64",
130 -- ["openbsd-i386"]   = "openbsd7.3",
131    ["openbsd-amd64"]  = "openbsd-amd64",
132    --
133    ["freebsd"]        = "freebsd-amd64",
134 -- ["freebsd-i386"]   = "freebsd",
135    ["freebsd-amd64"]  = "freebsd-amd64",
136    --
137 -- ["kfreebsd"]       = "kfreebsd-i386",
138 -- ["kfreebsd-i386"]  = "kfreebsd-i386",
139 -- ["kfreebsd-amd64"] = "kfreebsd-amd64",
140    --
141 -- ["linux-ppc"]      = "linux-ppc",
142 -- ["ppc"]            = "linux-ppc",
143    --
144 -- ["osx"]            = "osx-intel",
145 -- ["macosx"]         = "osx-intel",
146 -- ["osx-intel"]      = "osx-intel",
147 -- ["osxintel"]       = "osx-intel",
148    --
149 -- ["osx-ppc"]        = "osx-ppc",
150 -- ["osx-powerpc"]    = "osx-ppc",
151 -- ["osxppc"]         = "osx-ppc",
152 -- ["osxpowerpc"]     = "osx-ppc",
153    --
154    ["macosx"]         = "osx-64",
155    ["osx"]            = "osx-64",
156    ["osx-intel"]      = "osx-64",
157    ["osx-64"]         = "osx-64",
158    ["osx-arm"]        = "osx-arm64",
159    ["osx-arm64"]      = "osx-arm64",
160    --
161 -- ["solaris-intel"]  = "solaris-intel",
162    --
163 -- ["solaris-sparc"]  = "solaris-sparc",
164 -- ["solaris"]        = "solaris-sparc",
165    --
166    ["unknown"]        = "unknown",
167}
168
169function install.identify()
170
171    -- We have to be in "...../tex" where subdirectories are prefixed with
172    -- "texmf". We strip the "tex/texm*/" from the name in the list.
173
174    local hashdata = sha2 and sha2.HASH256 or md5.hex
175
176    local function collect(root,tree)
177
178        local path = root .. "/" .. tree
179
180        if isdir(path) then
181
182            local prefix  = path .. "/"
183            local files   = globdir(prefix .. "**")
184            local pattern = escapedpattern("^" .. prefix)
185
186            local details = { }
187            local total   = 0
188
189            for i=1,#files do
190                local name  = files[i]
191                local size  = filesize(name)
192                local base  = gsub(name,pattern,"")
193                local data  = loaddata(name)
194                if data and #data > 0 then
195                    local stamp = hashdata(data)
196                    details[i]  = { base, size, stamp }
197                    total       = total + size
198                else
199                    report("%-24s : bad file %a",tree,name)
200                end
201            end
202            report("%-24s : %4i files, %3.0f MB",tree,#files,total/(1000*1000))
203
204            savetable(path .. ".tma",details)
205
206        end
207
208    end
209
210    local sourceroot = file.join(dir.current(),"tex")
211
212    for d in walkdir("./tex") do
213        if find(d,"%texmf") then
214            collect(sourceroot,d)
215        end
216    end
217
218    savetable("./tex/status.tma",{
219        name    = "context",
220        version = "lmtx",
221        date    = os.date("%Y-%m-%d"),
222    })
223
224    os.exit()
225
226end
227
228local function disclaimer()
229    report("ConTeXt LMTX with LuaMetaTeX is still experimental and when you get a crash this")
230    report("can be due to a mismatch between Lua bytecode and the engine. In that case you can")
231    report("try the following:")
232    report("")
233    report("  - wipe the texmf-cache directory")
234    report("  - run: mtxrun --generate")
235    report("  - run: context --make")
236    report("")
237    report("When that doesn't solve the problem, ask on the mailing list (ntg-context@ntg.nl).")
238end
239
240function install.update()
241
242    local hashdata = sha2 and sha2.HASH256 or md5.hex
243
244    local function validdir(d)
245        local ok = isdir(d)
246        if not ok then
247            mkdirs(d)
248            ok = isdir(d)
249        end
250        return ok
251    end
252
253    local function download(what,url,target,total,done,hash)
254        local data = fetch(url .. "/" .. target)
255        if type(data) == "string" and #data > 0  then
256            if total and done then
257                report("%-8s : %3i %% : %8i : %s",what,round(100*done/total),#data,target)
258            else
259                report("%-8s : %8i : %s",what,#data,target)
260            end
261            if hash and hash ~= hashdata(data) then
262                return "different hash value"
263            elseif not validdir(dirname(target)) then
264                return "wrong target directory"
265            else
266                savedata(target,data)
267            end
268        else
269            return "unable to download"
270        end
271    end
272
273    local function remove(what,target)
274        report("%-8s : %8i : %s",what,filesize(target),target)
275        osremove(target)
276    end
277
278    local function ispresent(target)
279        return isfile(target)
280    end
281
282    local function hashed(list)
283        local hash = { }
284        for i=1,#list do
285            local l = list[i]
286            hash[l[1]] = l
287        end
288        return hash
289    end
290
291    local function run(fmt,...)
292        local command = formatters[fmt](...)
293     -- command = gsub(command,"/","\\")
294        report("running: %s",command)
295        osexecute(command)
296    end
297
298    local function prepare(tree)
299        tree = joinfile("tex",tree)
300        mkdirs(tree)
301    end
302
303    local function update(url,what,zipfile,skiplist)
304
305        local tree = joinfile("tex",what)
306
307        local ok = validdir(tree)
308        if not validdir(tree) then
309            report("invalid directory %a",tree)
310            return
311        end
312
313        local lua = tree .. ".tma"
314        local all = url .. "/" .. lua
315        local old = loadtable(lua)
316        local new = fetch(all)
317
318        if new then
319            new = loadstring(new)
320            if new then
321                new = new()
322            end
323        end
324
325        if not new then
326            report("invalid database %a",all)
327            return
328        end
329
330        local total = 0
331        local done  = 0
332        local count = 0
333
334        if not old then
335
336            if zipfile then
337                zipfile = addsuffix(what,"zip")
338            end
339            if zipfile then
340                local zipurl = url .. "/" .. zipfile
341                report("fetching %a",zipurl)
342                local zipdata = fetch(zipurl)
343                if zipdata then
344                    savedata(zipfile,zipdata)
345                else
346                    osremove(zipfile)
347                    zipfile = false
348                end
349            end
350
351            if type(zipfile) == "string" and isfile(zipfile) then
352
353                -- todo: pcall
354
355                report("unzipping %a",zipfile)
356
357                local specification = {
358                    zipname = zipfile,
359                    path    = ".",
360                 -- verbose = true,
361                    verbose = "steps",
362                    collect = true, -- sort of a check
363                }
364
365                if utilities.zipfiles.unzipdir(specification) then
366                    osremove(zipfile)
367                    goto done
368                else
369                    osremove(zipfile)
370                end
371            end
372
373            count = #new
374
375            report("installing %s, %i files",tree,count)
376
377            for i=1,count do
378                total = total + new[i][2]
379            end
380
381            for i=1,count do
382                local entry  = new[i]
383                local name   = entry[1]
384                local size   = entry[2]
385                local target = joinfile(tree,name)
386                done = done + size
387                if not skiplist or not skiplist[basename(name)] then
388                    local message = download("new",url,target,total,done)
389                    if message then
390                        report("%s",message)
391                    end
392                else
393                    report("skipping %s",target)
394                end
395            end
396
397            ::done::
398
399        else
400
401            report("updating %s, %i files",tree,#new)
402
403            local hold = hashed(old)
404            local hnew = hashed(new)
405            local todo = { }
406
407            for newname, newhash in sortedhash(hnew) do
408                local target = joinfile(tree,newname)
409                if not skiplist or not skiplist[basename(newname)] then
410                    local oldhash = hold[newname]
411                    local action  = nil
412                    if not oldhash then
413                        action = "added"
414                    elseif oldhash[3] ~= newhash[3] then
415                        action = "changed"
416                    elseif not ispresent(joinfile(tree,newname)) then
417                        action = "missing"
418                    end
419                    if action then
420                        local size = newhash[2]
421                        total = total + size
422                        todo[#todo+1] = {
423                            action = action,
424                            target = target,
425                            size   = size,
426                            hash   = newhash[3],
427                        }
428                    end
429                else
430                    report("skipping %s",target)
431                end
432            end
433
434            count = #todo
435
436            for i=1,count do
437                local entry = todo[i]
438                for i=1,5 do
439                    local target  = entry.target
440                    local message = download(entry.action,url,target,total,done,entry.hash)
441                    if message then
442                        if i == 5 then
443                            report("%s, try again later: %s",target)
444                            os.exit()
445                        else
446                            report("%s, trying again: %s",target)
447                            os.sleep(2)
448                        end
449                    else
450                        break
451                    end
452                end
453                done = done + entry.size
454            end
455
456            for oldname, oldhash in sortedhash(hold) do
457                local newhash = hnew[oldname]
458                local target  = joinfile(tree,oldname)
459                if not newhash and ispresent(target) then
460                    remove("removed",target)
461                end
462            end
463
464        end
465
466        savetable(lua,new)
467
468        return { tree, count, done }
469
470    end
471
472    local targetroot = dir.current()
473
474    local server     = environment.arguments.server   or ""
475    local instance   = environment.arguments.instance or ""
476    local osplatform = environment.arguments.platform or nil
477    local platform   = platforms[osplatform or os.platform or ""]
478
479    if (platform == "unknown" or platform == "" or not platform) and osplatform then
480        -- catches openbsdN.M kind of specifications
481        platform = osplatform
482    elseif not osplatform then
483        osplatform = platform
484    end
485
486    if server == "" then
487        server = "lmtx.contextgarden.net,lmtx.pragma-ade.com,lmtx.pragma-ade.nl,dmz.pragma-ade.nl"
488    end
489    if instance == "" then
490        instance = "install-lmtx"
491    end
492    if not platform then
493        report("unknown platform")
494        return
495    end
496
497    local list   = utilities.parsers.settings_to_array(server)
498    local server = false
499
500    for i=1,#list do
501        local host = list[i]
502        local data, status, detail = fetch(protocol .. "://" .. host .. "/" .. instance .. "/tex/status.tma")
503        if status == 200 and type(data) == "string" then
504            local t = loadstring(data)
505            if type(t) == "function" then
506                t = t()
507            end
508            if type(t) == "table" and t.name == "context" and t.version == "lmtx" then
509                server = host
510                break
511            end
512        end
513    end
514
515    if not server then
516        report("provide valid server and instance")
517        return
518    end
519
520    local url = protocol .. "://" .. server .. "/" .. instance .. "/"
521
522    local texmfplatform = "texmf-" .. platform
523
524    report("server   : %s",server)
525    report("instance : %s",instance)
526    report("platform : %s",osplatform)
527    report("system   : %s",ostype)
528
529    local status   = { }
530    local skiplist = {
531        ["mtxrun"]      = true,
532        ["context"]     = true,
533        ["mtxrun.exe"]  = true,
534        ["context.exe"] = true,
535    }
536
537    status[#status+1] = update(url,"texmf",true)
538    status[#status+1] = update(url,"texmf-context",true)
539    status[#status+1] = update(url,texmfplatform,false,skiplist)
540
541    prepare("texmf-cache")
542    prepare("texmf-project")
543    prepare("texmf-fonts")
544    prepare("texmf-local")
545    prepare("texmf-modules")
546
547    local binpath = joinfile(targetroot,"tex",texmfplatform,"bin")
548
549    local luametatex = "luametatex"
550    local luatex     = "luatex"
551    local mtxrun     = "mtxrun"
552    local context    = "context"
553
554    if ostype == "windows" then
555        luametatex = addsuffix(luametatex,"exe")
556        luatex     = addsuffix(luatex,"exe")
557        mtxrun     = addsuffix(mtxrun,"exe")
558        context    = addsuffix(context,"exe")
559    end
560
561    local luametatexbin = joinfile(binpath,luametatex)
562    local luatexbin     = joinfile(binpath,luatex)
563    local mtxrunbin     = joinfile(binpath,mtxrun)
564    local contextbin    = joinfile(binpath,context)
565
566    local cdir = currentdir()
567    local pdir = pushdir(binpath)
568
569    report("current  : %S",cdir or "<unable to check the curent path>")
570    report("target   : %S",pdir or "<unable to change to the binary path>")
571
572    if pdir ~= cdir then
573
574        report("removing : %s",mtxrun)
575        report("removing : %s",context)
576
577        osremove(mtxrun)
578        osremove(context)
579
580        if isfile(luametatex) then
581            lfs.symlink(luametatex,mtxrun)
582            lfs.symlink(luametatex,context)
583        end
584
585        if isfile(mtxrun) then
586            report("linked   : %s",mtxrun)
587        else
588            copyfile(luametatex,mtxrun)
589            report("copied   : %s",mtxrun)
590        end
591        if isfile(context) then
592            report("linked   : %s",context)
593        else
594            copyfile(luametatex,context)
595            report("copied   : %s",context)
596        end
597
598    end
599
600    popdir()
601
602    if lfs.setexecutable(luametatexbin) then
603        report("xbit set : %s",luametatexbin)
604    else
605     -- report("xbit bad : %s",luametatexbin)
606    end
607    if lfs.setexecutable(luatexbin) then
608        report("xbit set : %s",luatexbin)
609    else
610     -- report("xbit bad : %s",luatexbin)
611    end
612    if lfs.setexecutable(mtxrunbin) then
613        report("xbit set : %s",mtxrunbin)
614    else
615     -- report("xbit bad : %s",mtxrunbin)
616    end
617    if lfs.setexecutable(contextbin) then
618        report("xbit set : %s",contextbin)
619    else
620     -- report("xbit bad : %s",contextbin)
621    end
622
623    run("%s --generate",mtxrunbin)
624    if environment.argument("erase") then
625        run("%s --script cache --erase",mtxrunbin)
626        run("%s --generate",mtxrunbin)
627    end
628    run("%s --make en", contextbin)
629
630    -- in case we also install luatex:
631
632    run("%s --luatex --generate",contextbin)
633    run("%s --luatex --make en", contextbin)
634
635    -- in calling script: update mtxrun.exe and mtxrun.lua
636
637    report("")
638    for i=1,#status do
639        report("%-20s : %4i files with %9i bytes installed",unpack(status[i]))
640    end
641    report("")
642    disclaimer()
643    report("")
644
645    report("update, done")
646
647end
648
649function install.modules()
650    report("--modules in not yet implemented")
651end
652
653function install.fonts()
654    report("--fonts in not yet implemented")
655end
656
657function install.goodies()
658    report("--goodies in not yet implemented")
659end
660
661if environment.argument("secure") then
662    usecurl = checkcurl()
663    if usecurl then
664        protocol = "https"
665    else
666        report("no curl installed, quitting")
667        os.exit()
668    end
669end
670
671io.output():setvbuf("no")
672
673if environment.argument("identify") then
674    install.identify()
675elseif environment.argument("install") then
676    install.update()
677elseif environment.argument("update") then
678    install.update()
679elseif environment.argument("modules") then
680    install.modules()
681elseif environment.argument("fonts") then
682    install.fonts()
683elseif environment.argument("goodies") then
684    install.goodies()
685elseif environment.argument("exporthelp") then
686    application.export(environment.argument("exporthelp"),environment.files[1])
687else
688    application.help()
689    report("")
690    disclaimer()
691end
692
693