Move the package library into a separate module

Hopefully this makes it a little easier for people to use in custom
shells (and anything else where it might be useful).
This commit is contained in:
SquidDev 2020-05-14 17:27:50 +01:00
parent a4ae36b6b3
commit 086fccd997
3 changed files with 205 additions and 88 deletions

View File

@ -0,0 +1,121 @@
--- This provides a pure Lua implementation of the builtin @{require} function
-- and @{package} library.
--
-- Generally you do not need to use this module - it is injected into the
-- every program's environment. However, it may be useful when building a
-- custom shell or when running programs yourself.
--
-- @module cc.require
-- @usage Construct the package and require function, and insert them into a
-- custom environment.
--
-- local env = setmetatable({}, { __index = _ENV })
-- local r = require "cc.require"
-- env.require, env.package = r.make(env, "/")
local expect = require and require("cc.expect") or dofile("rom/modules/main/cc/expect.lua")
local expect = expect.expect
local function preload(package)
return function(name)
if package.preload[name] then
return package.preload[name]
else
return nil, "no field package.preload['" .. name .. "']"
end
end
end
local function from_file(package, env, dir)
return function(name)
local fname = string.gsub(name, "%.", "/")
local sError = ""
for pattern in string.gmatch(package.path, "[^;]+") do
local sPath = string.gsub(pattern, "%?", fname)
if sPath:sub(1, 1) ~= "/" then
sPath = fs.combine(dir, sPath)
end
if fs.exists(sPath) and not fs.isDir(sPath) then
local fnFile, sError = loadfile(sPath, nil, env)
if fnFile then
return fnFile, sPath
else
return nil, sError
end
else
if #sError > 0 then
sError = sError .. "\n "
end
sError = sError .. "no file '" .. sPath .. "'"
end
end
return nil, sError
end
end
local function make_require(package)
local sentinel = {}
return function(name)
expect(1, name, "string")
if package.loaded[name] == sentinel then
error("loop or previous error loading module '" .. name .. "'", 0)
end
if package.loaded[name] then
return package.loaded[name]
end
local sError = "module '" .. name .. "' not found:"
for _, searcher in ipairs(package.loaders) do
local loader = table.pack(searcher(name))
if loader[1] then
package.loaded[name] = sentinel
local result = loader[1](name, table.unpack(loader, 2, loader.n))
if result == nil then result = true end
package.loaded[name] = result
return result
else
sError = sError .. "\n " .. loader[2]
end
end
error(sError, 2)
end
end
--- Build an implementation of Lua's @{package} library, and a @{require}
-- function to load modules within it.
--
-- @tparam table env The environment to load packages into.
-- @tparam string dir The directory that relative packages are loaded from.
-- @treturn function The new @{require} function.
-- @treturn table The new @{package} library.
local function make_package(env, dir)
expect(1, env, "table")
expect(2, dir, "string")
local package = {}
package.loaded = {
_G = _G,
bit32 = bit32,
coroutine = coroutine,
math = math,
package = package,
string = string,
table = table,
}
package.path = "?;?.lua;?/init.lua;/rom/modules/main/?;/rom/modules/main/?.lua;/rom/modules/main/?/init.lua"
if turtle then
package.path = package.path .. ";/rom/modules/turtle/?;/rom/modules/turtle/?.lua;/rom/modules/turtle/?/init.lua"
elseif commands then
package.path = package.path .. ";/rom/modules/command/?;/rom/modules/command/?.lua;/rom/modules/command/?/init.lua"
end
package.config = "/\n;\n?\n!\n-"
package.preload = {}
package.loaders = { preload(package), from_file(package, env, dir) }
return make_require(package), package
end
return { make = make_package }

View File

