add browser mode
authorEvan Martin <martine@danga.com>
Sun, 23 Jan 2011 00:39:06 +0000 (16:39 -0800)
committerEvan Martin <martine@danga.com>
Sun, 23 Jan 2011 00:39:06 +0000 (16:39 -0800)
src/browse.py [new file with mode: 0755]
src/ninja.cc

diff --git a/src/browse.py b/src/browse.py
new file mode 100755 (executable)
index 0000000..0d586bf
--- /dev/null
@@ -0,0 +1,130 @@
+#!/usr/bin/python
+
+"""Simple web server for browsing dependency graph data.
+
+This script is inlined into the final executable and spawned by
+it when needed.
+"""
+
+import BaseHTTPServer
+import subprocess
+import sys
+import webbrowser
+
+def match_strip(prefix, line):
+    assert line.startswith(prefix)
+    return line[len(prefix):]
+
+def parse(text):
+    lines = text.split('\n')
+    node = lines.pop(0)
+    node = node[:-1]  # strip trailing colon
+
+    input = []
+    if lines and lines[0].startswith('  input:'):
+        input.append(match_strip('  input: ', lines.pop(0)))
+        while lines and lines[0].startswith('    '):
+            input.append(lines.pop(0).strip())
+
+    outputs = []
+    while lines:
+        output = []
+        output.append(match_strip('  output: ', lines.pop(0)))
+        while lines and lines[0].startswith('    '):
+            output.append(lines.pop(0).strip())
+        outputs.append(output)
+
+    return (node, input, outputs)
+
+def generate_html(data):
+    node, input, outputs = data
+    print '''<!DOCTYPE html>
+<style>
+body {
+    font-family: sans;
+    font-size: 0.8em;
+    margin: 4ex;
+}
+h1 {
+    font-weight: normal;
+    text-align: center;
+    margin: 0;
+}
+h2 {
+    font-weight: normal;
+}
+tt {
+    font-family: WebKitHack, monospace;
+}
+</style>'''
+    print '<table><tr><td colspan=3>'
+    print '<h1>%s</h1>' % node
+    print '</td></tr>'
+
+    print '<tr><td valign=top>'
+    print '<h2>input</h2>'
+    if input:
+        print '<p><tt>%s</tt>:</p>' % input[0]
+        print '<ul>'
+        for i in input[1:]:
+            print '<li><tt><a href="?%s">%s</a></tt></li>' % (i, i)
+        print '</ul>'
+    print '</td>'
+    print '<td width=50>&nbsp;</td>'
+
+    print '<td valign=top>'
+    print '<h2>outputs</h2>'
+    for output in outputs:
+        print '<p><tt>%s</tt>:</p>' % output[0]
+        print '<ul>'
+        for i in output[1:]:
+            print '<li><tt><a href="?%s">%s</a></tt></li>' % (i, i)
+        print '</ul>'
+    print '</td></tr></table>'
+
+def ninja_dump(target):
+    proc = subprocess.Popen(['./ninja', '-q', target], stdout=subprocess.PIPE)
+    return proc.communicate()[0]
+
+class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+    def do_GET(self):
+        assert self.path[0] == '/'
+        target = self.path[1:]
+
+        if target == '':
+            self.send_response(302)
+            self.send_header('Location', '?' + sys.argv[1])
+            self.end_headers()
+            return
+
+        if not target.startswith('?'):
+            self.send_response(404)
+            self.end_headers()
+            return
+        target = target[1:]
+
+        input = ninja_dump(target)
+
+        self.send_response(200)
+        self.end_headers()
+        stdout = sys.stdout
+        sys.stdout = self.wfile
+        try:
+            generate_html(parse(input.strip()))
+        finally:
+            sys.stdout = stdout
+
+    def log_message(self, format, *args):
+        pass  # Swallow console spam.
+
+port = 8000
+httpd = BaseHTTPServer.HTTPServer(('',port), RequestHandler)
+try:
+    print 'Web server running on port %d...' % port
+    webbrowser.open_new('http://localhost:%s' % port)
+    httpd.serve_forever()
+except KeyboardInterrupt:
+    print
+    pass  # Swallow console spam.
+
+
index 0c77489..d273bea 100644 (file)
 
 #include "graphviz.h"
 
+// Import browse.py as binary data.
+asm(
+".data\n"
+"browse_data_begin:\n"
+".incbin \"src/browse.py\"\n"
+"browse_data_end:\n"
+);
+// Declare the symbols defined above.
+extern const char browse_data_begin[];
+extern const char browse_data_end[];
+
 option options[] = {
   { "help", no_argument, NULL, 'h' },
   { }
@@ -25,6 +36,7 @@ void usage() {
 "  -n       dry run (don't run commands but pretend they succeeded)\n"
 "  -v       show all command lines\n"
 "  -q       show inputs/outputs of target (query mode)\n"
+"  -b       browse dependency graph of target in a web browser\n"
           );
 }
 
@@ -39,9 +51,10 @@ int main(int argc, char** argv) {
   const char* input_file = "build.ninja";
   bool graph = false;
   bool query = false;
+  bool browse = false;
 
   int opt;
-  while ((opt = getopt_long(argc, argv, "ghi:nvq", options, NULL)) != -1) {
+  while ((opt = getopt_long(argc, argv, "bghi:nvq", options, NULL)) != -1) {
     switch (opt) {
       case 'g':
         graph = true;
@@ -58,6 +71,9 @@ int main(int argc, char** argv) {
       case 'q':
         query = true;
         break;
+      case 'b':
+        browse = true;
+        break;
       case 'h':
       default:
         usage();
@@ -123,6 +139,30 @@ int main(int argc, char** argv) {
     return 0;
   }
 
+  if (browse) {
+    // Create a temporary file, dump the Python code into it, and
+    // delete the file, keeping our open handle to it.
+    char tmpl[] = "browsepy-XXXXXX";
+    int fd = mkstemp(tmpl);
+    unlink(tmpl);
+    const int browse_data_len = browse_data_end - browse_data_begin;
+    int len = write(fd, browse_data_begin, browse_data_len);
+    if (len < browse_data_len) {
+      perror("write");
+      return 1;
+    }
+
+    // exec Python, telling it to use our script file.
+    const char* command[] = {
+      "python", "/proc/self/fd/3", argv[0], NULL
+    };
+    execvp(command[0], (char**)command);
+
+    // If we get here, the exec failed.
+    printf("ERROR: Failed to spawn python for graph browsing, aborting.\n");
+    return 1;
+  }
+
   const char* kLogPath = ".ninja_log";
   if (!state.build_log_->Load(kLogPath, &err)) {
     fprintf(stderr, "error loading build log: %s\n", err.c_str());