l-os.lua /size: 20 Kb    last modification: 2024-01-16 09:02
1if not modules then modules = { } end modules ['l-os'] = {
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-- This file deals with some operating system issues. Please don't bother me
10-- with the pros and cons of operating systems as they all have their flaws
11-- and benefits. Bashing one of them won't help solving problems and fixing
12-- bugs faster and is a waste of time and energy.
13--
14-- path separators: / or \ ... we can use / everywhere
15-- suffixes       : dll so exe <none> ... no big deal
16-- quotes         : we can use "" in most cases
17-- expansion      : unless "" are used * might give side effects
18-- piping/threads : somewhat different for each os
19-- locations      : specific user file locations and settings can change over time
20--
21-- os.type     : windows | unix (new, we already guessed os.platform)
22-- os.name     : windows | msdos | linux | macosx | solaris | .. | generic (new)
23-- os.platform : extended os.name with architecture
24
25-- os.sleep() => socket.sleep()
26-- math.randomseed(tonumber(string.sub(string.reverse(tostring(math.floor(socket.gettime()*10000))),1,6)))
27
28local os = os
29local date, time, difftime = os.date, os.time, os.difftime
30local find, format, gsub, upper, gmatch = string.find, string.format, string.gsub, string.upper, string.gmatch
31local concat = table.concat
32local random, ceil, randomseed, modf = math.random, math.ceil, math.randomseed, math.modf
33local type, setmetatable, tonumber, tostring = type, setmetatable, tonumber, tostring
34
35-- This check needs to happen real early on. Todo: we can pick it up from the commandline
36-- if we pass --binpath= (which is useful anyway)
37
38do
39
40    local selfdir = os.selfdir
41
42    if selfdir == "" then
43        selfdir = nil
44    end
45
46    if not selfdir then
47
48        -- We need a fallback plan so let's see what we get.
49
50        if arg then
51            -- passed by mtx-context ... saves network access
52            for i=1,#arg do
53                local a = arg[i]
54                if find(a,"^%-%-[c:]*texmfbinpath=") then
55                    selfdir = gsub(a,"^.-=","")
56                    break
57                end
58            end
59        end
60
61        if not selfdir then
62            selfdir = os.selfbin or "luatex"
63            if find(selfdir,"[/\\]") then
64                selfdir = gsub(selfdir,"[/\\][^/\\]*$","")
65            elseif os.getenv then
66                local path = os.getenv("PATH")
67                local name = gsub(selfdir,"^.*[/\\][^/\\]","")
68                local patt = "[^:]+"
69                if os.type == "windows" then
70                    patt = "[^;]+"
71                    name = name .. ".exe"
72                end
73                local isfile
74                if lfs then
75                    -- we're okay as lfs is assumed present
76                    local attributes = lfs.attributes
77                    isfile = function(name)
78                        local a = attributes(name,"mode")
79                        return a == "file" or a == "link" or nil
80                    end
81                else
82                    -- we're not okay and much will not work as we miss lfs
83                    local open = io.open
84                    isfile = function(name)
85                        local f = open(name)
86                        if f then
87                            f:close()
88                            return true
89                        end
90                    end
91                end
92                for p in gmatch(path,patt) do
93                    -- possible speedup: there must be tex in 'p'
94                    if isfile(p .. "/" .. name) then
95                        selfdir = p
96                        break
97                    end
98                end
99            end
100        end
101
102        -- let's hope we're okay now
103
104        os.selfdir = selfdir or "."
105
106    end
107
108    -- print(os.selfdir) os.exit()
109
110end
111
112-- The following code permits traversing the environment table, at least in luatex. Internally all
113-- environment names are uppercase.
114
115-- The randomseed in Lua is not that random, although this depends on the operating system as well
116-- as the binary (Luatex is normally okay). But to be sure we set the seed anyway. It will be better
117-- in Lua 5.4 (according to the announcements.)
118
119math.initialseed = tonumber(string.sub(string.reverse(tostring(ceil(socket and socket.gettime()*10000 or time()))),1,6))
120
121randomseed(math.initialseed)
122
123if not os.__getenv__ then
124
125    os.__getenv__ = os.getenv
126    os.__setenv__ = os.setenv
127
128    if os.env then
129
130        local osgetenv  = os.getenv
131        local ossetenv  = os.setenv
132        local osenv     = os.env      local _ = osenv.PATH -- initialize the table
133
134        function os.setenv(k,v)
135            if v == nil then
136                v = ""
137            end
138            local K = upper(k)
139            osenv[K] = v
140            if type(v) == "table" then
141                v = concat(v,";") -- path
142            end
143            ossetenv(K,v)
144        end
145
146        function os.getenv(k)
147            local K = upper(k)
148            local v = osenv[K] or osenv[k] or osgetenv(K) or osgetenv(k)
149            if v == "" then
150                return nil
151            else
152                return v
153            end
154        end
155
156    else
157
158        local ossetenv  = os.setenv
159        local osgetenv  = os.getenv
160        local osenv     = { }
161
162        function os.setenv(k,v)
163            if v == nil then
164                v = ""
165            end
166            local K = upper(k)
167            osenv[K] = v
168        end
169
170        function os.getenv(k)
171            local K = upper(k) -- hm utf
172            local v = osenv[K] or osgetenv(K) or osgetenv(k)
173            if v == "" then
174                return nil
175            else
176                return v
177            end
178        end
179
180        local function __index(t,k)
181            return os.getenv(k)
182        end
183        local function __newindex(t,k,v)
184            os.setenv(k,v)
185        end
186
187        os.env = { }
188
189        setmetatable(os.env, { __index = __index, __newindex = __newindex } )
190
191    end
192
193end
194
195-- end of environment hack
196
197if not io.fileseparator then
198
199    if find(os.getenv("PATH"),";",1,true) then
200        io.fileseparator, io.pathseparator, os.type = "\\", ";", os.type or "windows"
201    else
202        io.fileseparator, io.pathseparator, os.type = "/" , ":", os.type or "unix"
203    end
204
205end
206
207os.type = os.type or (io.pathseparator == ";"       and "windows") or "unix"
208os.name = os.name or (os.type          == "windows" and "mswin"  ) or "linux"
209
210if os.type == "windows" then
211    os.libsuffix, os.binsuffix, os.binsuffixes = 'dll', 'exe', { 'exe', 'cmd', 'bat' }
212elseif os.name == "macosx" then
213    os.libsuffix, os.binsuffix, os.binsuffixes = 'dylib', '', { '' }
214else
215    os.libsuffix, os.binsuffix, os.binsuffixes = 'so', '', { '' }
216end
217
218do
219
220    local execute = os.execute
221    local iopopen = io.popen
222    local ostype  = os.type
223
224    local function resultof(command)
225        -- already has flush, b is new and we need it to pipe xz output
226        local handle = iopopen(command,ostype == "windows" and "rb" or "r")
227        if handle then
228            local result = handle:read("*all") or ""
229            handle:close()
230            return result
231        else
232            return ""
233        end
234    end
235
236    os.resultof = resultof
237
238    function os.pipeto(command)
239        return iopopen(command,"w") -- already has flush
240    end
241
242    local launchers = {
243        windows = "start %s",
244        macosx  = "open %s",
245        unix    = "xdg-open %s &> /dev/null &",
246    }
247
248    function os.launch(str)
249        local command = format(launchers[os.name] or launchers.unix,str)
250        -- todo: pcall
251    --     print(command)
252        execute(command)
253    end
254
255end
256
257do
258
259    local gettimeofday = os.gettimeofday or os.clock
260    os.gettimeofday    = gettimeofday
261
262    local startuptime = gettimeofday()
263
264    function os.runtime()
265        return gettimeofday() - startuptime
266    end
267
268    -- print(os.gettimeofday()-os.time())
269    -- os.sleep(1.234)
270    -- print (">>",os.runtime())
271    -- print(os.date("%H:%M:%S",os.gettimeofday()))
272    -- print(os.date("%H:%M:%S",os.time()))
273
274end
275
276-- We can use HOSTTYPE on some platforms (but not consistently on e.g. Linux).
277--
278-- os.bits = 32 | 64
279--
280-- os.uname() : return {
281--     machine  = "x86_64",
282--     nodename = "MYLAPTOP",
283--     release  = "build 9200",
284--     sysname  = "Windows",
285--     version  = "6.02",
286-- }
287
288do
289
290    local name         = os.name or "linux"
291    local platform     = os.getenv("MTX_PLATFORM") or ""
292    local architecture = os.uname and os.uname().machine -- lmtx
293    local bits         = os.getenv("MTX_BITS") or find(platform,"64") and 64 or 32
294
295    if platform ~= "" then
296
297        -- we're okay already
298
299    elseif os.type == "windows" then
300
301        -- PROCESSOR_ARCHITECTURE : binary platform
302        -- PROCESSOR_ARCHITEW6432 : OS platform
303
304        architecture = string.lower(architecture or os.getenv("PROCESSOR_ARCHITECTURE") or "")
305        if architecture == "x86_64" then
306            bits, platform = 64, "win64"
307        elseif find(architecture,"amd64") then
308            bits, platform = 64, "win64"
309        elseif find(architecture,"arm64") then
310            bits, platform = 64, "windows-arm64"
311        elseif find(architecture,"arm32") then
312            bits, platform = 32, "windows-arm32"
313        else
314            bits, platform = 32, "mswin"
315        end
316
317    elseif name == "linux" then
318
319        -- There is no way to detect if musl is used because there is no __MUSL__
320        -- and it looks like there never will be. Folks don't care about cases where
321        -- one ships multipe binaries (as with TeX distibutions) and want to select
322        -- the right one. So probably it expects users to compile locally in which
323        -- case we don't care to much as they can then sort it out.
324
325        architecture = architecture or os.getenv("HOSTTYPE") or resultof("uname -m") or ""
326        local musl = find(os.selfdir or "","linuxmusl")
327        if find(architecture,"x86_64") then
328            bits, platform = 64, musl and "linuxmusl" or "linux-64"
329        elseif find(architecture,"ppc") then
330            bits, platform = 32, "linux-ppc" -- this will be dropped
331        else
332            bits, platform = 32, musl and "linuxmusl" or "linux"
333        end
334
335    elseif name == "macosx" then
336
337        -- Identifying the architecture of OSX is quite a mess and this is the best
338        -- we can come up with. For some reason $HOSTTYPE is a kind of pseudo
339        -- environment variable, not known to the current environment. And yes,
340        -- uname cannot be trusted either, so there is a change that you end up with
341        -- a 32 bit run on a 64 bit system. Also, some proper 64 bit intel macs are
342        -- too cheap (low-end) and therefore not permitted to run the 64 bit kernel.
343
344        architecture = architecture or resultof("echo $HOSTTYPE") or ""
345        if architecture == "" then
346            bits, platform = 64, "osx-intel"
347        elseif find(architecture,"i386") then
348            bits, platform = 64, "osx-intel"
349        elseif find(architecture,"x86_64") then
350            bits, platform = 64, "osx-64"
351        elseif find(architecture,"arm64") then
352            bits, platform = 64, "osx-arm"
353        else
354            bits, platform = 32, "osx-ppc"
355        end
356
357    elseif name == "sunos" then
358
359        architecture = architecture or resultof("uname -m") or ""
360        if find(architecture,"sparc") then
361            bits, platform = 32, "solaris-sparc"
362        else -- if architecture == 'i86pc'
363            bits, platform = 32, "solaris-intel"
364        end
365
366    elseif name == "freebsd" then
367
368        architecture = architecture or os.getenv("MACHTYPE") or resultof("uname -m") or ""
369        if find(architecture,"amd64") or find(architecture,"AMD64") then
370            bits, platform = 64, "freebsd-amd64"
371        else
372            bits, platform = 32, "freebsd"
373        end
374
375    elseif name == "kfreebsd" then
376
377        architecture = architecture or os.getenv("HOSTTYPE") or resultof("uname -m") or ""
378        if architecture == "x86_64" then
379            bits, platform = 64, "kfreebsd-amd64"
380        else
381            bits, platform = 32, "kfreebsd-i386"
382        end
383
384    else
385
386        architecture = architecture or resultof("uname -m") or ""
387
388        if find(architecture,"aarch64") then
389            bits, platform = "linux-aarch64"
390        elseif find(architecture,"armv7l") then
391            -- linux-armel
392            bits, platform = 32, "linux-armhf"
393        elseif find(architecture,"mips64") or find(architecture,"mips64el") then
394            bits, platform = 64, "linux-mipsel"
395        elseif find(architecture,"mipsel") or find(architecture,"mips") then
396            bits, platform = 32, "linux-mipsel"
397        else
398            bits, platform = 64, "linux-64" -- was 32, "linux"
399        end
400
401    end
402
403    os.setenv("MTX_PLATFORM",platform)
404    os.setenv("MTX_BITS",    bits)
405
406    os.platform = platform
407    os.bits     = bits
408    os.newline  = name == "windows" and "\013\010" or "\010" -- crlf or lf
409
410end
411
412-- beware, we set the randomseed
413
414-- From wikipedia: Version 4 UUIDs use a scheme relying only on random numbers. This
415-- algorithm sets the version number as well as two reserved bits. All other bits
416-- are set using a random or pseudorandom data source. Version 4 UUIDs have the form
417-- xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx with hexadecimal digits x and hexadecimal
418-- digits 8, 9, A, or B for y. e.g. f47ac10b-58cc-4372-a567-0e02b2c3d479. As we don't
419-- call this function too often there is not so much risk on repetition.
420
421do
422
423    local t = { 8, 9, "a", "b" }
424
425    function os.uuid()
426        return format("%04x%04x-4%03x-%s%03x-%04x-%04x%04x%04x",
427            random(0xFFFF),random(0xFFFF),
428            random(0x0FFF),
429            t[ceil(random(4))] or 8,random(0x0FFF),
430            random(0xFFFF),
431            random(0xFFFF),random(0xFFFF),random(0xFFFF)
432        )
433    end
434
435end
436
437do
438
439    -- this is fragile because it depends on time and so we only check once during
440    -- a run (the computer doesn't move zones) .. Michal Vlasák made a better one
441
442 -- local d
443 --
444 -- function os.timezone()
445 --     d = d or ((tonumber(date("%H")) or 0) - (tonumber(date("!%H")) or 0))
446 --     if d > 0 then
447 --         return format("+%02i:00",d)
448 --     else
449 --         return format("-%02i:00",-d)
450 --     end
451 -- end
452
453    local hour, min
454
455    function os.timezone(difference)
456        if not hour then
457            -- somehow looks too complex:
458            local current   = time()
459            local utcdate   = date("!*t", current)
460            local localdate = date("*t", current)
461            localdate.isdst = false
462            local timediff  = difftime(time(localdate), time(utcdate))
463            hour, min = modf(timediff / 3600)
464            min = min * 60
465        end
466        if difference then
467            return hour, min
468        else
469            return format("%+03d:%02d",hour,min) -- %+ means: always show sign
470        end
471    end
472
473    -- localtime with timezone: 2021-10-22 10:22:54+02:00
474
475    local timeformat = format("%%s%s",os.timezone())
476    local dateformat = "%Y-%m-%d %H:%M:%S"
477    local lasttime   = nil
478    local lastdate   = nil
479
480    function os.fulltime(t,default)
481        t = t and tonumber(t) or 0
482        if t > 0 then
483            -- valid time
484        elseif default then
485            return default
486        else
487            t = time()
488        end
489        if t ~= lasttime then
490            lasttime = t
491            lastdate = format(timeformat,date(dateformat))
492        end
493        return lastdate
494    end
495
496    -- localtime without timezone: 2021-10-22 10:22:54
497
498    local dateformat = "%Y-%m-%d %H:%M:%S"
499    local lasttime   = nil
500    local lastdate   = nil
501
502    function os.localtime(t,default)
503        t = t and tonumber(t) or 0
504        if t > 0 then
505            -- valid time
506        elseif default then
507            return default
508        else
509            t = time()
510        end
511        if t ~= lasttime then
512            lasttime = t
513            lastdate = date(dateformat,t)
514        end
515        return lastdate
516    end
517
518    function os.converttime(t,default)
519        local t = tonumber(t)
520        if t and t > 0 then
521            return date(dateformat,t)
522        else
523            return default or "-"
524        end
525    end
526
527    -- table with values
528
529    function os.today()
530        return date("!*t")
531    end
532
533    -- utc time without timezone: 2021-10-22 08:22:54
534
535    function os.now()
536        return date("!%Y-%m-%d %H:%M:%S")
537    end
538
539end
540
541do
542
543    local cache = { }
544
545    local function which(filename)
546        local fullname = cache[filename]
547        if fullname == nil then
548            local suffix = file.suffix(filename)
549            local suffixes = suffix == "" and os.binsuffixes or { suffix }
550            for directory in gmatch(os.getenv("PATH"),"[^" .. io.pathseparator .."]+") do
551                local df = file.join(directory,filename)
552                for i=1,#suffixes do
553                    local dfs = file.addsuffix(df,suffixes[i])
554                    if io.exists(dfs) then
555                        fullname = dfs
556                        break
557                    end
558                end
559            end
560            if not fullname then
561                fullname = false
562            end
563            cache[filename] = fullname
564        end
565        return fullname
566    end
567
568    os.which = which
569    os.where = which
570
571    -- print(os.which("inkscape.exe"))
572    -- print(os.which("inkscape"))
573    -- print(os.which("gs.exe"))
574    -- print(os.which("ps2pdf"))
575
576end
577
578if not os.sleep then
579
580    local socket = socket
581
582    function os.sleep(n)
583        if not socket then
584            -- so we delay ... if os.sleep is really needed then one should also
585            -- be sure that socket can be found
586            socket = require("socket")
587        end
588        socket.sleep(n)
589    end
590
591end
592
593-- These are moved from core-con.lua (as I needed them elsewhere).
594
595do
596
597    local function isleapyear(year) -- timed for bram's cs practicum
598     -- return (year % 400 == 0) or (year % 100 ~= 0 and year % 4 == 0) -- 3:4:1600:1900 = 9.9 : 8.2 : 5.0 :  6.8 (29.9)
599        return (year % 4 == 0) and (year % 100 ~= 0 or year % 400 == 0) -- 3:4:1600:1900 = 5.1 : 6.5 : 8.1 : 10.2 (29.9)
600     -- return (year % 4 == 0) and (year % 400 == 0 or year % 100 ~= 0) -- 3:4:1600:1900 = 5.2 : 8.5 : 6.8 : 10.1 (30.6)
601    end
602
603    os.isleapyear = isleapyear
604
605    -- nicer:
606    --
607    -- local days = {
608    --     [false] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 },
609    --     [true]  = { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
610    -- }
611    --
612    -- local function nofdays(year,month)
613    --     return days[isleapyear(year)][month]
614    --     return month == 2 and isleapyear(year) and 29 or days[month]
615    -- end
616    --
617    -- more efficient:
618
619    local days = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
620
621    local function nofdays(year,month,day)
622        if not month then
623            return isleapyear(year) and 365 or 364
624        elseif not day then
625            return month == 2 and isleapyear(year) and 29 or days[month]
626        else
627            for i=1,month-1 do
628                day = day + days[i]
629            end
630            if month > 2 and isleapyear(year) then
631                day = day + 1
632            end
633            return day
634        end
635    end
636
637    os.nofdays = nofdays
638
639    function os.weekday(day,month,year)
640        return date("%w",time { year = year, month = month, day = day }) + 1
641    end
642
643    function os.validdate(year,month,day)
644        -- we assume that all three values are set
645        -- year is always ok, even if lua has a 1970 time limit
646        if month < 1 then
647            month = 1
648        elseif month > 12 then
649            month = 12
650        end
651        if day < 1 then
652            day = 1
653        else
654            local max = nofdays(year,month)
655            if day > max then
656                day = max
657            end
658        end
659        return year, month, day
660    end
661
662    function os.date(fmt,...)
663        if not fmt then
664            -- otherwise differences between unix, mingw and msvc
665            fmt = "%Y-%m-%d %H:%M"
666        end
667        return date(fmt,...)
668    end
669
670end
671
672do
673
674    local osexit   = os.exit
675    local exitcode = nil
676
677    function os.setexitcode(code)
678        exitcode = code
679    end
680
681    function os.exit(c)
682        if exitcode ~= nil then
683            return osexit(exitcode)
684        end
685        if c ~= nil then
686            return osexit(c)
687        end
688        return osexit()
689    end
690
691end
692