lualuabindmodule-search-path

How to set search paths allow lua addins as packages


tl;dr: I want to create lua packages with a custom directory name pattern, having problem with search paths.

The problem

I've got an application that I'm wanted to allow the user to write plugins for, following a similar model to Lightroom:

The problem I'm grappling with is how best to wrap the plugins as packages (modules + submodules) or regular scripts. I envisage that a plugin might include 3rd party modules:

Foo.myplugin/
    info.lua - returns a table with plugin name, version info, list of exported functions, etc
    Foo.lua - defines the main functions exported by this plugin, which calls other scripts:
    UsefulFunctions.lua - used by Foo.lua
    3rdparty/3rdparty.lua - 3rd party module

If I set the package search path, package.path to include

<appdata>/?.myplugin/?.lua

then I can load the package with Foo=require 'Foo'. However, I can't work out how to get submodules loaded. If Foo.lua calls UsefulFunctions=require 'UsefulFunctions' then this load fails because lua's search path tries to look for UsefulFunctions.myplugin/UsefulFunctions.lua. I can't load it with require 'Foo.UsefulFunctions' either, for similar reasons.

Some options:

Is there any way of providing the functionality I need?

I'm currently on Lua 5.1. I know 5.2 has more control over package search paths, but I don't think I have the option of updating to it at the moment. I'm also using luabind, though I don't think it is relevant to this.


Solution

  • You could customize the way Lua searches for modules using a custom searcher function, using the mechanisms outlined in the documentation of require and package.loaders.

    The trick is to detect that the module can be found in a directory with the .myplugins suffix and to keep track of the path of the bundles. Consider the following scripts.

    -- <appdata>/plugins/foo.myplugin/foo.lua
    
    local auxlib = require 'foo.auxlib'
    local M = {}
    function M.Foobnicator()
        print "Called: Foobnicator!!"
        auxlib.AuxFunction()
    end
    return M
    

     

    -- <appdata>/plugins/foo.myplugin/auxlib.lua
    
    local M = {}
    function M.AuxFunction()
        print "Called: AuxFunction!!"
    end
    return M
    

     

    -- main.lua
    
    package.path = package.path .. ";" 
        .. [[<appdata>/plugins/?.myplugin/?.lua]]
    local bundles = {}  -- holds bundle names and pathnames
    
    local function custom_searcher( module_name )
        if string.match( module_name, '%.' ) then
            -- module name has a dot in it - it is a submodule, 
            -- let's check if it is inside a bundle
            local main_module_name, subname = 
                string.match( module_name, '^([^.]-)%.(.+)' )
            local main_path = bundles[ main_module_name ]
            if main_path then  -- OK, it's a submodule of a known bundle
                local sub_fname = string.gsub( subname, '%.', '/' )
                -- replace main module filename with that of submodule
                local path = string.match( main_path, '^.*[/\\]' ) 
                    .. sub_fname .. '.lua'
                return loadfile( path )
            else    -- not a bundle - give up the search
                return
            end
        end
    
        -- search for the module scanning package.path
        for template in string.gmatch( package.path, '[^;]+' ) do
            if string.match( template, '%.myplugin' ) then -- bundle?                
                local module_path = 
                    string.gsub( template, '%?', module_name )
                local fh = io.open( module_path )     -- file exists?
                if fh then  -- module found
                    fh:close()
                    bundles[ module_name ] = module_path
                    return loadfile( module_path )
                end
            end
        end
    end
    
    -- sets the custom searcher as the first one so to take
    -- precedence over default ones
    table.insert( package.loaders, 1, custom_searcher )
    
    local foo = require 'foo'
    foo.Foobnicator()
    

    Running main.lua will produce the following output:

    Called: Foobnicator!!
    Called: AuxFunction!!
    

    I hope this will put you on the right track. Probably it doesn't cover every possibility and the error handling is not at all complete, but it should give you a good base to work on.