@ -11,6 +11,7 @@
-- @module[module] shell
local expect = dofile("rom/modules/main/cc/expect.lua").expect
local make_package = dofile("rom/modules/main/cc/require.lua").make
local multishell = multishell
local parentShell = shell
@ -28,94 +29,10 @@ local tCompletionInfo = parentShell and parentShell.getCompletionInfo() or {}
local tProgramStack = {}
local shell = {} --- @export
local function createShellEnv(sDir)
local tEnv = {}
tEnv.shell = shell
tEnv.multishell = multishell
local package = {}
package.loaded = {
_G = _G,
bit32 = bit32,
coroutine = coroutine,
math = math,
package = package,
string = string,
table = table,
}
package.path = "?;?.lua;?/init.lua;/rom/modules/main/?;/rom/modules/main/?.lua;/rom/modules/main/?/init.lua"
if turtle then
package.path = package.path .. ";/rom/modules/turtle/?;/rom/modules/turtle/?.lua;/rom/modules/turtle/?/init.lua"
elseif commands then
package.path = package.path .. ";/rom/modules/command/?;/rom/modules/command/?.lua;/rom/modules/command/?/init.lua"
end
package.config = "/\n;\n?\n!\n-"
package.preload = {}
package.loaders = {
function(name)
if package.preload[name] then
return package.preload[name]
else
return nil, "no field package.preload['" .. name .. "']"
end
end,
function(name)
local fname = string.gsub(name, "%.", "/")
local sError = ""
for pattern in string.gmatch(package.path, "[^;]+") do
local sPath = string.gsub(pattern, "%?", fname)
if sPath:sub(1, 1) ~= "/" then
sPath = fs.combine(sDir, sPath)
end
if fs.exists(sPath) and not fs.isDir(sPath) then
local fnFile, sError = loadfile(sPath, nil, tEnv)
if fnFile then
return fnFile, sPath
else
return nil, sError
end
else
if #sError > 0 then
sError = sError .. "\n "
end
sError = sError .. "no file '" .. sPath .. "'"
end
end
return nil, sError
end,
}
local sentinel = {}
local function require(name)
expect(1, name, "string")
if package.loaded[name] == sentinel then
error("loop or previous error loading module '" .. name .. "'", 0)
end
if package.loaded[name] then
return package.loaded[name]
end
local sError = "module '" .. name .. "' not found:"
for _, searcher in ipairs(package.loaders) do
local loader = table.pack(searcher(name))
if loader[1] then
package.loaded[name] = sentinel
local result = loader[1](name, table.unpack(loader, 2, loader.n))
if result == nil then result = true end
package.loaded[name] = result
return result
else
sError = sError .. "\n " .. loader[2]
end
end
error(sError, 2)
end
tEnv.package = package
tEnv.require = require
return tEnv
local function createShellEnv(dir)
local env = { shell = shell, multishell = multishell }
env.require, env.package = make_package(env, dir)
return env
end
-- Colours

View File

@ -0,0 +1,79 @@
describe("cc.require", function()
local r = require "cc.require"
local function mk()
local env = setmetatable({}, { __index = _ENV })
env.require, env.package = r.make({}, "/test-files/modules")
return env.require, env.package
end
local function setup(path, contents)
fs.delete("/test-files/modules")
io.open(path, "w"):write(contents):close()
end
describe("require", function()
it("errors on recursive modules", function()
local require, package = mk()
package.preload.pkg = function() require "pkg" end
expect.error(require, "pkg"):eq("loop or previous error loading module 'pkg'")
end)
it("supplies the current module name", function()
local require, package = mk()
package.preload.pkg = table.pack
expect(require("pkg")):same { n = 1, "pkg" }
end)
it("returns true instead of nil", function()
local require, package = mk()
package.preload.pkg = function() return nil end
expect(require("pkg")):eq(true)
end)
it("returns a constant value", function()
local require, package = mk()
package.preload.pkg = function() return {} end
expect(require("pkg")):eq(require("pkg"))
end)
it("returns an error on not-found modules", function()
local require, package = mk()
package.path = "/?;/?.lua"
expect.error(require, "pkg"):eq(
"module 'pkg' not found:\n" ..
" no field package.preload['pkg']\n" ..
" no file '/pkg'\n" ..
" no file '/pkg.lua'")
end)
end)
describe("the file loader", function()
local function get(path)
local require, package = mk()
if path then package.path = path end
return require
end
it("works on absolute paths", function()
local require = get("/test-files/?.lua")
setup("test-files/some_module.lua", "return 123")
expect(require("some_module")):eq(123)
end)
it("works on relative paths", function()
local require = get("?.lua")
setup("test-files/modules/some_module.lua", "return 123")
expect(require("some_module")):eq(123)
end)
it("fails on syntax errors", function()
local require = get("?.lua")
setup("test-files/modules/some_module.lua", "1")
expect.error(require, "some_module"):str_match(
"^module 'some_module' not found:\n" ..
" no field package.preload%['some_module'%]\n" ..
" [^:]*some_module.lua:1: unexpected symbol near '1'$"
)
end)
end)
end)