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