Initial code

This commit is contained in:
prestidigitator 2013-03-01 19:34:17 -08:00
parent a0fcd80f0b
commit bbccf2b18a
9 changed files with 698 additions and 1 deletions

200
README.md
View File

@ -1,4 +1,202 @@
minetest-lib-ModLib
===================
Library for Minetest mods and libraries to load other libraries with version requirements and one-time loading
Library for Minetest mods and libraries to load other libraries with version
requirements and one-time loading
General
=======
This is NOT a "mod"; it is a LIBRARY for use by mods and other libraries. That
means it does not modify how Minetest works in any way, but provides a useful
API to help mods do so. This particular library is used purely for managing
the loading of other libraries, and has no dependencies.
Installation
============
The file "lib/ModLib.lua" in this package should be installed in a "lib"
subdirectory of the main directory of the mod that depends on it. If other
mods under the same game also include versions of ModLib, only one instance of
any given version will be loaded. This is the same for any libraries that
ModLib is used to load as well.
Other libraries the mod depends on, and that will be loaded using ModLib,
should also be included in this "lib" subdirectory. Their filenames must have
the form "LibName_version.lua". LibName is the name of the library, which must
start with a letter and contain only letters and digits. And version is a
version number like "x.y.z" (with any number of components) where the periods
are replaced with dashes ("x-y-z"). Due to a definiciency in the current
version of Lua, another file called "ModLib_list.txt" must exist in the "lib"
subdirectory and must contain the name of each library file on a separate line,
with no extra characters or empty lines. In Linux this list can be generated
using the command "ls -1 >ModLib_list.txt" from within the "lib" subdirectory.
The ONLY exception to including the version number in the name of the library
file is for "ModLib.lua", and that is because it includes special boostrap code
and a version of it must be loadable from any mod that loads a library, without
depending on a specific version during bootstrap.
For example, if a mod "my_mod" depends on LibMod 1.0 and version 2.3 of another
mod library "MyLib" and is installed to:
.../mods/minetest/my_mod
then the following files must exist:
.../mods/minetest/my_mod/lib/ModLib_list.txt
.../mods/minetest/my_mod/lib/ModLib.lua
.../mods/minetest/my_mod/lib/MyLib_2-3.lua
and ModLib_list.txt must contain the following lines:
ModLib_list.txt
ModLib.lua
MyLib_2-3.lua
(The inclusion of "ModLib_list.txt" and "ModLib.lua" in this file is optional.)
Use
===
All libraries a mod depends on should be included with the mod. This ensures
these libraries are available to the mod whether or not they are also included
in other mods, and whether or not the version in any other mods is incompatible
with the version this mod needs. The exception would be if the mod depends on
another mod and uses a library the dependency includes in a limited fashion
rather than being directly dependent on details of the library API.
In order to load one of these dependent libraries from your mod through ModLib,
first you must bootstrap ModLib, then add your "lib" subdirectory to its
library load path, then ask it to load your library, specifying what versions
of the library are compatible with your mod. For the above example using
ModLib 1.0 and MyLib 2.3 this would look like:
-- Bootstrap ModLib
local MOD_NAME = minetest.get_current_modname()
local MOD_PATH = minetest.get_modpath(MOD_NAME)
local ModLib = dofile(MOD_PATH.."/lib/ModLib.lua")
-- Add to the library path:
ModLib.addDir(MOD_PATH.."/lib")
-- Load the dependent library. Note that this MyLib 2.3 OR LATER. If the
-- exact version were required, or if a range of versions were acceptable,
-- then a second (maximum) version number would be added to the call.
local MyLib = ModLib.load("MyLib", "2.3")
Note carefully the use of local variable rather than globals. This MUST always
be how a mod or library loads another library, in order to allow multiple
versions to coexist. Note, however, that you may add the module API as a field
of the module's own API in order to use it from other files or even other mods
(see the note above about libraries included by dependent mods). So my_mod
could then do this:
my_mod.MyLib = MyLib
which would allow my_mod.MyLib to be accessed from any Lua file in the mod or
any other mod that depends on it.
The use of ModLib from another library is only slightly different. The library
should assume that the mod that is loading it has added any library directories
to the load path already, so it can skip this step. So from a library this
looks like:
-- Bootstrap ModLib
local LOADING_MOD = minetest.get_current_modname()
local LOADING_MOD_PATH = minetest.get_modpath(MOD_NAME)
local ModLib = dofile(LOADING_MOD_PATH.."/lib/ModLib.lua")
-- Load the dependent library, with a minimum version of 1.0.
local MyOtherLib = ModLib.load("MyOtherLib", "1.0")
local MyLib = {}
...
return MyLib
Now let's assume that MyLib is updated a few times and everything works up to
version 3.0, but then 3.1 comes along and gets included in a bunch of mods, and
we discover that this update breaks my_mod. Until we can update my_mod to
account for the changes, we make a one-line change to make sure it will not
load anything above version 3.0 for my_mod:
local MyLib = ModLib.load("MyLib", "2.3", "3.0")
The other modules that can use version 3.1 will continue to use it and will not
be affected by this change.
Advanced Use
============
What if newer versions of ModLib come out, and your module depends on newer
features? Well, there must always be an unversioned "lib/ModLib.lua" that any
mod may load during bootstrap (from any mod that might load the library).
However, ModLib itself acts as another library that it can load by version, and
it is perfectly okay to have both the unversioned file name (with any actual
version in it) and versioned filenames in the load path. So just go ahead and
include your newer version of ModLib twice in the mod, with the two paths
"lib/ModLib.lua" and "lib/ModLib_x-y-z.lua".
What about more complex version dependencies? The API currently allows a
minimum version of a library to be specified, a maximum version, or both. What
if there are multiple ranges, or one particular plagued version that we want to
exclude? In future versions of ModLib the API may allow for more complex
version specification, but for now you can take advantage of the builtin error
handling mechanism of Lua to successively try to load acceptable versions or
ranges.
For example, take the above scenario and assume that MyLib comes out with
version 3.2 that fixes the issues my_mod had with version 3.1. How can we load
verison 3.2 if it is available, and otherwise load versions 2.3 through 3.0?
First, we could choose to obsolete use of the older versions and switch my_mod
to ship with version 3.2 instead of the old 2.3 to ensure it is available. But
we could also do this:
local status, MyLib = pcall(ModLib.load, "MyLib", "3.2")
if not (status and MyLib) then
status, MyLib = pcall(ModLib.load, "2.3", "3.0")
end
if not (status and MyLib) then
error("No compatible version of MyLib found")
end
Suggested Version Dependencies
==============================
How specific you make the version requirements is really up to you. It depends
on how you want to balance stability versus flexibility. For maximum
stability, you should require only versions that you have thoroughly tested
your mod or library against. For more flexibility, allowing future library
versions that might fix defects or enhanced behavior can be enabled by only
requiring a minimum version, at least until such time as up update to the
library breaks things.
A middle ground could be to require exact versions of unstable or uncertain
libraries, but allow libraries from more consistent developers to update
without limit, or until some large minor version number (e.g. 2.99999) but not
the next major version (e.g. 3.0).
Testing
=======
A couple of unit tests are included with ModLib. From the base directory of
the package, you can run the following, and should get the listed output:
$ lua test/ModLib_test.lua
ModLib tests PASSED
$ lua test/ModLib_Version.lua
ModLib.Version tests PASSED
These tests can also be run within Minetest by including the ModLib library in
a mod in the normal way, adding the "test" directory and all contents from this
package as a subdirectory in the mod's main directory, and running the
following from the mod's code:
local MOD_NAME = minetest.get_current_modname()
local MOD_PATH = minetest.get_modpath(MOD_NAME)
dofile(MOD_PATH.."test/ModLib_test.lua")
dofile(MOD_PATH.."test/ModLib_Version_test.lua")

