aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJez Ng <me@jezng.com>2013-06-06 17:46:13 -0700
committerJez Ng <me@jezng.com>2013-06-19 01:22:01 -0700
commit747f74e1055feaea11222978d46c421ac92602ea (patch)
tree2c153da61e78bb6a6d2ee9731a15787b01b81474
parent86e672a4371e57238fabdde3a35eeee03c8aa77f (diff)
Implement basic source maps. Closes #1252.
-rwxr-xr-xemcc9
-rwxr-xr-xtools/sourcemapper.js118
2 files changed, 126 insertions, 1 deletions
diff --git a/emcc b/emcc
index b8230e95..9c3514f3 100755
--- a/emcc
+++ b/emcc
@@ -49,7 +49,7 @@ emcc can be influenced by a few environment variables:
import os, sys, shutil, tempfile, subprocess, shlex, time, re, logging
from subprocess import PIPE, STDOUT
-from tools import shared
+from tools import shared, jsrun
from tools.shared import Compression, execute, suffix, unsuffixed, unsuffixed_basename
from tools.response_file import read_response_file
@@ -1585,12 +1585,17 @@ try:
else:
logging.debug('did not see memory initialization')
+ def generate_source_maps(filename, mapFileBaseName):
+ jsrun.run_js(shared.path_from_root('tools', 'sourcemapper.js'),
+ shared.NODE_JS, [filename, os.getcwd(), mapFileBaseName])
+
# If we were asked to also generate HTML, do that
if final_suffix == 'html':
logging.debug('generating HTML')
shell = open(shell_path).read()
html = open(target, 'w')
if not Compression.on:
+ if keep_js_debug: generate_source_maps(final, target)
html.write(shell.replace('{{{ SCRIPT_CODE }}}', open(final).read()))
else:
# Compress the main code
@@ -1661,6 +1666,8 @@ try:
from tools.split import split_javascript_file
split_javascript_file(final, unsuffixed(target), split_js_file)
else:
+ if keep_js_debug: generate_source_maps(final, target)
+
# copy final JS to output
shutil.move(final, target)
diff --git a/tools/sourcemapper.js b/tools/sourcemapper.js
new file mode 100755
index 00000000..1f28b371
--- /dev/null
+++ b/tools/sourcemapper.js
@@ -0,0 +1,118 @@
+#! /usr/bin/env node
+
+"use strict";
+
+function countLines(s) {
+ var count = 0;
+ for (var i = 0, l = s.length; i < l; i ++) {
+ var c = s[i];
+ if (c === '\n') count++;
+ }
+ return count;
+}
+
+/*
+ * Extracts the line (not block) comments from the generated function code and
+ * invokes commentHandler with (comment content, line number of comment). This
+ * function implements a simplistic lexer with the following assumptions:
+ * 1. "// EMSCRIPTEN_START_FUNCS" and "// EMSCRIPTEN_END_FUNCS" are unique
+ * markers for separating library code from generated function code. Things
+ * will break if they appear as part of a string in library code (but OK if
+ * they occur in generated code).
+ * 2. Between these two markers, no regexes are used.
+ */
+function extractComments(source, commentHandler) {
+ var state = 'code';
+ var commentContent = '';
+ var functionStartIdx = source.indexOf('// EMSCRIPTEN_START_FUNCS');
+ var functionEndIdx = source.lastIndexOf('// EMSCRIPTEN_END_FUNCS');
+ var lineCount = countLines(source.slice(0, functionStartIdx));
+
+ for (var i = functionStartIdx; i < functionEndIdx; i++) {
+ var c = source[i];
+ var nextC = source[i+1];
+ if (c === '\n') lineCount++;
+ switch (state) {
+ case 'code':
+ if (c === '/') {
+ if (nextC === '/') { state = 'lineComment'; i++; }
+ else if (nextC === '*') { state = 'blockComment'; i++; }
+ }
+ else if (c === '"') state = 'doubleQuotedString';
+ else if (c === '\'') state = 'singleQuotedString';
+ break;
+ case 'lineComment':
+ if (c === '\n') {
+ state = 'code';
+ commentHandler(commentContent, lineCount);
+ commentContent = "";
+ } else {
+ commentContent += c;
+ }
+ break;
+ case 'blockComment':
+ if (c === '*' && nextC === '/') state = 'code';
+ case 'singleQuotedString':
+ if (c === '\\') i++;
+ else if (c === '\'') state = 'code';
+ break;
+ case 'doubleQuotedString':
+ if (c === '\\') i++;
+ else if (c === '"') state = 'code';
+ break;
+ }
+ }
+}
+
+function generateMap(fileName, sourceRoot, mapFileBaseName) {
+ var fs = require('fs');
+ var path = require('path');
+ var SourceMapGenerator = require('source-map').SourceMapGenerator;
+
+ var generator = new SourceMapGenerator({ file: fileName });
+ var generatedSource = fs.readFileSync(fileName, 'utf-8');
+ var seenFiles = Object.create(null);
+
+ extractComments(generatedSource, function(content, generatedLineNumber) {
+ console.log(content, generatedLineNumber)
+ var matches = /@line (\d+) "([^"]*)"/.exec(content);
+ if (matches === null) return;
+ var originalLineNumber = parseInt(matches[1], 10);
+ var originalFileName = matches[2];
+
+ if (!(originalFileName in seenFiles)) {
+ seenFiles[originalFileName] = true;
+ try {
+ generator.setSourceContent(originalFileName,
+ fs.readFileSync(sourceRoot + "/" + originalFileName));
+ } catch (e) {
+ console.warn("Unable to find original file for " + originalFileName);
+ }
+ }
+
+ generator.addMapping({
+ generated: { line: generatedLineNumber, column: 0 },
+ original: { line: originalLineNumber, column: 0 },
+ source: originalFileName
+ });
+ });
+
+ var mapFileName = mapFileBaseName + '.map';
+ fs.writeFileSync(mapFileName, generator.toString());
+
+ var lastLine = generatedSource.slice(generatedSource.lastIndexOf('\n'));
+ if (!/sourceMappingURL/.test(lastLine))
+ fs.appendFileSync(fileName, '//@ sourceMappingURL=' + path.basename(mapFileName));
+}
+
+if (require.main === module) {
+ if (process.argv.length < 3) {
+ console.log('Usage: ./sourcemapper.js <filename> <source root (default: .)> ' +
+ '<map file basename (default: filename)>');
+ process.exit(1);
+ } else {
+ var sourceRoot = process.argv.length > 3 ? process.argv[3] : ".";
+ var mapFileBaseName = process.argv.length > 4 ? process.argv[4] : process.argv[2];
+ generateMap(process.argv[2], sourceRoot, mapFileBaseName);
+ }
+}