#!/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

