summary refs log blame commit diff stats
path: root/src/cratera/bootstrap.lua
blob: 88cef17c1aa78a1102ca84ced89f3de23a66383b (plain) (tree)

































































































































































































































































                                                                                                 
--[[
    Cratera Bootstrap
    Copyright (C) 2024  Soni L.

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
--]]

--[[
    Parts of this file are based on PUC-Rio Lua, available under the following
    license:

    * Copyright (C) 1994-2018 Lua.org, PUC-Rio.
    *
    * Permission is hereby granted, free of charge, to any person obtaining
    * a copy of this software and associated documentation files (the
    * "Software"), to deal in the Software without restriction, including
    * without limitation the rights to use, copy, modify, merge, publish,
    * distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so, subject to
    * the following conditions:
    *
    * The above copyright notice and this permission notice shall be
    * included in all copies or substantial portions of the Software.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
    * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
    * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
    * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--]]

-- The Cratera Bootstrap library sets up Cratera "proper", not just the
-- compiler. However, it also expects a vanilla(-compatible) Lua environment.
-- In particular, it uses the io library to access the filesystem.
-- It also expects _G to be the default environment.

-- NOTE: file:read() has the a-bit-annoying property of resetting EOF and other
-- errors every time it's called, so we must pass (n, 0) if we want a reliable
-- EOF indicator (which we do).

local cratera = require "cratera"

local cratera_env = setmetatable({lua=_G}, {__index=_G})
cratera_env._G = cratera_env
-- cratera.lib actually injects itself into *us*!
package.loaded[...] = cratera_env

local io_open = io.open
local stdin = io.stdin
-- only use setfenv if load doesn't support env argument
local setfenv = (loadstring or load)("return setfenv", nil, nil, {})()
-- we don't know the real BUFSIZ, use a common memory page size (16K, as on M1)
local BUFSIZ = 16384

local cratera_loader_base = cratera.load
-- it is convenient to set cratera_env.load
-- it will be replaced with the proper function in cratera.lib
cratera_env.load = cratera_loader_base

-- duplicated with init.lua
local LUA_SIGNATURE
if string.dump then
    LUA_SIGNATURE = string.dump(function() end):sub(1,1)
else
    LUA_SIGNATURE = string.char(27)
end

-- based on lauxlib.c, PUC Rio
local function skip_interpreter_line(file)
    -- lua skips UTF-8 BOM, we do not.
    -- join us, reject the BOM.
    local initial_buf = ""
    -- see NOTE at the start of this file
    local cdata, err_or_more_data = file:read(1, 0)
    if cdata == "#" and err_or_more_data then
        -- skip interpreter line
        repeat
            cdata, err_or_more_data = file:read(1, 0)
        until cdata == "\n" or not cdata or not err_or_more_data
        -- align line numbers so error messages are correct!
        initial_buf = initial_buf .. "\n"
        if cdata and err_or_more_data then
            cdata, err_or_more_data = file:read(1, 0)
        end
    end
    return initial_buf, cdata, err_or_more_data
end

