local PROGRAMNAME = "amalg.lua"
local CACHEFILENAME = "amalg.cache"
Amalg is a Lua tool for bundling a Lua script and dependent
Lua modules in a single .lua
file for easier distribution.
The name of the script used in warning messages and the name of the cache file can be configured here by changing these local variables:
local PROGRAMNAME = "amalg.lua"
local CACHEFILENAME = "amalg.cache"
Lua 5.4 changed the format of the package.searchpath error message
local LUAVERSION = tonumber( _VERSION:match( "(%d+%.%d+)" ) )
local NOTFOUNDPREFIX = LUAVERSION < 5.4 and "" or "\n\t"
Wrong use of the command line may cause warnings to be printed to the console. This function is for printing those warnings:
local function warn( ... )
io.stderr:write( "WARNING ", PROGRAMNAME, ": " )
local n = select( '#', ... )
for i = 1, n do
local v = tostring( (select( i, ... )) )
io.stderr:write( v, i == n and '\n' or '\t' )
end
end
Function for parsing the command line of amalg.lua
when invoked
as a script. The following flags are supported:
--
: stop parsing command line flags (all remaining arguments
are considered module names)-a
, --no-argfix
: do not apply the arg
fix (local alias
for the global arg
table)-c
, --use-cache
: add the modules listed in the cache file
amalg.cache
-C <file>
, --cache-file=<file>
: add the modules listed in
the cache file -d
, --debug
: enable debug mode (file names and line numbers
in error messages will point to the original location)-f
, --fallback
: use embedded modules only as a fallback-h
, --help
: print help-i <pattern>
, --ignore=<pattern>
: ignore modules in the
cache file matching the given pattern (can be given multiple
times)-o <file>
, --output=<file>
: specify output file (default is
stdout
)-p <file>
, --prefix=<file>
: use file contents as prefix
code for the amalgamated script (i.e. usually as a package
module stub)-s <file>
, --script=<file>
: specify main script to bundle-S <shebang>
, --shebang=<shebang>
: Specify shebang line to
use for the resulting script-t <plugin>
, --transform=<plugin>
: use transformation
plugin (can be given multiple times)-v <file>
, --virtual-io=<file>
: embed as virtual resource
(can be given multiple times)-x
, --c-libs
: also embed compiled C modules-z <plugin>
, --zip=<plugin>
: use (de-)compression plugin
(can be given multiple times)Other arguments are assumed to be module names. For an inconsistent command line (e.g. duplicate options) a warning is printed to the console.
local function parsecommandline( ... )
local options = {
modules = {}, argfix = true, ignorepatterns = {}, plugins = {},
packagefieldname = "preload", virtualresources = {}
}
local pluginalreadyadded = {} -- to remove duplicates
local function makesetter( what, fieldname, optionname )
return function( v )
if v then
if options[ fieldname ] then
warn( "Resetting "..what.." '"..options[ fieldname ]..
"'! Using '"..v.."' now!" )
end
options[ fieldname ] = v
else
warn( "Missing argument for "..optionname.." option!" )
end
end
end
local setoutputname = makesetter( "output file", "outputname", "-o/--output" )
local setcachefilename = makesetter( "cache file", "cachefile", "-C/--cache-file" )
local setmainscript = makesetter( "main script", "scriptname", "-s/--script" )
local setshebang = makesetter( "shebang line", "shebang", "-S/--shebang" )
local setprefixfile = makesetter( "prefix file", "prefixfile", "-p/--prefix" )
local function addignorepattern( v )
if v then
if not pcall( string.match, "", v ) then
warn( "Invalid Lua pattern: '"..v.."'" )
else
options.ignorepatterns[ #options.ignorepatterns+1 ] = v
end
else
warn( "Missing argument for -i/--ignore option!" )
end
end
local function addtransformation( v )
if v then
local transform = "amalg."..v..".transform"
require( transform )
if not pluginalreadyadded[ v ] then
options.plugins[ #options.plugins+1 ] = { transform }
pluginalreadyadded[ v ] = true
end
else
warn( "Missing argument for -t/--transform option!" )
end
end
local function addcompression( v )
if v then
local deflate = "amalg."..v..".deflate"
local inflate = "amalg."..v..".inflate"
require( deflate )
require( inflate )
if not pluginalreadyadded[ v ] then
options.plugins[ #options.plugins+1 ] = { deflate, inflate }
pluginalreadyadded[ v ] = true
end
else
warn( "Missing argument for -z/--zip option!" )
end
end
local function addvirtualioresource( v )
if v then
options.virtualresources[ #options.virtualresources+1 ] = v
else
warn( "Missing argument for -v/--virtual-io option!" )
end
end
local i, n = 1, select( '#', ... )
while i <= n do
local a = select( i, ... )
if a == "--" then
for j = i+1, n do
options.modules[ select( j, ... ) ] = true
end
break
elseif a == "-h" or a == "--help" then
i = i + 1
options.showhelp = true
elseif a == "-o" or a == "--output" then
i = i + 1
setoutputname( i <= n and select( i, ... ) )
elseif a == "-p" or a == "--prefix" then
i = i + 1
setprefixfile( i <= n and select( i, ... ) )
elseif a == "-s" or a == "--script" then
i = i + 1
setmainscript( i <= n and select( i, ... ) )
elseif a == "-S" or a == "--shebang" then
i = i + 1
setshebang( i <= n and select( i, ... ) )
elseif a == "-i" or a == "--ignore" then
i = i + 1
addignorepattern( i <= n and select( i, ... ) )
elseif a == "-t" or a == "--transform" then
i = i + 1
addtransformation( i <= n and select( i, ... ) )
elseif a == "-z" or a == "--zip" then
i = i + 1
addcompression( i <= n and select( i, ... ) )
elseif a == "-v" or a == "--virtual-io" then
i = i + 1
addvirtualioresource( i <= n and select( i, ... ) )
elseif a == "-f" or a == "--fallback" then
options.packagefieldname = "postload"
elseif a == "-c" or a == "--use-cache" then
options.usecache = true
elseif a == "-C" or a == "--cache-file" then
options.usecache = true
i = i + 1
setcachefilename( i <= n and select( i, ... ) )
elseif a == "-x" or a == "--c-libs" then
options.embedcmodules = true
elseif a == "-d" or a == "--debug" then
options.debugmode = true
elseif a == "-a" or a == "--no-argfix" then
options.argfix = false
else
local prefix = a:sub( 1, 2 )
if prefix == "-o" then
setoutputname( a:sub( 3 ) )
elseif prefix == "-p" then
setprefixfile( a:sub( 3 ) )
elseif prefix == "-s" then
setmainscript( a:sub( 3 ) )
elseif prefix == "-S" then
setshebang( a:sub( 3 ) )
elseif prefix == "-i" then
addignorepattern( a:sub( 3 ) )
elseif prefix == "-t" then
addtransformation( a:sub( 3 ) )
elseif prefix == "-z" then
addcompression( a:sub( 3 ) )
elseif prefix == "-v" then
addvirtualioresource( a:sub( 3 ) )
elseif prefix == "-C" then
options.usecache = true
setcachefilename( a:sub( 3 ) )
elseif a:sub( 1, 1 ) == "-" then
local option, value = a:match( "^(%-%-[%w%-]+)=(.*)$" )
if option == "--output" then
setoutputname( value )
elseif option == "--prefix" then
setprefixfile( value )
elseif option == "--script" then
setmainscript( value )
elseif option == "--shebang" then
setshebang( value )
elseif option == "--ignore" then
addignorepattern( value )
elseif option == "--transform" then
addtransformation( value )
elseif option == "--zip" then
addcompression( value )
elseif option == "--virtual-io" then
addvirtualioresource( value )
elseif option == "--cache-file" then
options.usecache = true
setcachefilename( value )
else
warn( "Unknown/invalid command line flag: "..a )
end
else
options.modules[ a ] = true
end
end
i = i + 1
end
return options
end
The approach for embedding precompiled Lua files is different from
the normal way of pasting the source code, so this function detects
whether a file is a binary file (Lua bytecode starts with the ESC
character):
local function isbytecode( path )
local file, result = io.open( path, "rb" ), false
if file then
result = file:read( 1 ) == "\027"
file:close()
end
return result
end
The readfile
funciton reads the whole contents of a file into
memory without any processing.
local function readfile( path, isbinary )
local file = assert( io.open( path, isbinary and "rb" or "r" ) )
local data = assert( file:read( "*a" ) )
file:close()
return data
end
Lua files to be embedded into the resulting amalgamation are read
into memory in a single go, because under some circumstances (e.g.
binary chunks, shebang lines, -d
command line flag) some
preprocessing/escaping is necessary. This function reads a whole
Lua file and returns the contents as a Lua string. If there are
compression/transformation plugins specified, the deflate parts of
those plugins are executed on the file contents in the given order.
local function readluafile( path, plugins, stdinallowed )
local isbinary, bytes
if stdinallowed and path == "-" then
bytes = assert( io.read( "*a" ) )
isbinary = bytes:sub( 1, 1 ) == "\027"
path = "<stdin>"
else
isbinary = isbytecode( path )
bytes = readfile( path, isbinary )
end
local shebang
if not isbinary then
Shebang lines are only supported by Lua at the very beginning of a source file, so they have to be removed before the source code can be embedded in the output. A byte-order-marker is removed as well if present.
bytes = bytes:gsub( "^\239\187\191", "" )
shebang = bytes:match( "^(#[^\n]*)" )
bytes = bytes:gsub( "^#[^\n]*", "" )
end
for _, pluginspec in ipairs( plugins ) do
local r, b = require( pluginspec[ 1 ] )( bytes, not isbinary, path )
bytes, isbinary = r, (isbinary or not b)
end
return bytes, isbinary, shebang
end
C extension modules and virtual resources may be embedded into the amalgamated script as well. Compression/decompression plugins are applied, transformation plugins are skipped because transformation plugins usually expect and produce Lua source code.
local function readbinfile( path, plugins )
local bytes = readfile( path, true )
for _, pluginspec in ipairs( plugins ) do
if pluginspec[ 2 ] then
bytes = require( pluginspec[ 1 ] )( bytes, false, path )
end
end
return bytes
end
Lua 5.1’s string.format("%q")
doesn’t convert all control
characters to decimal escape sequences like the newer Lua versions
do. This might cause problems on some platforms (i.e. Windows) when
loading a Lua script (opened in text mode) that contains binary
code.
local function qformat( code )
local s = ("%q"):format( code )
return (s:gsub( "(%c)(%d?)", function( c, d )
if c ~= "\n" then
return (d~="" and "\\%03d" or "\\%d"):format( c:byte() )..d
end
end ))
end
When the -c
command line flag is given, the contents of the cache
file amalg.cache
are used to specify the modules to embed. This
function is used to load the cache file. <filename>
is optional:
local function readcache( filename )
local chunk = loadfile( filename or CACHEFILENAME, "t", {} )
if chunk then
if setfenv then setfenv( chunk, {} ) end
local result = chunk()
if type( result ) == "table" then
return result
end
end
end
When loaded as a module, amalg.lua
collects Lua modules and C
modules that are require
d and updates the cache file
amalg.cache
. This function saves the updated cache contents to
the file:
local function writecache( cache )
local file = assert( io.open( CACHEFILENAME, "w" ) )
file:write( "return {\n" )
if type( cache[ 1 ] ) == "string" then
file:write( " ", qformat( cache[ 1 ] ), ",\n" )
end
for k, v in pairs( cache ) do
if type( k ) == "string" and type( v ) == "string" then
file:write( " [ ", qformat( k ), " ] = ", qformat( v ), ",\n" )
end
end
file:write( "}\n" )
file:close()
end
The standard Lua function package.searchpath
available in Lua 5.2
and up is used to locate the source files for Lua modules and
library files for C modules. For Lua 5.1 a backport is provided.
local searchpath = package.searchpath
if not searchpath then
local delimiter = package.config:match( "^(.-)\n" ):gsub( "%%", "%%%%" )
function searchpath( name, path )
local pname = name:gsub( "%.", delimiter ):gsub( "%%", "%%%%" )
local messages = {}
for subpath in path:gmatch( "[^;]+" ) do
local fpath = subpath:gsub( "%?", pname )
local file = io.open( fpath, "r" )
if file then
file:close()
return fpath
end
messages[ #messages+1 ] = "\n\tno file '"..fpath.."'"
end
return nil, table.concat( messages )
end
end
Every active plugin’s inflate part is called on the code in the reverse
order the deflate parts were executed on the input files. The closing
parentheses are not included in the resulting string. The
closeinflatecalls
function below is responsible for those.
local function openinflatecalls( plugins )
local s = ""
for _, pluginspec in ipairs( plugins ) do
if pluginspec[ 2 ] then
s = s.." require( "..qformat( pluginspec[ 2 ] ).." )("
end
end
return s
end
The closing parentheses needed by the result of the
openinflatecalls
function above is generated by this function.
local function closeinflatecalls( plugins )
local count = 0
for _, pluginspec in ipairs( plugins ) do
if pluginspec[ 2 ] then count = count + 1 end
end
return (" )"):rep( count )
end
Lua modules are written to the output file in a format that can be loaded by the Lua interpreter.
local function writeluamodule( out, modulename, path, plugins,
packagefieldname, debugmode, argfix )
local bytes, isbinary = readluafile( path, plugins )
if isbinary or debugmode then
Precompiled Lua modules are loaded via the standard Lua
function load
(or loadstring
in Lua 5.1). Since this
preserves file name and line number information, this
approach is used for all files if the debug mode is active
(-d
command line option). This is also necessary if
decompression steps need to happen or if the final
transformation plugin produces Lua byte-code.
out:write( "package.", packagefieldname, "[ ", qformat( modulename ),
" ] = assert( (loadstring or load)(",
openinflatecalls( plugins ), " ",
qformat( bytes ), closeinflatecalls( plugins ),
", '@'..", qformat( path ), " ) )\n\n" )
else
Under normal circumstances Lua files are pasted into a
new anonymous vararg function, which then is put into
package.preload
so that require
can find it. Each
function gets its own _ENV
upvalue (on Lua 5.2+), and
special care is taken that _ENV
always is the first
upvalue (important for the module
function on Lua 5.2).
Lua 5.1 compiled with LUA_COMPAT_VARARG
(the default) will
create a local arg
variable to emulate the vararg handling
of Lua 5.0. This might interfere with Lua modules that access
command line arguments via the arg
global. As a workaround
amalg.lua
adds a local alias to the global arg
table
unless the -a
command line flag is specified.
out:write( "do\nlocal _ENV = _ENV\n",
"package.", packagefieldname, "[ ", qformat( modulename ),
" ] = function( ... ) ",
argfix and "local arg = _G.arg;\n" or "_ENV = _ENV;\n",
bytes:gsub( "%s*$", "" ), "\nend\nend\n\n" )
end
end
This is the main function for the use case where amalg.lua
is run
as a script. It parses the command line, creates the output files,
collects the module and script sources, and writes the amalgamated
source.
local function amalgamate( ... )
local options = parsecommandline( ... )
local errors = {}
if options.showhelp then
print( ([[%s <options> [--] <modules...>
available options:
-a, --no-argfix: disable `arg` fix
-c, --use-cache: take module names from `%s` cache file
-C <file>, --cache-file=<file>: take module names from <file>
-d, --debug: preserve file names and line numbers
-f, --fallback: use embedded modules as fallback only
-h, --help: print help/usage
-i <pattern>, --ignore=<pattern>: ignore matching modules from
cache (can be specified multiple times)
-o <file>, --output=<file>: write output to <file>
-p <file>, --prefix=<file>: add the file contents as prefix
(very early) in the amalgamation
-s <file>, --script=<file>: embed <file> as main script
-S <shebang>, --shebang=<shebang>: specify shebang line to use
-t <plugin>, --transform=<plugin>: use transformation plugin
(can be specified multiple times)
-v <file>, --virtual-io=<file>: store <file> in amalgamation
(can be specified multiple times)
-x, --c-libs: also embed C modules
-z <plugin>, --zip=<plugin>: use (de-)compression plugin
(can be specified multiple times)
]]):format( PROGRAMNAME, CACHEFILENAME ) )
return
end
When instructed to on the command line, the cache file is loaded,
and the modules are added to the ones listed on the command line
unless they are ignored via the -i
command line option.
if options.usecache then
local cache = readcache( options.cachefile )
for k, v in pairs( cache or {} ) do
local addmodule = true
if type( k ) == "string" then
for _, pattern in ipairs( options.ignorepatterns ) do
if k:match( pattern ) then
addmodule = false
break
end
end
else
addmodule = false
if k == 1 and options.scriptname == nil then
options.scriptname = v
end
end
if addmodule then
options.modules[ k ] = v
end
end
end
local out = io.stdout
if options.outputname and options.outputname ~= "-" then
out = assert( io.open( options.outputname, "w" ) )
end
If a main script is to be embedded, this includes the same shebang line that was used in the main script, so that the resulting amalgamation can be run without explicitly specifying the interpreter on unixoid systems (if a shebang line was specified in the first place, that is). However, a shebang line specifed via command line options takes precedence!
local scriptbytes, scriptisbinary, shebang
if options.scriptname and options.scriptname ~= "" then
scriptbytes, scriptisbinary, shebang = readluafile( options.scriptname,
options.plugins, true )
if options.shebang then
if options.shebang:match( "^#!" ) then
shebang = options.shebang
elseif options.shebang:match( "^%s*$" ) then
shebang = nil
else
shebang = "#!"..options.shebang
end
end
if shebang then
out:write( shebang, "\n\n" )
end
end
The -p
command line switch allows to embed Lua code into the
amalgamation right after the shebang line. This can be used to
provide stubs for the standard package
module required for
the amalgamated script to work correctly in case the Lua
implementation does not provide a sufficient package
module
implementation on its own. This is sometimes the case when Lua
is embedded into host programs (e.g. Redis, WoW, etc.). The bits
of the package
module that are necessary depend on the command
line switches given, but you will need at least package.preload
and a require
function that uses it.
if options.prefixfile then
out:write( readfile( options.prefixfile ), "\n" )
end
If fallback loading is requested, the module loaders of the
amalgamated modules are registered in table package.postload
,
and an extra searcher function is added at the end of
package.searchers
.
if options.packagefieldname == "postload" then
out:write( [=[
do
local assert = assert
local type = assert( type )
local searchers = package.searchers or package.loaders
local postload = {}
package.postload = postload
searchers[ #searchers+1 ] = function( mod )
assert( type( mod ) == "string", "module name must be a string" )
local loader = postload[ mod ]
if loader == nil then
return "\n\tno field package.postload['"..mod.."']"
else
return loader
end
end
end
]=] )
end
The inflate parts of every compression plugin must be included into the output. Later plugins can be compressed by plugins that have already been processed.
local activeplugins = {}
for _, pluginspec in ipairs( options.plugins ) do
if pluginspec[ 2 ] then
local path, message = searchpath( pluginspec[ 2 ], package.path )
if not path then
error( "module `"..pluginspec[ 2 ].."' not found:"..NOTFOUNDPREFIX..message )
end
writeluamodule( out, pluginspec[ 2 ], path, activeplugins, "preload" )
end
activeplugins[ #activeplugins+1 ] = pluginspec
end
Sorts modules alphabetically. Modules will be embedded in alphabetical order. This ensures deterministic output.
local modulenames = {}
for modulename in pairs( options.modules ) do
modulenames[ #modulenames+1 ] = modulename
end
table.sort( modulenames )
Every module given on the command line and/or in the cache file is processed.
for _, modulename in ipairs( modulenames ) do
local moduletype = options.modules[ modulename ]
Only Lua modules are handled for now, so modules that are definitely C modules are skipped and handled later.
if moduletype ~= "C" then
local path, message = searchpath( modulename, package.path )
if not path and (moduletype == "L" or not options.embedcmodules) then
The module is supposed to be a Lua module, but it cannot be found, so an error is raised.
error( "module `"..modulename.."' not found:"..NOTFOUNDPREFIX..message )
elseif not path then
Module possibly is a C module, so it is tried again later. But the current error message is saved in case the given name isn’t a C module either.
options.modules[ modulename ], errors[ modulename ] = "C", NOTFOUNDPREFIX..message
else
writeluamodule( out, modulename, path, options.plugins,
options.packagefieldname, options.debugmode,
options.argfix )
end
end
end
If the -x
command line flag is active, C modules are embedded
as strings, and written out to temporary files on demand by the
amalgamated code.
if options.embedcmodules then
local dllembedded = {}
The amalgamation of C modules is split into two parts:
One part generates a temporary file name for the C library
and writes the binary code stored in the amalgamation to
that file, while the second loads the resulting dynamic
library using package.loadlib
. The split is necessary
because multiple modules could be loaded from the same
library, and the amalgamated code has to simulate that.
Shared dynamic libraries are embedded and extracted only once.
To make the loading of C modules more robust, the necessary
global functions are saved in upvalues (because user-supplied
code might be run before a C module is loaded). The upvalues
are local to a do ... end
block, so they aren’t visible in
the main script code.
On Windows the result of os.tmpname()
is not an absolute
path by default. If that’s the case the value of the TMP
environment variable is prepended to make it absolute.
The temporary dynamic library files may or may not be
cleaned up when the amalgamated code exits (this probably
works on POSIX machines (all Lua versions) and on Windows
with Lua 5.1). The reason is that starting with version 5.2
Lua ensures that libraries aren’t unloaded before normal
user-supplied __gc
metamethods have run to avoid a case
where such a metamethod would call an unloaded C function.
As a consequence the amalgamated code tries to remove the
temporary library files before they are actually unloaded.
local prefix = [=[
do
local assert = assert
local os_remove = assert( os.remove )
local package_loadlib = assert( package.loadlib )
local dlls = {}
local function temporarydll( code )
local tmpname = assert( os.tmpname() )
if package.config:match( "^([^\n]+)" ) == "\\" then
if not tmpname:match( "[\\/][^\\/]+[\\/]" ) then
local tmpdir = assert( os.getenv( "TMP" ) or os.getenv( "TEMP" ),
"could not detect temp directory" )
local first = tmpname:sub( 1, 1 )
local hassep = first == "\\" or first == "/"
tmpname = tmpdir..((hassep) and "" or "\\")..tmpname
end
end
local f = assert( io.open( tmpname, "wb" ) )
assert( f:write( code ) )
f:close()
local sentinel = newproxy and newproxy( true )
or setmetatable( {}, { __gc = true } )
getmetatable( sentinel ).__gc = function() os_remove( tmpname ) end
return { tmpname, sentinel }
end
]=]
for _, modulename in ipairs( modulenames ) do
local moduletype = options.modules[ modulename ]
if moduletype == "C" then
Try a search strategy similar to the standard C module searcher first and then the all-in-one strategy to locate the library files for the C modules to embed.
local path, message = searchpath( modulename, package.cpath )
if not path then
errors[ modulename ] = (errors[ modulename ] or "")..NOTFOUNDPREFIX..message
path, message = searchpath( modulename:gsub( "%..*$", "" ), package.cpath )
if not path then
error( "module `"..modulename.."' not found:"..
errors[ modulename ]..NOTFOUNDPREFIX..message )
end
end
local qpath = qformat( path )
Builds the symbol(s) to look for in the dynamic library. There may be multiple candidates because of optional version information in the module names and the different approaches of the different Lua versions in handling that.
local openf = modulename:gsub( "%.", "_" )
local openf1, openf2 = openf:match( "^([^%-]*)%-(.*)$" )
if not dllembedded[ path ] then
local code = readbinfile( path, options.plugins )
dllembedded[ path ] = true
local qcode = qformat( code )
The temporarydll
function saves the embedded binary
code into a temporary file for later loading.
out:write( prefix, "\ndlls[ ", qpath, " ] = temporarydll(",
openinflatecalls( options.plugins ), " ", qcode,
closeinflatecalls( options.plugins ), " )\n" )
prefix = ""
end -- shared libary not embedded already
Adds a function to package.preload
to load the temporary
DLL or shared object file. This function tries to mimic the
behavior of Lua 5.3 which is to strip version information
from the module name at the end first, and then at the
beginning if that failed.
local qm = qformat( modulename )
out:write( "\npackage.", options.packagefieldname, "[ ", qm,
" ] = function()\n local dll = dlls[ ", qpath,
" ][ 1 ]\n" )
if openf1 then
out:write( " local loader = package_loadlib( dll, ",
qformat( "luaopen_"..openf1 ), " )\n",
" if not loader then\n",
" loader = assert( package_loadlib( dll, ",
qformat( "luaopen_"..openf2 ), " ) )\n end\n" )
else
out:write( " local loader = assert( package_loadlib( dll, ",
qformat( "luaopen_"..openf ), " ) )\n" )
end
out:write( " return loader( ", qm, ", dll )\nend\n" )
end -- is a C module
end -- for all given module names
if prefix == "" then
out:write( "end\n\n" )
end
end -- if embedcmodules
Virtual resources are embedded like dlls, and the Lua standard
io functions are monkey-patched to search for embedded files
first. The amalgamated script includes a complete implementation
of file io that works on strings embedded in the amalgamation if
(and only if) the file is opened in read-only mode.
To reduce the size of the embedded code, error handling is mostly
left out (since the resources are static, you can make sure that
no errors occur). Also, emulating the IO library for four
different Lua versions on many different architectures and OSes
is very challenging. Therefore, there might be corner cases
where the virtual IO functions behave slightly differently than
the native IO functions. This applies in particular to the "*n"
format for read
or lines
.
In addition to file IO functions and methods, loadfile
and
dofile
are patched as well.
if #options.virtualresources > 0 then
out:write( [=[
do
local vfile = {}
local vfile_mt = { __index = vfile }
local assert = assert
local select = assert( select )
local setmetatable = assert( setmetatable )
local tonumber = assert( tonumber )
local type = assert( type )
local table_unpack = assert( unpack or table.unpack )
local io_open = assert( io.open )
local io_lines = assert( io.lines )
local _loadfile = assert( loadfile )
local _dofile = assert( dofile )
local virtual = {}
function io.open( path, mode )
if (mode == "r" or mode == "rb") and virtual[ path ] then
return setmetatable( { offset=0, data=virtual[ path ] }, vfile_mt )
else
return io_open( path, mode )
end
end
function io.lines( path, ... )
if virtual[ path ] then
return setmetatable( { offset=0, data=virtual[ path ] }, vfile_mt ):lines( ... )
else
return io_lines( path, ... )
end
end
function loadfile( path, ... )
if virtual[ path ] then
local s = virtual[ path ]:gsub( "^%s*#[^\n]*\n", "" )
return (loadstring or load)( s, "@"..path, ... )
else
return _loadfile( path, ... )
end
end
function dofile( path )
if virtual[ path ] then
local s = virtual[ path ]:gsub( "^%s*#[^\n]*\n", "" )
return assert( (loadstring or load)( s, "@"..path ) )()
else
return _dofile( path )
end
end
function vfile:close() return true end
vfile.flush = vfile.close
vfile.setvbuf = vfile.close
function vfile:write() return self end
local function lines_iterator( state )
return state.file:read( table_unpack( state, 1, state.n ) )
end
function vfile:lines( ... )
return lines_iterator, { file=self, n=select( '#', ... ), ... }
end
local function _read( self, n, fmt, ... )
if n > 0 then
local o = self.offset
if o >= #self.data then return nil end
if type( fmt ) == "number" then
self.offset = o + fmt
return self.data:sub( o+1, self.offset ), _read( self, n-1, ... )
elseif fmt == "n" or fmt == "*n" then
local p, e, x = self.data:match( "^%s*()%S+()", o+1 )
if p then
o = p - 1
for i = p+1, e-1 do
local newx = tonumber( self.data:sub( p, i ) )
if newx then
x, o = newx, i
elseif i > o+3 then
break
end
end
else
o = #self.data
end
self.offset = o
return x, _read( self, n-1, ... )
elseif fmt == "l" or fmt == "*l" then
local s, p = self.data:match( "^([^\r\n]*)\r?\n?()", o+1 )
self.offset = p-1
return s, _read( self, n-1, ... )
elseif fmt == "L" or fmt == "*L" then
local s, p = self.data:match( "^([^\r\n]*\r?\n?)()", o+1 )
self.offset = p-1
return s, _read( self, n-1, ... )
elseif fmt == "a" or fmt == "*a" then
self.offset = #self.data
return self.data:sub( o+1, self.offset )
end
end
end
function vfile:read( ... )
local n = select( '#', ... )
if n > 0 then
return _read( self, n, ... )
else
return _read( self, 1, "l" )
end
end
function vfile:seek( whence, offset )
whence, offset = whence or "cur", offset or 0
if whence == "set" then
self.offset = offset
elseif whence == "cur" then
self.offset = self.offset + offset
elseif whence == "end" then
self.offset = #self.data + offset
end
return self.offset
end
]=] )
for _, v in ipairs( options.virtualresources ) do
local qdata = qformat( readbinfile( v, options.plugins ) )
out:write( "\nvirtual[ ", qformat( v ), " ] =",
openinflatecalls( options.plugins ), " ", qdata,
closeinflatecalls( options.plugins ), "\n" )
end
out:write( "end\n\n" )
end -- if #options.virtualresources > 0
If a main script is specified on the command line (-s
flag),
embed it now that all dependency modules are available to
require
.
if options.scriptname and options.scriptname ~= "" then
if scriptisbinary or options.debugmode then
if options.scriptname == "-" then
options.scriptname = "<stdin>"
end
out:write( "assert( (loadstring or load)(",
openinflatecalls( options.plugins ), " ",
qformat( scriptbytes ),
closeinflatecalls( options.plugins ),
", '@'..", qformat( options.scriptname ),
" ) )( ... )\n\n" )
else
out:write( scriptbytes )
end
end
if options.outputname and options.outputname ~= "-" then
out:close()
end
end
If amalg.lua
is loaded as a module, it intercepts require
calls
(more specifically calls to the searcher functions) to collect all
require
d module names and store them in the cache. The cache file
amalg.cache
is updated when the program terminates.
local function collect()
local searchers = package.searchers or package.loaders
When the searchers table has been modified, it is unknown which
elements in the table to replace, so amalg.lua
bails out with
an error. The luarocks.loader
module which inserts itself at
position 1 in the package.searchers
table is explicitly
supported, though!
local offset = 0
if package.loaded[ "luarocks.loader" ] then offset = 1 end
assert( #searchers == 4+offset, "package.searchers has been modified" )
local cache = readcache() or {}
The updated cache is written to disk when the following value is
garbage collected, which should happen at lua_close()
.
local sentinel = newproxy and newproxy( true )
or setmetatable( {}, { __gc = true } )
getmetatable( sentinel ).__gc = function()
if type( arg ) == "table" then
cache[ 1 ] = arg[ 0 ]
end
writecache( cache )
end
local luasearcher = searchers[ 2+offset ]
local csearcher = searchers[ 3+offset ]
local aiosearcher = searchers[ 4+offset ] -- all in one searcher
local function addcacheentry( tag, mname, ... )
if type( (...) ) == "function" then
cache[ mname ] = tag
end
return ...
end
The replacement searchers just forward to the original versions, but also update the cache if the search was successful.
searchers[ 2+offset ] = function( ... )
local _ = sentinel -- make sure that sentinel is an upvalue
return addcacheentry( "L", ..., luasearcher( ... ) )
end
searchers[ 3+offset ] = function( ... )
local _ = sentinel -- make sure that sentinel is an upvalue
return addcacheentry( "C", ..., csearcher( ... ) )
end
searchers[ 4+offset ] = function( ... )
local _ = sentinel -- make sure that sentinel is an upvalue
return addcacheentry( "C", ..., aiosearcher( ... ) )
end
Since calling os.exit
might skip the lua_close()
call, the
os.exit
function is monkey-patched to also save the updated
cache to the cache file on disk.
if type( os ) == "table" and type( os.exit ) == "function" then
local type, os_exit = type, os.exit
function os.exit( ... ) -- luacheck: ignore os
if type( _G ) == "table" and type( _G.arg ) == "table" then
cache[ 1 ] = _G.arg[ 0 ]
end
writecache( cache )
return os_exit( ... )
end
end
end
To determine whether amalg.lua
is run as a script or loaded as a
module it uses the debug module to walk the call stack looking for
a require
call. If such a call is found, amalg.lua
has been
require
d as a module.
local function isscript()
local i = 3
local info = debug.getinfo( i, "f" )
while info do
if info.func == require then
return false
end
i = i + 1
info = debug.getinfo( i, "f" )
end
return true
end
This checks whether amalg.lua
has been called as a script or
loaded as a module and acts accordingly, by calling the
corresponding main function:
if isscript() then
amalgamate( ... )
else
collect()
end