util-lib.lua /size: 17 Kb    last modification: 2021-10-28 13:50
1if not modules then modules = { } end modules ['util-lib'] = {
2    version   = 1.001,
3    comment   = "companion to luat-lib.mkiv",
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-- not used in context any more
10
11--[[
12
13The problem with library bindings is manyfold. They are of course platform
14dependent and while a binary with its directly related libraries are often
15easy to maintain and load, additional libraries can each have their demands.
16
17One important aspect is that loading additional libraries from within the
18loaded one is also operating system dependent. There can be shared libraries
19elsewhere on the system and as there can be multiple libraries with the same
20name but different usage and versioning there can be clashes. So there has to
21be some logic in where to look for these sublibraries.
22
23We found out that for instance on windows libraries are by default sought on
24the parents path and then on the binary paths and these of course can be in
25an out of our control, thereby enlarging the changes on a clash. A rather
26safe solution for that to load the library on the path where it sits.
27
28Another aspect is initialization. When you ask for a library t.e.x it will
29try to initialize luaopen_t_e_x no matter if such an inializer is present.
30However, because loading is configurable and in the case of luatex is already
31partly under out control, this is easy to deal with. We only have to make
32sure that we inform the loader that the library has been loaded so that
33it won't load it twice.
34
35In swiglib we have chosen for a clear organization and although one can use
36variants normally in the tex directory structure predictability is more or
37less the standard. For instance:
38
39.../tex/texmf-mswin/bin/lib/luatex/lua/swiglib/mysql/core.dll
40.../tex/texmf-mswin/bin/lib/luajittex/lua/swiglib/mysql/core.dll
41.../tex/texmf-mswin/bin/lib/luatex/context/lua/swiglib/mysql/core.dll
42.../tex/texmf-mswin/bin/lib/swiglib/lua/mysql/core.dll
43.../tex/texmf-mswin/bin/lib/swiglib/lua/mysql/5.6/core.dll
44
45The lookups are determined via an entry in texmfcnf.lua:
46
47CLUAINPUTS = ".;$SELFAUTOLOC/lib/{$engine,luatex}/lua//",
48
49A request for t.e.x is converted to t/e/x.dll or t/e/x.so depending on the
50platform. Then we use the regular finder to locate the file in the tex
51directory structure. Once located we goto the path where it sits, load the
52file and return to the original path. We register as t.e.x in order to
53prevent reloading and also because the base name is seldom unique.
54
55The main function is a big one and evolved out of experiments that Luigi
56Scarso and I conducted when playing with variants of SwigLib. The function
57locates the library using the context mkiv resolver that operates on the
58tds tree and if that doesn't work out well, the normal clib path is used.
59
60The lookups is somewhat clever in the sense that it can deal with (optional)
61versions and can fall back on non versioned alternatives if needed, either
62or not using a wildcard lookup.
63
64This code is experimental and by providing a special abstract loader (called
65swiglib) we can start using the libraries.
66
67A complication is that we might end up with a luajittex path matching before a
68luatex path due to the path spec. One solution is to first check with the engine
69prefixed. This could be prevented by a more strict lib pattern but that is not
70always under our control. So, we first check for paths with engine in their name
71and then without.
72
73]]--
74
75local type          = type
76local next          = next
77local pcall         = pcall
78local gsub          = string.gsub
79local find          = string.find
80local sort          = table.sort
81local pathpart      = file.pathpart
82local nameonly      = file.nameonly
83local joinfile      = file.join
84local removesuffix  = file.removesuffix
85local addsuffix     = file.addsuffix
86local findfile      = resolvers.findfile
87local findfiles     = resolvers.findfiles
88local expandpaths   = resolvers.expandedpathlistfromvariable
89local qualifiedpath = file.is_qualified_path
90local isfile        = lfs.isfile
91
92local done = false
93
94-- We can check if there are more that one component, and if not, we can
95-- append 'core'.
96
97local function locate(required,version,trace,report,action)
98    if type(required) ~= "string" then
99        report("provide a proper library name")
100        return
101    end
102    if trace then
103        report("requiring library %a with version %a",required,version or "any")
104    end
105    local found_library = nil
106    local required_full = gsub(required,"%.","/") -- package.helpers.lualibfile
107    local required_path = pathpart(required_full)
108    local required_base = nameonly(required_full)
109    if qualifiedpath(required) then
110        -- also check with suffix
111        if isfile(addsuffix(required,os.libsuffix)) then
112            if trace then
113                report("qualified name %a found",required)
114            end
115            found_library = required
116        else
117            if trace then
118                report("qualified name %a not found",required)
119            end
120        end
121    else
122        -- initialize a few variables
123        local required_name = required_base .. "." .. os.libsuffix
124        local version       = type(version) == "string" and version ~= "" and version or false
125--         local engine        = "luatex" -- environment.ownmain or false
126        local engine        = environment.ownmain or false
127        --
128        if trace and not done then
129            local list = expandpaths("lib") -- fresh, no reuse
130            for i=1,#list do
131               report("tds path %i: %s",i,list[i])
132            end
133        end
134        -- helpers
135        local function found(locate,asked_library,how,...)
136            if trace then
137                report("checking %s: %a",how,asked_library)
138            end
139            return locate(asked_library,...)
140        end
141        local function check(locate,...)
142            local found = nil
143            if version then
144                local asked_library = joinfile(required_path,version,required_name)
145                if trace then
146                    report("checking %s: %a","with version",asked_library)
147                end
148                found = locate(asked_library,...)
149            end
150            if not found or found == "" then
151                local asked_library = joinfile(required_path,required_name)
152                if trace then
153                    report("checking %s: %a","with version",asked_library)
154                end
155                found = locate(asked_library,...)
156            end
157            return found and found ~= "" and found or false
158        end
159        -- Alternatively we could first collect the locations and then do the two attempts
160        -- on this list but in practice this is not more efficient as we might have a fast
161        -- match anyway.
162        local function attempt(checkpattern)
163            -- check cnf spec using name and version
164            if trace then
165                report("checking tds lib paths strictly")
166            end
167            local found = findfile and check(findfile,"lib")
168            if found and (not checkpattern or find(found,checkpattern)) then
169                return found
170            end
171            -- check cnf spec using wildcard
172            if trace then
173                report("checking tds lib paths with wildcard")
174            end
175            local asked_library = joinfile(required_path,".*",required_name)
176            if trace then
177                report("checking %s: %a","latest version",asked_library)
178            end
179            local list = findfiles(asked_library,"lib",true)
180            if list and #list > 0 then
181                sort(list)
182                local found = list[#list]
183                if found and (not checkpattern or find(found,checkpattern)) then
184                    return found
185                end
186            end
187            -- Check lib paths using name and version.
188            if trace then
189                report("checking lib paths")
190            end
191            package.extralibpath(environment.ownpath)
192            local paths   = package.libpaths()
193            local pattern = "/[^/]+%." .. os.libsuffix .. "$"
194            for i=1,#paths do
195                required_path = gsub(paths[i],pattern,"")
196                local found = check(lfs.isfound)
197                if type(found) == "string" and (not checkpattern or find(found,checkpattern)) then
198                    return found
199                end
200            end
201            return false
202        end
203        if engine then
204            if trace then
205                report("attemp 1, engine %a",engine)
206            end
207            found_library = attempt("/"..engine.."/")
208            if not found_library then
209                if trace then
210                    report("attemp 2, no engine",asked_library)
211                end
212                found_library = attempt()
213            end
214        else
215            found_library = attempt()
216        end
217    end
218    -- load and initialize when found
219    if not found_library then
220        if trace then
221            report("not found: %a",required)
222        end
223        library = false
224    else
225        if trace then
226            report("found: %a",found_library)
227        end
228        local result, message = action(found_library,required_base)
229        if result then
230            library = result
231        else
232            library = false
233            report("load error: message %a, library %a",tostring(message or "unknown"),found_library or "no library")
234        end
235    end
236    if trace then
237        if not library then
238            report("unknown library: %a",required)
239        else
240            report("stored library: %a",required)
241        end
242    end
243    return library or nil
244end
245
246resolvers.locatelib = locate -- for now
247
248-- swiglib is no longer officially supported
249
250do
251
252    local report_swiglib = logs.reporter("swiglib")
253    local trace_swiglib  = false
254    local savedrequire   = require
255    local loadedlibs     = { }
256    local loadlib        = package.loadlib
257
258    local pushdir = dir.push
259    local popdir  = dir.pop
260
261    trackers.register("resolvers.swiglib", function(v) trace_swiglib = v end)
262
263    function requireswiglib(required,version)
264        local library = loadedlibs[library]
265        if library == nil then
266            local trace_swiglib = trace_swiglib or package.helpers.trace
267            library = locate(required,version,trace_swiglib,report_swiglib,function(name,base)
268                pushdir(pathpart(name))
269                local opener = "luaopen_" .. base
270                if trace_swiglib then
271                    report_swiglib("opening: %a with %a",name,opener)
272                end
273                local library, message = loadlib(name,opener)
274                local libtype = type(library)
275                if libtype == "function" then
276                    library = library()
277                else
278                    report_swiglib("load error: %a returns %a, message %a, library %a",opener,libtype,(string.gsub(message or "no message","[%s]+$","")),found_library or "no library")
279                    library = false
280                end
281                popdir()
282                return library
283            end)
284            loadedlibs[required] = library or false
285        end
286        return library
287    end
288
289--[[
290
291For convenience we make the require loader function swiglib aware. Alternatively
292we could put the specific loader in the global namespace.
293
294]]--
295
296    function require(name,version)
297        if find(name,"^swiglib%.") then
298            return requireswiglib(name,version)
299        else
300            return savedrequire(name)
301        end
302    end
303
304--[[
305
306At the cost of some overhead we provide a specific loader so that we can keep
307track of swiglib usage which is handy for development. In context this is the
308recommended loader.
309
310]]--
311
312    local swiglibs    = { }
313    local initializer = "core"
314
315    function swiglib(name,version)
316        local library = swiglibs[name]
317        if not library then
318            statistics.starttiming(swiglibs)
319            if trace_swiglib then
320                report_swiglib("loading %a",name)
321            end
322            if not find(name,"%." .. initializer .. "$") then
323                fullname = "swiglib." .. name .. "." .. initializer
324            else
325                fullname = "swiglib." .. name
326            end
327            library = requireswiglib(fullname,version)
328            swiglibs[name] = library
329            statistics.stoptiming(swiglibs)
330        end
331        return library
332    end
333
334    statistics.register("used swiglibs", function()
335        if next(swiglibs) then
336            return string.format("%s, initial load time %s seconds",table.concat(table.sortedkeys(swiglibs)," "),statistics.elapsedtime(swiglibs))
337        end
338    end)
339
340end
341
342if FFISUPPORTED and ffi and ffi.load then
343
344--[[
345
346We use the same lookup logic for ffi loading.
347
348]]--
349
350    local report_ffilib = logs.reporter("ffilib")
351    local trace_ffilib  = false
352    local savedffiload  = ffi.load
353
354 -- local pushlibpath = package.pushlibpath
355 -- local poplibpath  = package.poplibpath
356
357 -- ffi.savedload = savedffiload
358
359    trackers.register("resolvers.ffilib", function(v) trace_ffilib = v end)
360
361 -- pushlibpath(pathpart(name))
362 -- local state, library = pcall(savedffiload,nameonly(name))
363 -- poplibpath()
364
365    local loaded = { }
366
367    local function locateindeed(name)
368        name = removesuffix(name)
369        local l = loaded[name]
370        if l == nil then
371            local state, library = pcall(savedffiload,name)
372            if type(library) == "userdata" then
373                l = library
374            elseif type(state) == "userdata" then
375                l = state
376            else
377                l = false
378            end
379            loaded[name] = l
380        elseif trace_ffilib then
381            report_ffilib("reusing already loaded %a",name)
382        end
383        return l
384    end
385
386    local function getlist(required)
387        local list = directives.value("system.librarynames" )
388        if type(list) == "table" then
389            list = list[required]
390            if type(list) == "table" then
391                if trace then
392                    report("using lookup list for library %a: % | t",required,list)
393                end
394                return list
395            end
396        end
397        return { required }
398    end
399
400    function ffilib(name,version)
401        name = removesuffix(name)
402        local l = loaded[name]
403        if l ~= nil then
404            if trace_ffilib then
405                report_ffilib("reusing already loaded %a",name)
406            end
407            return l
408        end
409        local list = getlist(name)
410        if version == "system" then
411            for i=1,#list do
412                local library = locateindeed(list[i])
413                if type(library) == "userdata" then
414                    return library
415                end
416            end
417        else
418            for i=1,#list do
419                local library = locate(list[i],version,trace_ffilib,report_ffilib,locateindeed)
420                if type(library) == "userdata" then
421                    return library
422                end
423            end
424        end
425    end
426
427    function ffi.load(name)
428        local list = getlist(name)
429        for i=1,#list do
430            local library = ffilib(list[i])
431            if type(library) == "userdata" then
432                return library
433            end
434        end
435        if trace_ffilib then
436            report_ffilib("trying to load %a using normal loader",name)
437        end
438        -- so here we don't store
439        for i=1,#list do
440            local state, library = pcall(savedffiload,list[i])
441            if type(library) == "userdata" then
442                return library
443            elseif type(state) == "userdata" then
444                return library
445            end
446        end
447    end
448
449end
450
451--[[
452
453-- So, we now have:
454
455trackers.enable("resolvers.ffilib")
456trackers.enable("resolvers.swiglib")
457
458local gm = require("swiglib.graphicsmagick.core")
459local gm = swiglib("graphicsmagick.core")
460local sq = swiglib("mysql.core")
461local sq = swiglib("mysql.core","5.6")
462
463ffilib("libmysql","5.6.14")
464
465-- Watch out, the last one is less explicit and lacks the swiglib prefix.
466
467]]--
468
469do
470
471    local isfile = lfs.isfile
472    local report = logs.reporter("resolvers","lib")
473    local trace  = false
474
475    trackers.register("resolvers.lib", function(v) trace = v end)
476
477    local function action(filename)
478        return isfile(filename) and filename or false
479    end
480
481    function resolvers.findlib(required) -- todo: cache
482        local list = directives.value("system.librarynames" )
483        local only = nameonly(required)
484        if type(list) == "table" then
485            list = list[only]
486            if type(list) == "table" then
487                if trace then
488                    report("using lookup list for library %a: % | t",only,list)
489                end
490            else
491                list = { only }
492            end
493        else
494            list = { only }
495        end
496        for i=1,#list do
497            local name  = list[i]
498            local found = locate(name,false,trace,report,action)
499            if found then
500                return found
501            end
502        end
503        local getpaths = resolvers.expandedpathlistfromvariable
504        if getpaths then
505            local list = getpaths("PATH")
506            local base = addsuffix(only,os.libsuffix)
507            for i=1,#list do
508                local full  = joinfile(list[i],base)
509                local found = locate(full,false,trace,report,action)
510                if found then
511                    return found
512                end
513            end
514        end
515    end
516
517end
518