348
lib/ModLib.lua Normal file
View File

@ -0,0 +1,348 @@
-- Constants that must NEVER change
local REGISTRY_KEY = "ModLib:registry"
local LIB_DIRS_KEY = "ModLib:libdirs"
local THIS_LIB_NAME = "ModLib"
-- Constants for THIS version
local THIS_VERSION_STR = "1.0"
local LIB_NAME_PATT = "^[a-zA-Z][a-zA-Z0-9]*$"
local LIB_FILENAME_PATT = "^([a-zA-Z][a-zA-Z0-9]*)_([0-9]+)()"
local LIB_FILENAME_EXTRA_PATT = "^%-([0-9]+)()"
local LIB_FILENAME_END_PATT = "^%.lua$"
local HACKY_DIR_LIST_FILENAME = "ModLib_list.txt"
-- Class Version, used to store both a hash and sorted table of library
-- instances by canonicalized version number.
--- Type initialized from "x.y.z" version strings, able to compare
-- lexicographically, component by component (x, then y, then z, etc.).
-- For example, if these versions are defined:
--
-- verA = ModLib.Version("1")
-- verB = ModLib.Version("1.0")
-- verC = ModLib.Version("1.0.0.3")
-- verD = ModLib.Version("1.1")
-- verE = ModLib.Version("2.0")
--
-- then the following expression will evaluate to true:
--
-- verA == verB and verB < verC and verC < verD and verD < verE
--
-- Converting a version back to a string will return the "canonicalized"
-- version number by stripping all trailing zeroes (until there is only one
-- component remaining). For example, given the above definitions, all of the
-- following are true:
--
-- tostring(verB) == "1"
-- tostring(verD) == "1.1"
-- tostring(ModLib.Version("0.0.0")) = "0"
-- tostring(ModLib.Version("0.1")) = "0.1"
local Version = {}
local Version_meta = {}
local Version_inst_meta = {}
setmetatable(Version, Version_meta)
local function Version_constr(class, verstr)
self = {}
local len = string.len(verstr)
local si = 1
local ei, comp
while si <= len do
ei = string.find(verstr, ".", si, true)
if not ei then ei = len+1 end
if ei <= si or ei == len then
self = nil
break
end
comp = string.sub(verstr, si, ei-1)
if not string.find(comp, "^[0-9]+$") then
self = nil
break;
end
table.insert(self, tonumber(comp))
si = ei+1
end
if self then
while #self > 1 and self[#self] == 0 do
table.remove(self)
end
setmetatable(self, Version_inst_meta)
end
return self
end
Version_meta.__call = Version_constr
local function Version_cmp(ver1, ver2)
local diff
for i = 1, math.min(#ver1, #ver2) do
diff = ver1[i] - ver2[i]
if diff > 0 then return 1 elseif diff < 0 then return -1 end
end
diff = #ver1 - #ver2
if diff > 0 then return 1 elseif diff < 0 then return -1 end
return 0
end
Version_inst_meta.__eq = function(ver1, ver2)
if #ver1 ~= #ver2 then return false end
return (Version_cmp(ver1, ver2) == 0)
end
Version_inst_meta.__lt = function(ver1, ver2)
return (Version_cmp(ver1, ver2) < 0)
end
local function Version_concat(val1, val2)
if type(val1) == "table" then
val1 = tostring(val1)
end
if type(val2) == "table" then
val2 = tostring(val2)
end
return val1..val2
end
Version_inst_meta.__concat = Version_concat
local function Version_tostring(ver)
return table.concat(ver, ".")
end
Version_inst_meta.__tostring = Version_tostring
-- Bootstrap stuff, so we only "load" this library once
local THIS_VERSION = Version(THIS_VERSION_STR)
local registry = _G[REGISTRY_KEY] or {}
_G[REGISTRY_KEY] = registry
local libDirs = _G[LIB_DIRS_KEY] or {}
_G[LIB_DIRS_KEY] = libDirs
local modLibVersions = registry[THIS_LIB_NAME] or {}
registry[THIS_LIB_NAME] = modLibVersions
local modLibVersionsByValue = modLibVersions.byValue or {}
modLibVersions.byValue = modLibVersionsByValue
local modLibVersionsSorted = modLibVersions.sorted or {}
modLibVersions.sorted = modLibVersionsSorted
local ModLib = modLibVersionsByValue[tostring(THIS_VERSION)]
if ModLib then return ModLib end
-- Finally the ModLib class itself, with Version enclosed for external use:
--- Used to load versions of libraries with version requirements, and load each
-- version only once.
--
-- Example of use:
--
-- local MOD_NAME = minetest.get_current_modname()
-- local MOD_PATH = minetest.get_modpath(MOD_NAME)
-- local ModLib = dofile(MOD_PATH.."/lib/ModLib_1-0.lua")
-- ModLib.addDir(MOD_PATH.."/lib")
-- local MyLib = ModLib.load("MyLib", "i.j.k", "m.n.p")
--
-- where "i.j.k" and "m.n.p" are version strings indicating the minimum and
-- maximum versions of "MyLib" that can be used by the module (with any number
-- of components, not limited to the three shown here). This will attempt to
-- load a file called "MyLib_a-b-c.lua" from any of the directories added
-- through ModLib.addDir(dir) meeting the requirement that
-- "i.j.k" <= "a.b.c" <= "m.n.p" lexicographically (after trailing zero
-- components are stripped from each version string). If multiple files meet
-- the requirement, the one with greatest "a.b.c" will be used. To require an
-- exact version, use the same value for the minimum and maximum versions. To
-- require ANY version (the highest version will be used), omit both
-- versions (or use nil for them). To specify a half-open range, use nil for
-- either (or omit the maximum).
--
-- These are the requirements for mod libraries:
--
-- 1.) The library name must begin with a letter, and must contain only
-- letters and digits.
-- 2.) They MUST NOT set any global variables except in the rare case that it
-- is always going to be okay to share the value between ALL versions
-- of the library (note the local variables in the example above). In
-- particular, the "MyLib" variable here must NOT be set as a global
-- variable by the library, and must instead be a local variable returned
-- from the file.
-- 3.) They MUST return the main class of the library itself from the file.
-- 4.) They MAY optionally set MyLib.VERSION in the returned main class, but
-- this will be overwritten by ModLib with the canonicalized version
-- string "a.b.c", constructed from the name of the file the library is
-- loaded from.
--
-- Note that ModLib itself is a mod library, so it could conceivably be used
-- to load a different version of itself after the initial bootstrap loading
-- (though it's unlikely you'd want to)!
--
-- Here is an example of a dirt simple mod library that defines a field and a
-- function:
--
-- -- MyDumbLib_1-0.lua
-- local MyDumbLib = {}
-- MyDumbLib.value = 1
-- function MyDumbLib.foo() print("foo!") end
-- return MyDumbLib
--
-- NOTE: Due to a deficiency in the Lua 5.1 standard library, directories
-- cannot be listed without the aid of a C extension to the API. Therefore,
-- you must create a file called "ModLib_list.txt" in each directory added
-- through ModLib.addDir(dir) that contains one simple filename per line of
-- the valid library files in that directory. This can be done in Linux using
-- the command "ls -1 >ModLib_list.txt" from within the directory. Hopefully
-- this library becomes a part of the Minetest core at some point, and all of
-- this can be done flawlessly from the C++ instead.
ModLib = {}
ModLib.Version = Version
ModLib.VERSION = tostring(THIS_VERSION)
-- Returns index of the last array element from a sorted array less than or
-- equal to a value, zero if all elements of the table are greater than the
-- value or if the table is empty.
local function binarySearchUpperBound(table, value)
if #table < 1 then return 0 end
if table[#table] <= value then return #table end
if table[1] > value then return 0 end
local si = 1
local ei = #table
local mi = math.floor((si+ei)/2)
local mv = table[mi]
while mi > si do
local mv = table[mi]
if mv <= value then
si = mi
else
ei = mi
end
mi = math.floor((si+ei)/2)
mv = table[mi]
end
return si
end
local function addLib(libName, ver, filePath)
local verStr = tostring(ver)
if not registry[libName].byValue[verStr] then
local loader = loadfile(filePath)
local lib = loader(libName)
lib.VERSION = tostring(ver)
registry[libName].byValue[verStr] = lib
local sorted = registry[libName].sorted
local pos = binarySearchUpperBound(sorted, ver)
if not pos then pos = 0 end
table.insert(sorted, pos+1, ver)
end
end
local function ModLib_load(libName, minVerStr, maxVerStr)
local minVer = minVerStr and Version(minVerStr)
local maxVer = maxVerStr and Version(maxVerStr)
if minVerStr and not minVer then
error("Bad version string '"..minVerStr.."'", 2)
end
if maxVerStr and not maxVer then
error("Bad version string '"..maxVerStr.."'", 2)
end
if not string.match(libName, LIB_NAME_PATT) then
error("Bad library name '"..libName.."'", 2)
end
if minVer and maxVer and minVer > maxVer then
error("Min version greater than max version", 2)
end
local libVersions = registry[libName] or {}
registry[libName] = libVersions
local libVersionsByValue = libVersions.byValue or {}
libVersions.byValue = libVersionsByValue
local libVersionsSorted = libVersions.sorted or {}
libVersions.sorted = libVersionsSorted
if maxVer then
local lib = libVersionsByValue[tostring(maxVer)]
if lib then return lib end
end
for i, dir in ipairs(libDirs) do
local listFile = io.open(dir.."/"..HACKY_DIR_LIST_FILENAME)
if listFile then
listFile:close()
for fileName in io.lines(dir.."/"..HACKY_DIR_LIST_FILENAME) do
local name, verStr, i = string.match(fileName, LIB_FILENAME_PATT)
if name and verStr and i then
while i <= #fileName do
local verComp, ni = string.match(fileName,
LIB_FILENAME_EXTRA_PATT,
i)
if verComp and ni then
verStr = verStr.."."..verComp
i = ni
else
break
end
end
if not string.match(fileName, LIB_FILENAME_END_PATT, i) then
name = nil
verStr = nil
end
end
if name and verStr and name == libName then
local ver = Version(verStr)
if (not minVer or ver >= minVer) and
(not maxVer or ver <= maxVer)
then
local filePath = dir.."/"..fileName
local file = io.open(filePath)
if file then
file:close()
addLib(libName, ver, filePath)
if maxVer and ver == maxVer then
return libVersionsByValue[tostring(maxVer)]
end
end
end
end
end
end
end
local pos = (maxVer) and binarySearchUpperBound(libVersionsSorted, maxVer)
or #libVersionsSorted
local ver = (pos and pos >= 1 and pos <= #libVersionsSorted)
and libVersionsSorted[pos]
or nil
if not ver or (minVer and ver < minVer) then
local msg = "Library "
if minVer then msg = msg..tostring(minVer).." < " end
msg = msg.."'"..libName.."'"
if maxVer then msg = msg.." < "..tostring(maxVer) end
msg = msg.." not found"
error(msg, 2)
end
return libVersionsByValue[tostring(ver)]
end
ModLib.load = ModLib_load
local function ModLib_addDir(dir)
if not libDirs[dir] then
table.insert(libDirs, dir)
libDirs[dir] = #libDirs
end
end
ModLib.addDir = ModLib_addDir
return ModLib

View File

@ -0,0 +1,41 @@
local BASE_DIR
if minetest then
local LOADING_MOD = minetest.get_current_modname()
BASE_DIR = minetest.get_modpath(LOADING_MOD)
else
BASE_DIR = os.getenv("PWD")
end
local ModLib = dofile(BASE_DIR.."/lib/ModLib.lua")
local verA = ModLib.Version("1")
local verB = ModLib.Version("1.0")
local verC = ModLib.Version("1.0.0.3")
local verD = ModLib.Version("1.1")
local verE = ModLib.Version("2.0")
assert("1" == tostring(verA))
assert("1" == tostring(verB))
assert("1.0.0.3" == tostring(verC))
assert("1.1" == tostring(verD))
assert("2" == tostring(verE))
assert(verA == verB)
assert(verB < verC)
assert(verC < verD)
assert(verD < verE)
assert(verA <= verB)
assert(verB <= verC)
assert(verC <= verD)
assert(verD <= verE)
assert("0" == tostring(ModLib.Version("0.0.0")))
assert("0.1" == tostring(ModLib.Version("0.1")))
local verF = ModLib.Version("3.14")
local verG = ModLib.Version("3.4")
assert(verG < verF)
print("ModLib.Version tests PASSED")

94
test/ModLib_test.lua Normal file
View File

@ -0,0 +1,94 @@
local BASE_DIR
if minetest then
local LOADING_MOD = minetest.get_current_modname()
BASE_DIR = minetest.get_modpath(LOADING_MOD)
else
BASE_DIR = os.getenv("PWD")
end
local ModLib = dofile(BASE_DIR.."/lib/ModLib.lua")
ModLib.addDir(BASE_DIR.."/lib")
ModLib.addDir(BASE_DIR.."/test/lib")
assert(not pcall(ModLib.load, "Zoog"))
local MyLib1 = ModLib.load("MyLib", "1", "1")
local MyLib1b = ModLib.load("MyLib", "1.0", "1.0")
assert(MyLib1)
assert(MyLib1b)
assert(MyLib1 == MyLib1b)
assert("1" == MyLib1.VERSION)
assert("1.0" == MyLib1.v)
local MyLib1_1 = ModLib.load("MyLib", "1.1", "1.1")
assert(MyLib1_1)
assert(MyLib1_1 ~= MyLib1)
assert("1.1" == MyLib1_1.VERSION)
assert("1.1" == MyLib1_1.v)
local MyLib2 = ModLib.load("MyLib", "2.0.0.0.0.0", "2")
assert(MyLib2)
assert(MyLib2 ~= MyLib1)
assert(MyLib2 ~= MyLib1_1)
assert("2" == MyLib2.VERSION)
assert("2.0.0" == MyLib2.v)
local MyLib2_0_5 = ModLib.load("MyLib", "2.0.5", "2.0.5")
assert(MyLib2_0_5)
assert(MyLib2_0_5 ~= MyLib1)
assert(MyLib2_0_5 ~= MyLib1_1)
assert(MyLib2_0_5 ~= MyLib2)
assert("2.0.5" == MyLib2_0_5.VERSION)
assert("2.0.5" == MyLib2_0_5.v)
local MyLib_any = ModLib.load("MyLib")
assert(MyLib_any)
assert(MyLib_any == MyLib2_0_5)
local MyLib_min0_5 = ModLib.load("MyLib", "0.5")
local MyLib_min1_0_5 = ModLib.load("MyLib", "1.0.5")
local MyLib_min1_5 = ModLib.load("MyLib", "1.0.5")
local MyLib_min2 = ModLib.load("MyLib", "2")
assert(not pcall(ModLib.load, "MyLib", "3"))
assert(MyLib_min0_5)
assert(MyLib_min1_0_5)
assert(MyLib_min1_5)
assert(MyLib_min2)
assert(MyLib_min0_5 == MyLib2_0_5)
assert(MyLib_min1_0_5 == MyLib2_0_5)
assert(MyLib_min1_5 == MyLib2_0_5)
assert(MyLib_min2 == MyLib2_0_5)
assert(not pcall(ModLib.load, "MyLib", nil, "0.5"))
local MyLib_max1 = ModLib.load("MyLib", nil, "1")
local MyLib_max1_0_1 = ModLib.load("MyLib", nil, "1.0.1")
local MyLib_max1_1 = ModLib.load("MyLib", nil, "1.1")
local MyLib_max1_5 = ModLib.load("MyLib", nil, "1.5")
local MyLib_max2 = ModLib.load("MyLib", nil, "2")
local MyLib_max3 = ModLib.load("MyLib", nil, "3")
assert(MyLib_max1 == MyLib1)
assert(MyLib_max1_0_1 == MyLib1)
assert(MyLib_max1_1 == MyLib1_1)
assert(MyLib_max1_5 == MyLib1_1)
assert(MyLib_max2 == MyLib2)
assert(MyLib_max3 == MyLib2_0_5)
assert(not pcall(ModLib.load, "MyLib", "2", "1"))
local MyLib_1_to_2 = ModLib.load("MyLib", "1", "2")
local MyLib_1_1_to_2 = ModLib.load("MyLib", "1.1", "2")
local MyLib_2_to_3 = ModLib.load("MyLib", "2", "3")
assert(not pcall(ModLib.load, "MyLib", "2.5", "3"))
assert(MyLib_1_to_2)
assert(MyLib_1_to_2 == MyLib2)
assert(MyLib_1_1_to_2 == MyLib2)
assert(MyLib_2_to_3 == MyLib2_0_5)
print("ModLib tests PASSED")

4
test/lib/ModLib_list.txt Normal file
View File

@ -0,0 +1,4 @@
MyLib_1-0.lua
MyLib_1-1.lua
MyLib_2-0-0.lua
MyLib_2-0-5.lua

3
test/lib/MyLib_1-0.lua Normal file
View File

@ -0,0 +1,3 @@
local MyLib = {}
MyLib.v = "1.0"
return MyLib

3
test/lib/MyLib_1-1.lua Normal file
View File

@ -0,0 +1,3 @@
local MyLib = {}
MyLib.v = "1.1"
return MyLib

3
test/lib/MyLib_2-0-0.lua Normal file
View File

@ -0,0 +1,3 @@
local MyLib = {}
MyLib.v = "2.0.0"
return MyLib

3
test/lib/MyLib_2-0-5.lua Normal file
View File

@ -0,0 +1,3 @@
local MyLib = {}
MyLib.v = "2.0.5"
return MyLib