local function cratera_loadfile(filename, mode, env)
    if env == nil then
        -- load default env - if it doesn't exist, load lua env
        env = cratera_env
    end
    local file, err
    if filename ~= nil then
        file, err = io_open(filename, "r")
        if not file then
            return nil, ("cannot open %s: %s"):format(filename, err)
        end
    else
        file = stdin
    end
    local initial_buf, cdata, err_or_more_data = skip_interpreter_line(file)
    if cdata == LUA_SIGNATURE and err_or_more_data and filename then
        -- binary file
        file:close()
        file, err = io_open(filename, "rb")
        if not file then
            return nil, ("cannot reopen %s: %s"):format(filename, err)
        end
        initial_buf, cdata, err_or_more_data = skip_interpreter_line(file)
    end
    if cdata then
        initial_buf = initial_buf .. cdata
    end
    -- must declare and set separately
    -- this is a bit involved. importantly: the chunk (passed to load()) always
    -- tail calls into the "reader", however we redefine "reader" based on
    -- various condtions (on first call, on eof, on error)
    local reader
    reader = function()
        reader = function()
            local buf
            buf, err_or_more_data = file:read(BUFSIZ, 0)
            if not buf or not err_or_more_data then
                reader = function() end
            end
            if buf == "" then return reader() end
            return buf
        end
        if not err_or_more_data then
            reader = function() end
        end
        if initial_buf == "" then return reader() end
        return initial_buf
    end
    local chunkname
    if not filename then
        chunkname = "=stdin"
    else
        chunkname = "@" .. filename
    end
    local func, loader_err = cratera_loader_base(function()
        return reader()
    end, filename, mode, env)
    local closeok, closeerr = file:close()
    -- at this point, err_or_more_data *should* be nil.
    if err_or_more_data then
        -- unless it's an error
        return nil, ("cannot read %s: %s"):format(filename, err_or_more_data)
    end
    -- lua doesn't quite check this properly but it's possible for an error to
    -- be delayed until close
    if not closeok and closeerr then
        return nil, ("cannot read %s: %s"):format(filename, closeerr)
    end
    if setfenv and func and env then
        setfenv(func, env)
    end
    -- arity adjustments
    if func then
        return func
    else
        return func, loader_err
    end
end

cratera_env.loadfile = cratera_loadfile

local searchers = package.searchers or package.loaders
local dirsep, pathsep, pathmark = package.config:match("^([^\n]+)\n([^\n]+)\n([^\n]+)")
local CRATERA_SEP = ".cratera.d" .. dirsep
local tconcat = table.concat
do
    -- discover the cratera path
    local default_path = require "cratera.prebuilt.path"
    local cratera_path_env = os.getenv("CRATERA_PATH")

    if cratera_path_env then
        local default_mark = cratera_path_env:find(pathsep..pathsep, 1, true)
        if not default_mark then
            cratera_env.craterapath = cratera_path_env
        else
            local left = cratera_path_env:sub(1, default_mark - 1)
            local right = cratera_path_env:sub(default_mark + 2)
            if #left > 0 and #right > 0 then
                cratera_env.craterapath = left .. ";" .. default_path .. ";" .. right
            elseif #left > 0 then
                cratera_env.craterapath = left .. ";" .. default_path
            elseif #right > 0 then
                cratera_env.craterapath = default_path .. ";" .. right
            else
                cratera_env.craterapath = default_path
            end
        end
    else
        cratera_env.craterapath = default_path
    end
end

local error = error
local type = type
local assert = assert

local psearchpath = package.searchpath or (function()
    assert(#pathsep == 1, "unsupported lua configuration")
    assert(#pathmark == 1, "unsupported lua configuration")
    local pathfinder = "[^"..pathsep.."]*"
    return function(name, path, sep, rep)
        assert(sep == ".")
        -- table trick is safe to use with "untrusted" input (e.g. dirsep)
        local filename = name:gsub("%.", {["."]=rep})
        -- this is a bit inefficient but it's the best we can do (safely)...
        local filepaths = path:gsub(".", {[pathmark]=filename})
        -- this is safe given the previous assertions
        for filepath in filepaths:gmatch(pathfinder) do
            local file = io_open(filepath)
            if file then
                file:close()
                return filepath
            end
        end
        return nil, "no file '" .. filepaths:gsub(".", {[pathsep]="\n\tno file'"}) .. "'"
    end
end)()

-- put cratera searcher first, before lua searcher
table.insert(searchers, 2, function(modname)
    if type(cratera_env.craterapath) ~= "string" then
        error("craterapath must be a string")
    end
    local filename, err = psearchpath(modname, cratera_env.craterapath, ".", CRATERA_SEP)
    if not filename then
        return err
    end
    local mod, err = cratera_loadfile(filename)
    if mod then
        return mod, filename
    else
        error(("error loading module '%s' from file '%s':\n\t%s"):format(modname, filename, err))
    end
end)

-- load the cratera lib.
require "cratera.lib"