-- LTN-11 derivative package management system.

-- Written by Rici Lake, released into the public domain.

-- Each "file" is assumed to be an object constructor: it takes some arguments
-- and returns an object. If no object can be constructed, it *should* return
-- <nil, error>, but it is currently called inside a pcall anyway.

-- In an ideal world, a chunk itself could be a constructor function but
-- Lua (as of 5.0.2) does not provide a mechanism to pass arguments to
-- chunk functions. So for now we use a shim which actually calls the chunk
-- and returns the constructor function. It is highly recommended that
-- the chunk have the format:
--  -- comments
--  return function(<parameters>)
--    ...
--    return <new object>
--  end

-- We also define a specific type of package: a module.
-- A module constructor is a function of one argument:
--   pkgtable = ctr(pkgtable)
-- The return value must be the actual package table, filled in with
-- package functions. If the function is called by import, there
-- will always be a packagetable. If it is called by new (or in
-- any other way) all bets are off.

-- Object construction:
--   ctr, err = new(pkgname)
--   -- but usually:
--   object = new(pkgname)(ctr args)
--   new actually returns the constructor function for the named
--   package, if it can find one; otherwise it returns nil and an error.
--   Constructors are cached, so the filesystem is only consulted once.
--   Although this may seem odd, it allows the syntax:
--      aFoo = new "Foo" (42)
--   which I find clearer than new("Foo", 42).
--   If "Foo" cannot be found, the error message will, unfortunately, be lost.
--   So some might prefer:
--    constructor = new
--    function new(pkgname) return assert(constructor, pkgname) end
--  or:
--    local Foo = assert(new "Foo")
-- Importing a package:
--   pkgtable = import(pkgname)
--   Package tables are cached, so this will only consult the filesystem
--   once (and only call the package constructor function once).
--   import will throw an error if it is not successful. To avoid this, use pcall.

-- Additional interfaces are provided in the package table "package"
-- Register a constructor:
--   func, x = package.registerCtr(name, func, x)
-- Register a package table:
--   pkgtable, x = package.registerPkg(name, pkgtable, x)
-- Note: You can invalidate a cache by setting the second argument
--  to nil (or just omitting it altogether), but in order to get
--  a new constructor to run for a package, both caches need to
--  be invalidated:  package.registerCtr(name, pkg.registerPkg(name))
-- Possible interfaces:
--   pkgtable = package.reload(name) -- but this needs work still
-- Utility functions:
--   chunk, err = package.loadFromPath(path, name)
--     loads the chunk which results from compiling the file found by
--     applying name to path. If the file isn't found or there is a syntax
--     error, returns nil and an error message. This uses the loadfile
--     function in what package thinks is the global environment, and sets
--     the global environment of the chunk to what package thinks is the
--     global environment.
--  for val = package.paths(path, name) do ... end
--     produces each substitution in turn.
-- Configuration:
--  package.loadpackage
--   is the function used by the package package to find constructors.
--   To be compatible with loadfile, the function is expected to return
--   a function of no arguments which returns the constructor.
--   The find function has the interface:
--     func, err = finder(name)
--   where err is used only if func is nil, and is a string indicating
--   the error.
--  package.path
--    The default path for the default package loader (which uses
--    loadFromPath).
-- Note 1:
-- loadFromPath depends on paths and loadfile. paths depends on the string
-- library. If the string library isn't present (!), paths will return the
-- null iterator. If loadfile isn't present, loadFromPath will return nil
-- and an error. (If string isn't present and loadfile is, loadfile will
-- never be called.)
-- Note 2:
-- If you want to substitute your own loadfile, it must return an
-- error message starting "cannot read" if the file (or whatever) doesn't
-- exist. This is crude, but I can't see another way of doing it.
-- Note 3:
-- package is itself a packaged object (not a module), but that only helps when
-- constructing sandboxes. The constructor takes a table which will be the
-- global table, and inserts the packaging utilities.

-- some default functions
local empty = function() end
local ident = function(x) return x end

-- This will need to be changed if string needs to be preinitialised

local strfind = string and string.find or empty
local gsub = string and string.gsub or ident

-- safe version of setfenv. Note that the arguments are
-- reversed, so that it can be used with loadfile, simply
-- passing through error indications.

local mysetfenv =
  and function(env, func, err)
        if type(func) == "function" then setfenv(func, env) else func = nil end
        return func, not func and (err or "mysetfenv failed")
  or  function(_, func) return func end

-- The actual package constructor, which is put into the initial
-- constructor cache

