Skip to content

Commit 9b32d3e

Browse files
buu700kripken
authored andcommitted
Add SINGLE_FILE option to embed all subresources into emitted JS (#5296)
As discussed in #5279, subresource paths are converted into base64 data URIs.
1 parent f043f19 commit 9b32d3e

12 files changed

+508
-187
lines changed

emcc.py

Lines changed: 117 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,10 @@ def do_minify(self):
277277
self.cleanup_shell = True
278278

279279

280+
def embed_memfile(options):
281+
return shared.Settings.SINGLE_FILE or (shared.Settings.MEM_INIT_METHOD == 0 and (not shared.Settings.MAIN_MODULE and not shared.Settings.SIDE_MODULE and options.debug_level < 4))
282+
283+
280284
#
281285
# Main run() function
282286
#
@@ -801,7 +805,7 @@ def detect_fixed_language_mode(args):
801805
options.separate_asm = True
802806
logging.warning('forcing separate asm output (--separate-asm), because -s PRECISE_F32=2 or -s USE_PTHREADS=2 was passed.')
803807
if options.separate_asm:
804-
shared.Settings.SEPARATE_ASM = os.path.basename(asm_target)
808+
shared.Settings.SEPARATE_ASM = shared.JS.get_subresource_location(asm_target)
805809

806810
if 'EMCC_STRICT' in os.environ:
807811
shared.Settings.STRICT = os.environ.get('EMCC_STRICT') != '0'
@@ -918,6 +922,8 @@ def check(input_file):
918922
logging.warning('disabling closure because debug info was requested')
919923
options.use_closure_compiler = False
920924

925+
assert not (shared.Settings.EMTERPRETIFY_FILE and shared.Settings.SINGLE_FILE), 'cannot have both EMTERPRETIFY_FILE and SINGLE_FILE enabled at the same time'
926+
921927
assert not (shared.Settings.NO_DYNAMIC_EXECUTION and options.use_closure_compiler), 'cannot have both NO_DYNAMIC_EXECUTION and closure compiler enabled at the same time'
922928

923929
if options.use_closure_compiler:
@@ -1110,10 +1116,16 @@ def check(input_file):
11101116
os.environ['EMCC_WASM_BACKEND_BINARYEN'] = '1'
11111117

11121118
if shared.Settings.BINARYEN:
1113-
# set file locations, so that JS glue can find what it needs
1114-
shared.Settings.WASM_TEXT_FILE = os.path.basename(wasm_text_target)
1115-
shared.Settings.WASM_BINARY_FILE = os.path.basename(wasm_binary_target)
1116-
shared.Settings.ASMJS_CODE_FILE = os.path.basename(asm_target)
1119+
if shared.Settings.SINGLE_FILE:
1120+
# placeholder strings for JS glue, to be replaced with subresource locations in do_binaryen
1121+
shared.Settings.WASM_TEXT_FILE = shared.FilenameReplacementStrings.WASM_TEXT_FILE
1122+
shared.Settings.WASM_BINARY_FILE = shared.FilenameReplacementStrings.WASM_BINARY_FILE
1123+
shared.Settings.ASMJS_CODE_FILE = shared.FilenameReplacementStrings.ASMJS_CODE_FILE
1124+
else:
1125+
# set file locations, so that JS glue can find what it needs
1126+
shared.Settings.WASM_TEXT_FILE = shared.JS.get_subresource_location(wasm_text_target)
1127+
shared.Settings.WASM_BINARY_FILE = shared.JS.get_subresource_location(wasm_binary_target)
1128+
shared.Settings.ASMJS_CODE_FILE = shared.JS.get_subresource_location(asm_target)
11171129

11181130
shared.Settings.ASM_JS = 2 # when targeting wasm, we use a wasm Memory, but that is not compatible with asm.js opts
11191131
shared.Settings.GLOBAL_BASE = 1024 # leave some room for mapping global vars
@@ -1538,6 +1550,10 @@ def get_final():
15381550
shared.Settings.MEM_INIT_METHOD = 1
15391551
else:
15401552
assert shared.Settings.MEM_INIT_METHOD != 1
1553+
1554+
if embed_memfile(options):
1555+
shared.Settings.SUPPORT_BASE64_EMBEDDING = 1
1556+
15411557
final = shared.Building.emscripten(final, append_ext=False, extra_args=extra_args)
15421558
if DEBUG: save_intermediate('original')
15431559

@@ -1625,7 +1641,8 @@ def get_final():
16251641

16261642
with ToolchainProfiler.profile_block('memory initializer'):
16271643
memfile = None
1628-
if shared.Settings.MEM_INIT_METHOD > 0:
1644+
1645+
if shared.Settings.MEM_INIT_METHOD > 0 or embed_memfile(options):
16291646
memfile = target + '.mem'
16301647
shared.try_delete(memfile)
16311648
def repl(m):
@@ -1636,20 +1653,17 @@ def repl(m):
16361653
while membytes and membytes[-1] == 0:
16371654
membytes.pop()
16381655
if not membytes: return ''
1639-
if not options.memory_init_file:
1656+
if shared.Settings.MEM_INIT_METHOD == 2:
16401657
# memory initializer in a string literal
16411658
return "memoryInitializer = '%s';" % shared.JS.generate_string_initializer(list(membytes))
16421659
open(memfile, 'wb').write(''.join(map(chr, membytes)))
16431660
if DEBUG:
16441661
# Copy into temp dir as well, so can be run there too
16451662
shared.safe_copy(memfile, os.path.join(shared.get_emscripten_temp_dir(), os.path.basename(memfile)))
1646-
if not shared.Settings.BINARYEN:
1647-
return 'memoryInitializer = "%s";' % os.path.basename(memfile)
1663+
if not shared.Settings.BINARYEN or 'asmjs' in shared.Settings.BINARYEN_METHOD or 'interpret-asm2wasm' in shared.Settings.BINARYEN_METHOD:
1664+
return 'memoryInitializer = "%s";' % shared.JS.get_subresource_location(memfile, embed_memfile(options))
16481665
else:
1649-
# with wasm, we may have the mem init file in the wasm binary already
1650-
return ('memoryInitializer = Module["wasmJSMethod"].indexOf("asmjs") >= 0 || '
1651-
'Module["wasmJSMethod"].indexOf("interpret-asm2wasm") >= 0 ? "%s" : null;'
1652-
% os.path.basename(memfile))
1666+
return 'memoryInitializer = null;'
16531667
src = re.sub(shared.JS.memory_initializer_pattern, repl, open(final).read(), count=1)
16541668
open(final + '.mem.js', 'w').write(src)
16551669
final += '.mem.js'
@@ -1661,15 +1675,6 @@ def repl(m):
16611675
logging.debug('wrote memory initialization to %s', memfile)
16621676
else:
16631677
logging.debug('did not see memory initialization')
1664-
elif not shared.Settings.MAIN_MODULE and not shared.Settings.SIDE_MODULE and options.debug_level < 4:
1665-
# not writing a binary init, but we can at least optimize them by splitting them up
1666-
src = open(final).read()
1667-
src = shared.JS.optimize_initializer(src)
1668-
if src is not None:
1669-
logging.debug('optimizing memory initialization')
1670-
open(final + '.mem.js', 'w').write(src)
1671-
final += '.mem.js'
1672-
src = None
16731678

16741679
if shared.Settings.USE_PTHREADS:
16751680
target_dir = os.path.dirname(os.path.abspath(target))
@@ -1829,6 +1834,9 @@ def get_eliminate():
18291834
if options.proxy_to_worker:
18301835
generate_worker_js(target, js_target, target_basename)
18311836

1837+
if embed_memfile(options):
1838+
shared.try_delete(memfile)
1839+
18321840
for f in generated_text_files_with_native_eols:
18331841
tools.line_endings.convert_line_endings_in_file(f, os.linesep, options.output_eol)
18341842
log_time('final emitting')
@@ -2352,6 +2360,24 @@ def do_binaryen(final, target, asm_target, options, memfile, wasm_binary_target,
23522360
passes.append('minifyWhitespace')
23532361
final = shared.Building.js_optimizer_no_asmjs(final, passes)
23542362
if DEBUG: save_intermediate('postclean', 'js')
2363+
# replace placeholder strings with correct subresource locations
2364+
if shared.Settings.SINGLE_FILE:
2365+
f = open(final, 'rb')
2366+
js = f.read()
2367+
f.close()
2368+
f = open(final, 'wb')
2369+
for target, replacement_string, should_embed in [
2370+
(wasm_text_target, shared.FilenameReplacementStrings.WASM_TEXT_FILE, True),
2371+
(wasm_binary_target, shared.FilenameReplacementStrings.WASM_BINARY_FILE, True),
2372+
(asm_target, shared.FilenameReplacementStrings.ASMJS_CODE_FILE, not shared.Building.is_wasm_only())
2373+
]:
2374+
if should_embed and os.path.isfile(target):
2375+
js = js.replace(replacement_string, shared.JS.get_subresource_location(target))
2376+
else:
2377+
js = js.replace(replacement_string, '')
2378+
shared.try_delete(target)
2379+
f.write(js)
2380+
f.close()
23552381
return final
23562382

23572383

@@ -2399,11 +2425,17 @@ def generate_html(target, options, js_target, target_basename,
23992425
} else {
24002426
// note: no support for code mods (PRECISE_F32==2)
24012427
console.log('running code on the main thread');
2428+
var filename = '%s';
2429+
var fileBytes = tryParseAsDataURI(filename);
24022430
var script = document.createElement('script');
2403-
script.src = "%s.js";
2431+
if (fileBytes) {
2432+
script.innerHTML = intArrayToString(fileBytes);
2433+
} else {
2434+
script.src = filename;
2435+
}
24042436
document.body.appendChild(script);
24052437
}
2406-
''' % proxy_worker_filename
2438+
''' % shared.JS.get_subresource_location(proxy_worker_filename + '.js')
24072439
else:
24082440
# Normal code generation path
24092441
script.src = base_js_target
@@ -2417,33 +2449,40 @@ def generate_html(target, options, js_target, target_basename,
24172449
# We need to load the emterpreter file before anything else, it has to be synchronously ready
24182450
script.un_src()
24192451
script.inline = '''
2452+
var emterpretURL = '%s';
24202453
var emterpretXHR = new XMLHttpRequest();
2421-
emterpretXHR.open('GET', '%s', true);
2454+
emterpretXHR.open('GET', emterpretURL, true);
24222455
emterpretXHR.responseType = 'arraybuffer';
24232456
emterpretXHR.onload = function() {
2424-
Module.emterpreterFile = emterpretXHR.response;
2457+
if (emterpretXHR.status === 200 || emterpretXHR.status === 0) {
2458+
Module.emterpreterFile = emterpretXHR.response;
2459+
} else {
2460+
var emterpretURLBytes = tryParseAsDataURI(emterpretURL);
2461+
if (emterpretURLBytes) {
2462+
Module.emterpreterFile = emterpretURLBytes.buffer;
2463+
}
2464+
}
24252465
%s
24262466
};
24272467
emterpretXHR.send(null);
2428-
''' % (shared.Settings.EMTERPRETIFY_FILE, script.inline)
2468+
''' % (shared.JS.get_subresource_location(shared.Settings.EMTERPRETIFY_FILE), script.inline)
24292469

24302470
if options.memory_init_file:
24312471
# start to load the memory init file in the HTML, in parallel with the JS
24322472
script.un_src()
24332473
script.inline = ('''
2434-
(function() {
2435-
var memoryInitializer = '%s';
2436-
if (typeof Module['locateFile'] === 'function') {
2437-
memoryInitializer = Module['locateFile'](memoryInitializer);
2438-
} else if (Module['memoryInitializerPrefixURL']) {
2439-
memoryInitializer = Module['memoryInitializerPrefixURL'] + memoryInitializer;
2440-
}
2441-
var meminitXHR = Module['memoryInitializerRequest'] = new XMLHttpRequest();
2442-
meminitXHR.open('GET', memoryInitializer, true);
2443-
meminitXHR.responseType = 'arraybuffer';
2444-
meminitXHR.send(null);
2445-
})();
2446-
''' % os.path.basename(memfile)) + script.inline
2474+
var memoryInitializer = '%s';
2475+
if (typeof Module['locateFile'] === 'function') {
2476+
memoryInitializer = Module['locateFile'](memoryInitializer);
2477+
} else if (Module['memoryInitializerPrefixURL']) {
2478+
memoryInitializer = Module['memoryInitializerPrefixURL'] + memoryInitializer;
2479+
}
2480+
Module['memoryInitializerRequestURL'] = memoryInitializer;
2481+
var meminitXHR = Module['memoryInitializerRequest'] = new XMLHttpRequest();
2482+
meminitXHR.open('GET', memoryInitializer, true);
2483+
meminitXHR.responseType = 'arraybuffer';
2484+
meminitXHR.send(null);
2485+
''' % shared.JS.get_subresource_location(memfile)) + script.inline
24472486

24482487
# Download .asm.js if --separate-asm was passed in an asm.js build, or if 'asmjs' is one
24492488
# of the wasm run methods.
@@ -2454,22 +2493,37 @@ def generate_html(target, options, js_target, target_basename,
24542493
if len(asm_mods) == 0:
24552494
# just load the asm, then load the rest
24562495
script.inline = '''
2496+
var filename = '%s';
2497+
var fileBytes = tryParseAsDataURI(filename);
24572498
var script = document.createElement('script');
2458-
script.src = "%s";
2499+
if (fileBytes) {
2500+
script.innerHTML = intArrayToString(fileBytes);
2501+
} else {
2502+
script.src = filename;
2503+
}
24592504
script.onload = function() {
24602505
setTimeout(function() {
24612506
%s
24622507
}, 1); // delaying even 1ms is enough to allow compilation memory to be reclaimed
24632508
};
24642509
document.body.appendChild(script);
2465-
''' % (os.path.basename(asm_target), script.inline)
2510+
''' % (shared.JS.get_subresource_location(asm_target), script.inline)
24662511
else:
24672512
# may need to modify the asm code, load it as text, modify, and load asynchronously
24682513
script.inline = '''
2514+
var codeURL = '%s';
24692515
var codeXHR = new XMLHttpRequest();
2470-
codeXHR.open('GET', '%s', true);
2516+
codeXHR.open('GET', codeURL, true);
24712517
codeXHR.onload = function() {
2472-
var code = codeXHR.responseText;
2518+
var code;
2519+
if (codeXHR.status === 200 || codeXHR.status === 0) {
2520+
code = codeXHR.responseText;
2521+
} else {
2522+
var codeURLBytes = tryParseAsDataURI(codeURL);
2523+
if (codeURLBytes) {
2524+
code = intArrayToString(codeURLBytes);
2525+
}
2526+
}
24732527
%s
24742528
var blob = new Blob([code], { type: 'text/javascript' });
24752529
codeXHR = null;
@@ -2485,21 +2539,36 @@ def generate_html(target, options, js_target, target_basename,
24852539
document.body.appendChild(script);
24862540
};
24872541
codeXHR.send(null);
2488-
''' % (os.path.basename(asm_target), '\n'.join(asm_mods), script.inline)
2542+
''' % (shared.JS.get_subresource_location(asm_target), '\n'.join(asm_mods), script.inline)
24892543

24902544
if shared.Settings.BINARYEN and not shared.Settings.BINARYEN_ASYNC_COMPILATION:
24912545
# We need to load the wasm file before anything else, it has to be synchronously ready TODO: optimize
24922546
script.un_src()
24932547
script.inline = '''
2548+
var wasmURL = '%s';
24942549
var wasmXHR = new XMLHttpRequest();
2495-
wasmXHR.open('GET', '%s', true);
2550+
wasmXHR.open('GET', wasmURL, true);
24962551
wasmXHR.responseType = 'arraybuffer';
24972552
wasmXHR.onload = function() {
2498-
Module.wasmBinary = wasmXHR.response;
2553+
if (wasmXHR.status === 200 || wasmXHR.status === 0) {
2554+
Module.wasmBinary = wasmXHR.response;
2555+
} else {
2556+
var wasmURLBytes = tryParseAsDataURI(wasmURL);
2557+
if (wasmURLBytes) {
2558+
Module.wasmBinary = wasmURLBytes.buffer;
2559+
}
2560+
}
24992561
%s
25002562
};
25012563
wasmXHR.send(null);
2502-
''' % (os.path.basename(wasm_binary_target), script.inline)
2564+
''' % (shared.JS.get_subresource_location(wasm_binary_target), script.inline)
2565+
2566+
# when script.inline isn't empty, add required helper functions such as tryParseAsDataURI
2567+
if script.inline:
2568+
for file in ['src/arrayUtils.js', 'src/base64Utils.js']:
2569+
f = open(shared.path_from_root(file), 'r')
2570+
script.inline = f.read() + script.inline
2571+
f.close()
25032572

25042573
html = open(target, 'wb')
25052574
html_contents = shell.replace('{{{ SCRIPT }}}', script.replacement())

src/arrayUtils.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// All functions here should be maybeExported from jsifier.js
2+
3+
/** @type {function(string, boolean=, number=)} */
4+
function intArrayFromString(stringy, dontAddNull, length) {
5+
var len = length > 0 ? length : lengthBytesUTF8(stringy)+1;
6+
var u8array = new Array(len);
7+
var numBytesWritten = stringToUTF8Array(stringy, u8array, 0, u8array.length);
8+
if (dontAddNull) u8array.length = numBytesWritten;
9+
return u8array;
10+
}
11+
12+
// Temporarily duplicating function pending Python preprocessor support
13+
var ASSERTIONS;
14+
var intArrayToString = ASSERTIONS ?
15+
function (array) {
16+
var ret = [];
17+
for (var i = 0; i < array.length; i++) {
18+
var chr = array[i];
19+
if (chr > 0xFF) {
20+
assert(false, 'Character code ' + chr + ' (' + String.fromCharCode(chr) + ') at offset ' + i + ' not in 0x00-0xFF.');
21+
chr &= 0xFF;
22+
}
23+
ret.push(String.fromCharCode(chr));
24+
}
25+
return ret.join('');
26+
} :
27+
function (array) {
28+
var ret = [];
29+
for (var i = 0; i < array.length; i++) {
30+
var chr = array[i];
31+
if (chr > 0xFF) {
32+
chr &= 0xFF;
33+
}
34+
ret.push(String.fromCharCode(chr));
35+
}
36+
return ret.join('');
37+
}
38+
;

0 commit comments

Comments
 (0)