#!/usr/bin/env python3 # # Copyright 2001 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Simple web server for browsing dependency graph data. This script is inlined into the final executable and spawned by it when needed. """ try: import http.server as httpserver import socketserver except ImportError: import BaseHTTPServer as httpserver import SocketServer as socketserver import argparse import os import socket import subprocess import sys import webbrowser if sys.version_info >= (3, 2): from html import escape else: from cgi import escape try: from urllib.request import unquote except ImportError: from urllib2 import unquote from collections import namedtuple Node = namedtuple("Node", ["inputs", "rule", "target", "outputs"]) # Ideally we'd allow you to navigate to a build edge or a build node, # with appropriate views for each. But there's no way to *name* a build # edge so we can only display nodes. # # For a given node, it has at most one input edge, which has n # different inputs. This becomes node.inputs. (We leave out the # outputs of the input edge due to what follows.) The node can have # multiple dependent output edges. Rather than attempting to display # those, they are summarized by taking the union of all their outputs. # # This means there's no single view that shows you all inputs and outputs # of an edge. But I think it's less confusing than alternatives. def match_strip(line, prefix): if not line.startswith(prefix): return (False, line) return (True, line[len(prefix) :]) def html_escape(text): return escape(text, quote=True) def parse(text): lines = iter(text.split("\n")) target = None rule = None inputs = [] outputs = [] try: target = next(lines)[:-1] # strip trailing colon line = next(lines) (match, rule) = match_strip(line, " input: ") if match: (match, line) = match_strip(next(lines), " ") while match: type = None (match, line) = match_strip(line, "| ") if match: type = "implicit" (match, line) = match_strip(line, "|| ") if match: type = "order-only" inputs.append((line, type)) (match, line) = match_strip(next(lines), " ") match, _ = match_strip(line, " outputs:") if match: (match, line) = match_strip(next(lines), " ") while match: outputs.append(line) (match, line) = match_strip(next(lines), " ") except StopIteration: pass return Node(inputs, rule, target, outputs) def create_page(body): return ( """<!DOCTYPE html> <style> body { font-family: sans; font-size: 0.8em; margin: 4ex; } h1 { font-weight: normal; font-size: 140%; text-align: center; margin: 0; } h2 { font-weight: normal; font-size: 120%; } tt { font-family: WebKitHack, monospace; white-space: nowrap; } .filelist { -webkit-columns: auto 2; } </style> """ + body ) def generate_html(node): document = ["<h1><tt>%s</tt></h1>" % html_escape(node.target)] if node.inputs: document.append("<h2>target is built using rule <tt>%s</tt> of</h2>" % html_escape(node.rule)) if len(node.inputs) > 0: document.append("<div class=filelist>") for input, type in sorted(node.inputs): extra = "" if type: extra = " (%s)" % html_escape(type) document.append( '<tt><a href="?%s">%s</a>%s</tt><br>' % (html_escape(input), html_escape(input), extra) ) document.append("</div>") if node.outputs: document.append("<h2>dependent edges build:</h2>") document.append("<div class=filelist>") for output in sorted(node.outputs): document.append('<tt><a href="?%s">%s</a></tt><br>' % (html_escape(output), html_escape(output))) document.append("</div>") return "\n".join(document) def ninja_dump(target): cmd = [args.ninja_command, "-f", args.f, "-t", "query", target] proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) return proc.communicate() + (proc.returncode,) class RequestHandler(httpserver.BaseHTTPRequestHandler): def do_GET(self): assert self.path[0] == "/" target = unquote(self.path[1:]) if target == "": self.send_response(302) self.send_header("Location", "?" + args.initial_target) self.end_headers() return if not target.startswith("?"): self.send_response(404) self.end_headers() return target = target[1:] ninja_output, ninja_error, exit_code = ninja_dump(target) if exit_code == 0: page_body = generate_html(parse(ninja_output.strip())) else: # Relay ninja's error message. page_body = "<h1><tt>%s</tt></h1>" % html_escape(ninja_error) self.send_response(200) self.end_headers() self.wfile.write(create_page(page_body).encode("utf-8")) def log_message(self, format, *args): pass # Swallow console spam. parser = argparse.ArgumentParser(prog="ninja -t browse") parser.add_argument("--port", "-p", default=8000, type=int, help="Port number to use (default %(default)d)") parser.add_argument( "--hostname", "-a", default="localhost", type=str, help="Hostname to bind to (default %(default)s)" ) parser.add_argument("--no-browser", action="store_true", help="Do not open a webbrowser on startup.") parser.add_argument("--ninja-command", default="ninja", help="Path to ninja binary (default %(default)s)") parser.add_argument("-f", default="build.ninja", help="Path to build.ninja file (default %(default)s)") parser.add_argument("initial_target", default="all", nargs="?", help="Initial target to show (default %(default)s)") class HTTPServer(socketserver.ThreadingMixIn, httpserver.HTTPServer): # terminate server immediately when Python exits. daemon_threads = True args = parser.parse_args() port = args.port hostname = args.hostname httpd = HTTPServer((hostname, port), RequestHandler) try: if hostname == "": hostname = socket.gethostname() print("Web server running on %s:%d, ctl-C to abort..." % (hostname, port)) print("Web server pid %d" % os.getpid(), file=sys.stderr) if not args.no_browser: webbrowser.open_new("http://%s:%s" % (hostname, port)) httpd.serve_forever() except KeyboardInterrupt: print() pass # Swallow console spam.