--[[
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_env._VERSION = cratera._VERSION
-- 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, chunkname, 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"