-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathamalg.lua
1015 lines (952 loc) · 37.3 KB
/
amalg.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/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