local function packageCtr(g)
  g = g or {}                -- FIXME
  local package = {}

  local _CTR = {package = packageCtr}
  local _PKG = {package = package}

  -- On the initial call, g should be the globals table. If this is called
  -- with new "package", it's up to the caller to supply a globals table
  -- if they have one set up.
  -- if loadfile and/or loadpackage don't exist, set them to something which
  -- always fails. They can always be updated later. loadfile is used by the
  -- default loadpackage; we probably don't actually need both of them.
  if not g.loadfile then 
    function g.loadfile(modname)
      return nil, "cannot read '"..modname .. "'; no package loader"

  if not package.loadpackage then 
    function package.loadpackage(modname)
      return nil, "cannot read '"..modname .. "'; no package loader"

  if not package.path then package.path = "" end
  -- shim function which runs the chunk in order to get the constructor
  -- This is not ideal; if Lua changes to allow chunks to have arguments,
  -- this could just be ditched.
  local function loadfileshim(fname)
    local chunk, err = mysetfenv(g, g.loadfile(fname))
    if chunk then return chunk()
             else return chunk, err
  local function new(pkgname)
    local ctr, err = _CTR[pkgname]
    if ctr then return ctr
           else ctr, err = package.loadpackage(pkgname)
                _CTR[pkgname] = ctr
                return ctr, err
  end = new
  -- import is subtler because we need to precreate the package table
  -- in case we have a circular reference.
  -- TODO: 
  --   Should actually use Wim's trigger to report an error on use of circular
  --   dependency.
  --   Or maybe it's ok to just have a stack overflow error instead of trying.
  --   We might want to track module dependencies here.
  function g.import(pkgname)
    local pkg = _PKG[pkgname]
    if pkg then return pkg else pkg = {} end
    _PKG[pkgname] = pkg
    local ok, rv, err = pcall(new, pkgname)
    if ok and rv then ok, rv, err = pcall(rv, pkg) end
    if ok and rv then return rv end
    -- If there was an error we need to reset the package cache and
    -- propagate the error.
    _PKG[pkgname] = nil
    -- First, figure out which one is the error :)
    if not ok then err = rv else err = err or "module returned nothing" end
    -- The error might or might not have a line number and might or
    -- might not have a traceback. We use a heuristic:
    if strfind(err, "^[^\n]*:%d+:") then
      -- seems to have a line number at least, so we'll add our own
      -- whine at the beginning.
      err = "import failed for '" .. pkgname .. "':\n" .. err
    error(err, 2)

  -- (Un)register packages and/or constructors.
  -- Perhaps new/import should actually use these functions :)
  function package.registerCtr(name, func, x)
    _CTR[name] = func
    return func, x
  function package.registerPkg(name, pkgtable, x)
    _PKG[name] = pkgtable
    return pkgtable, x

  -- This stuff should probably go somewhere else. But where?

  -- This would probably have been easier as a coroutine
  local function paths(path, name)
    local ee = 1
    local function aux(path)
      local _, e, seg = strfind(path, "([^;]+)", ee)
      if seg then ee = e + 2; return (gsub(seg, "%?", name)) end
    return aux, path, name
  package.paths = paths
  function package.loadFromPath(path, name)
    for seg in paths(path, name) do
      local ctr, err = loadfileshim(seg)
      if ctr then return ctr end  -- we found it, fine
      -- (presumably) the error result is either a read error or
      -- a syntax error. "Read error" is not quite good enough;
      -- we would like to continue on file not found but stop on,
      -- for example, removable media ejected during read. Furthermore,
      -- there are possibilities other than syntax error, but we're
      -- going to try to suppress tracebacks on those, so there.
      local _, _, errline = strfind(err or "", "(^[^\n]+)")
      if errline and not strfind(errline, "^cannot read") then
        -- there was a syntax error, return it.
        return ctr, errline
      -- otherwise, keep trying
    return nil, "cannot find '""' in '"..path.."'"
  -- return the newly created globals table.
  return g

-- If we've done this right, we'll only be called once by a correctly
-- functioning program. Hope that's true, cause we're going to construct
-- the initial package in the (supposed) root environment.

package = import "package"

-- The initialisation makes it possible for import to work with
-- standard libraries, without modifying code.
-- We only put the library packages into _PKG, not into _CTR, for now.
-- If any library is not in the globals table, the corresponding line
-- is a silent no-op.
package.registerPkg("table", table)
package.registerPkg("string", string)
package.registerPkg("math", math)
package.registerPkg("io", io)
package.registerPkg("os", os)
package.registerPkg("coroutine", coroutine)
package.registerPkg("coro", coroutine) -- coroutine is too long :)
package.registerPkg("debug", debug)

-- default path
package.path = LUA_PATH or (os and os.env and os.env "LUA_PATH")
                        or "?.lua;?"
-- default top-level loadpackage
function package.loadpackage(fname)
  return package.loadFromPath(package.path, fname)

Produced by TNT, the Lua-linter. TNT/0.5 Copyright (C) 2004 Rici Lake