Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
lua-amalg/src/amalg.lua
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
executable file
1015 lines (952 sloc)
37.3 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env lua | |
| -- **Amalg** is a Lua tool for bundling a Lua script and dependent | |
| -- Lua modules in a single `.lua` file for easier distribution. | |
| -- | |
| -- ## Implementation | |
| -- | |
| -- 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 <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 | |