mtx-install-modules.lua /size: 11 Kb    last modification: 2024-01-16 10:21
1if not modules then modules = { } end modules ['mtx-install-modules'] = {
2    version   = 1.234,
3    comment   = "companion to mtxrun.lua",
4    author    = "Hans Hagen",
5    copyright = "ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- Installing tikz is a bit tricky because there are many packages involved and it's
10-- sort of impossible to derive from the names what to include in the installation.
11-- I tried to use the ctan scrips we ship but there is no way to reliably derive a
12-- set from the topics or packages using the web api (there are also some
13-- inconsistencies between the json and xml interfaces that will not be fixed). A
14-- wildcard pull of everything tikz/pgf is likely to fail or at least gives files we
15-- don't want and/or need, the solution is to be specific.
16--
17-- After that was implemented the script changed name and now also installs the
18-- third party modules.
19--
20-- We use curl and not the built in socket library because all kind of ssl and
21-- redirection can kick in and who know how it evolves.
22--
23-- We use the context unzipper because we cannot be sure if unzip is present on the
24-- system. In many cases windows, linux and osx installations lack it by default.
25--
26-- This script should be run in the tex root where there is also a texmf-context sub
27-- directory; it will quit otherwise. The modules path will be created when absent.
28--
29-- Maybe some day we can get the modules from ctan but then we need a consistent
30-- names and such.
31
32local find = string.find
33
34local helpinfo = [[
35<?xml version="1.0"?>
36<application>
37 <metadata>
38  <entry name="name">mtx-install</entry>
39  <entry name="detail">ConTeXt Installer</entry>
40  <entry name="version">2.01</entry>
41 </metadata>
42 <flags>
43  <category name="basic">
44   <subcategory>
45    <flag name="list"><short>list modules</short></flag>
46    <flag name="installed"><short>list installed modules</short></flag>
47    <flag name="install"><short>install modules</short></flag>
48    <flag name="uninstall"><short>uninstall modules</short></flag>
49    <flag name="module"><short>install (zip) file(s)</short></flag>
50   </subcategory>
51  </category>
52 </flags>
53 <examples>
54  <category>
55   <title>Examples</title>
56   <subcategory>
57    <example><command>mtxrun --script install-modules --list</command></example>
58   </subcategory>
59   <subcategory>
60    <example><command>mtxrun --script install-modules --install filter letter</command></example>
61    <example><command>mtxrun --script install-modules --install tikz</command></example>
62    <example><command>mtxrun --script install-modules --install --all</command></example>
63   </subcategory>
64   <subcategory>
65    <example><command>mtxrun --script install-modules --install   --module t-letter.zip</command></example>
66    <example><command>mtxrun --script install-modules --uninstall --module t-letter.zip</command></example>
67   </subcategory>
68   <subcategory>
69    <example><command>mtxrun --script install-modules --installed</command></example>
70   </subcategory>
71  </category>
72 </examples>
73</application>
74]]
75
76local application = logs.application {
77    name     = "mtx-install-modules",
78    banner   = "ConTeXt Module Installer 1.00",
79    helpinfo = helpinfo,
80}
81
82local report = application.report
83
84scripts         = scripts         or { }
85scripts.modules = scripts.modules or { }
86
87local okay, curl = pcall(require,"libs-imp-curl")
88
89local fetched = curl and curl.fetch and function(str)
90    local data, message = curl.fetch {
91        url            = str,
92        followlocation = true,
93        sslverifyhost  = false,
94        sslverifypeer  = false,
95    }
96    if not data then
97        report("some error: %s",message)
98    end
99    return data
100end or function(str)
101    -- So, no redirect to http, which means that we cannot use the built in socket
102    -- library. What if the client is happy with http?
103    local data = os.resultof("curl -sSL " .. str)
104    return data
105end
106
107-- We use some abstraction:
108
109local urls = {
110    ctan    = "https://mirrors.ctan.org/install",
111    modules = "https://modules.contextgarden.net/dl"
112}
113
114-- Some package this in the root which is asking for conflicts.
115
116-- local badones = {
117--  -- "LICENSE",
118--  -- "README",
119--  -- "README.md",
120--  -- "VERSION",
121-- }
122
123local tmpzipfile = "temp.zip"
124local checkdir   = "texmf-context"
125local targetdir  = "texmf-modules"
126local basefile   = "mtx-install-imp-modules.lua"
127local lists      = false
128
129-- local lists = {
130--     ["tikz"] = {
131--         url   = "ctan",
132--         zips  = { }, -- table of zip files
133--         wipes = { }, -- (nested) table of delete patterns
134--     },
135--     ["filter"] = {
136--         url   = "modules",
137--         zips  = { "t-filter.zip" }
138--     },
139-- }
140
141local function loadlists()
142    if not lists then
143        lists = { }
144        local mainfile = resolvers.findfile(basefile)
145        if mainfile and mainfile ~= "" then
146            local path  = file.pathpart(mainfile)
147            local files = dir.glob((path == "" and "." or path) .. "/mtx-install-imp*.lua")
148            for i=1,#files do
149                local name = files[i]
150                local data = table.load(name)
151                if data then
152                    local entries = data.lists
153                    if entries then
154                        report("loading entries from file %a",name)
155                        for entry, data in table.sortedhash(entries) do
156                            if lists[entry] then
157                                report("entry %a already set from %a",entry,name)
158                            else
159                                lists[entry] = data
160                            end
161                        end
162                    else
163                        report("no entries in file %a",name)
164                    end
165                end
166            end
167        else
168            report("base file %a is not found",basefile)
169        end
170        report()
171    end
172end
173
174local function validate(n)
175    return not (
176           find(n,"latex")
177        or find(n,"lualatex")
178        or find(n,"plain")
179        or find(n,"optex")
180     -- or find(n,"luatex")
181     -- or find(n,"pdftex")
182    )
183end
184
185local function install(list,wipe)
186    if type(list) ~= "table" then
187        report("unknown specification")
188    end
189    local zips  = list.zips
190    local wipes = list.wipes
191    if type(zips) ~= "table" then
192        report("incomplete specification")
193    else
194     -- report("installing into %a",targetdir)
195        for i=1,#zips do
196            local remote = list.url
197            local where  = zips[i]
198            local hash = file.addsuffix(sha2.HASH256(where),"tma")
199            local data = table.load(hash)
200            if data then
201                local name = data.name
202                local list = data.list
203                if name and list then
204                    report()
205                    report("removing %i old files for %a",#list,name)
206                    report()
207                    for i=1,#list do
208                        os.remove(list[i])
209                    end
210                end
211            end
212            if not wipe then
213                if remote then
214                    where = (urls[remote] or remote) .. "/" .. where
215                end
216                local data  = fetched(where)
217                if string.find(data,"^PK") then
218                    io.savedata(tmpzipfile,data)
219                    report("from %a",where)
220                    report("into %a",targetdir)
221                    local done = utilities.zipfiles.unzipdir {
222                        zipname  = tmpzipfile,
223                        path     = ".",
224                        verbose  = "steps",
225                        collect  = true,
226                        validate = validate,
227                    }
228                    table.save(hash,{ name = where, list = done })
229                    os.remove(tmpzipfile)
230                else
231                    report("unknown %a",where)
232                end
233            end
234        end
235
236        local function wiper(wipes)
237            for i=1,#wipes do
238                local s = wipes[i]
239                if type(s) == "table" then
240                    wiper(s)
241                elseif type(s) == "string" then
242                    local t = dir.glob(s)
243                    report("wiping %i files in %a",#t,s)
244                    for i=1,#t do
245-- report("wiping %a",t[i])
246                        os.remove(t[i])
247                    end
248                end
249            end
250        end
251
252        if type(wipes) == "table" then
253            wiper(wipes)
254        end
255    end
256end
257
258function scripts.modules.list()
259    loadlists()
260    for k, v in table.sortedhash(lists) do
261        report("%-20s: %-36s : % t",k,urls[v.url],v.zips)
262    end
263end
264
265function scripts.modules.installed()
266    local files = dir.glob(targetdir .. "/*.tma")
267    if files then
268        for i=1,#files do
269            local data = table.load(files[i])
270            if data then
271                local name = data.name
272                local list = data.list
273                if name and list then
274                    report("%4i : %s",#list,name)
275                end
276            end
277        end
278    end
279end
280
281function scripts.modules.install(wipe)
282    local curdir = dir.current()
283    local done   = false
284    if not lfs.isdir(checkdir) then
285        report("unknown subdirectory %a",checkdir)
286    elseif not dir.mkdirs(targetdir) then
287        report("unable to create %a",targetdir)
288    elseif not lfs.chdir(targetdir) then
289        report("unable to go into %a",targetdir)
290    elseif environment.argument("module") or environment.argument("modules") then
291        local files = environment.files
292        if #files == 0 then
293            report("no archive names provided")
294        else
295            for i=1,#files do
296                local name = files[i]
297                if url.hasscheme(name) then
298                    install({ url = false, zips = { file.addsuffix(name,"zip") } }, wipe)
299                else
300                    loadlists()
301                    install({ url = "modules", zips = { file.addsuffix(name,"zip") } }, wipe)
302                end
303            end
304            done = files
305        end
306    else
307        loadlists()
308        local files = environment.argument("all") and table.sortedkeys(lists) or environment.files
309        if #files == 0 then
310            report("no module names provided")
311        else
312            for i=1,#files do
313                local name = files[i]
314                local list = lists[name]
315                if list then
316                    install(list,wipe)
317                end
318            end
319            done = files
320        end
321    end
322    if done then
323        --
324     -- for i=1,#badones do
325     --     os.remove(badones[i])
326     -- end
327        local okay = false
328        local files = dir.glob("*")
329        for i=1,#files do
330            local name = files[i]
331            if file.suffix(name) == "tma" then
332                -- keep it
333            else
334                if not okay then
335                    report()
336                    okay = true
337                end
338                report("removed %a",name)
339                os.remove(name)
340            end
341        end
342        --
343        report()
344        report("renewing file database")
345        report()
346        resolvers.renewcache()
347        resolvers.load()
348        report()
349        report("installed: % t",done)
350        report()
351    end
352    lfs.chdir(curdir)
353end
354
355function scripts.modules.uninstall()
356    scripts.modules.install(true)
357end
358
359if environment.argument("list") then
360    scripts.modules.list()
361elseif environment.argument("installed") then
362    scripts.modules.installed()
363elseif environment.argument("install") then
364    scripts.modules.install()
365elseif environment.argument("uninstall") then
366    scripts.modules.uninstall()
367elseif environment.argument("exporthelp") then
368    application.export(environment.argument("exporthelp"),environment.files[1])
369else
370    application.help()
371    report("")
372end
373
374