1695b41eeSopenharmony_ci#!/usr/bin/env python3
2695b41eeSopenharmony_ci#
3695b41eeSopenharmony_ci# Copyright 2001 Google Inc. All Rights Reserved.
4695b41eeSopenharmony_ci#
5695b41eeSopenharmony_ci# Licensed under the Apache License, Version 2.0 (the "License");
6695b41eeSopenharmony_ci# you may not use this file except in compliance with the License.
7695b41eeSopenharmony_ci# You may obtain a copy of the License at
8695b41eeSopenharmony_ci#
9695b41eeSopenharmony_ci#     http://www.apache.org/licenses/LICENSE-2.0
10695b41eeSopenharmony_ci#
11695b41eeSopenharmony_ci# Unless required by applicable law or agreed to in writing, software
12695b41eeSopenharmony_ci# distributed under the License is distributed on an "AS IS" BASIS,
13695b41eeSopenharmony_ci# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14695b41eeSopenharmony_ci# See the License for the specific language governing permissions and
15695b41eeSopenharmony_ci# limitations under the License.
16695b41eeSopenharmony_ci
17695b41eeSopenharmony_ci"""Simple web server for browsing dependency graph data.
18695b41eeSopenharmony_ci
19695b41eeSopenharmony_ciThis script is inlined into the final executable and spawned by
20695b41eeSopenharmony_ciit when needed.
21695b41eeSopenharmony_ci"""
22695b41eeSopenharmony_ci
23695b41eeSopenharmony_citry:
24695b41eeSopenharmony_ci    import http.server as httpserver
25695b41eeSopenharmony_ci    import socketserver
26695b41eeSopenharmony_ciexcept ImportError:
27695b41eeSopenharmony_ci    import BaseHTTPServer as httpserver
28695b41eeSopenharmony_ci    import SocketServer as socketserver
29695b41eeSopenharmony_ciimport argparse
30695b41eeSopenharmony_ciimport os
31695b41eeSopenharmony_ciimport socket
32695b41eeSopenharmony_ciimport subprocess
33695b41eeSopenharmony_ciimport sys
34695b41eeSopenharmony_ciimport webbrowser
35695b41eeSopenharmony_ciif sys.version_info >= (3, 2):
36695b41eeSopenharmony_ci    from html import escape
37695b41eeSopenharmony_cielse:
38695b41eeSopenharmony_ci    from cgi import escape
39695b41eeSopenharmony_citry:
40695b41eeSopenharmony_ci    from urllib.request import unquote
41695b41eeSopenharmony_ciexcept ImportError:
42695b41eeSopenharmony_ci    from urllib2 import unquote
43695b41eeSopenharmony_cifrom collections import namedtuple
44695b41eeSopenharmony_ci
45695b41eeSopenharmony_ciNode = namedtuple('Node', ['inputs', 'rule', 'target', 'outputs'])
46695b41eeSopenharmony_ci
47695b41eeSopenharmony_ci# Ideally we'd allow you to navigate to a build edge or a build node,
48695b41eeSopenharmony_ci# with appropriate views for each.  But there's no way to *name* a build
49695b41eeSopenharmony_ci# edge so we can only display nodes.
50695b41eeSopenharmony_ci#
51695b41eeSopenharmony_ci# For a given node, it has at most one input edge, which has n
52695b41eeSopenharmony_ci# different inputs.  This becomes node.inputs.  (We leave out the
53695b41eeSopenharmony_ci# outputs of the input edge due to what follows.)  The node can have
54695b41eeSopenharmony_ci# multiple dependent output edges.  Rather than attempting to display
55695b41eeSopenharmony_ci# those, they are summarized by taking the union of all their outputs.
56695b41eeSopenharmony_ci#
57695b41eeSopenharmony_ci# This means there's no single view that shows you all inputs and outputs
58695b41eeSopenharmony_ci# of an edge.  But I think it's less confusing than alternatives.
59695b41eeSopenharmony_ci
60695b41eeSopenharmony_cidef match_strip(line, prefix):
61695b41eeSopenharmony_ci    if not line.startswith(prefix):
62695b41eeSopenharmony_ci        return (False, line)
63695b41eeSopenharmony_ci    return (True, line[len(prefix):])
64695b41eeSopenharmony_ci
65695b41eeSopenharmony_cidef html_escape(text):
66695b41eeSopenharmony_ci    return escape(text, quote=True)
67695b41eeSopenharmony_ci
68695b41eeSopenharmony_cidef parse(text):
69695b41eeSopenharmony_ci    lines = iter(text.split('\n'))
70695b41eeSopenharmony_ci
71695b41eeSopenharmony_ci    target = None
72695b41eeSopenharmony_ci    rule = None
73695b41eeSopenharmony_ci    inputs = []
74695b41eeSopenharmony_ci    outputs = []
75695b41eeSopenharmony_ci
76695b41eeSopenharmony_ci    try:
77695b41eeSopenharmony_ci        target = next(lines)[:-1]  # strip trailing colon
78695b41eeSopenharmony_ci
79695b41eeSopenharmony_ci        line = next(lines)
80695b41eeSopenharmony_ci        (match, rule) = match_strip(line, '  input: ')
81695b41eeSopenharmony_ci        if match:
82695b41eeSopenharmony_ci            (match, line) = match_strip(next(lines), '    ')
83695b41eeSopenharmony_ci            while match:
84695b41eeSopenharmony_ci                type = None
85695b41eeSopenharmony_ci                (match, line) = match_strip(line, '| ')
86695b41eeSopenharmony_ci                if match:
87695b41eeSopenharmony_ci                    type = 'implicit'
88695b41eeSopenharmony_ci                (match, line) = match_strip(line, '|| ')
89695b41eeSopenharmony_ci                if match:
90695b41eeSopenharmony_ci                    type = 'order-only'
91695b41eeSopenharmony_ci                inputs.append((line, type))
92695b41eeSopenharmony_ci                (match, line) = match_strip(next(lines), '    ')
93695b41eeSopenharmony_ci
94695b41eeSopenharmony_ci        match, _ = match_strip(line, '  outputs:')
95695b41eeSopenharmony_ci        if match:
96695b41eeSopenharmony_ci            (match, line) = match_strip(next(lines), '    ')
97695b41eeSopenharmony_ci            while match:
98695b41eeSopenharmony_ci                outputs.append(line)
99695b41eeSopenharmony_ci                (match, line) = match_strip(next(lines), '    ')
100695b41eeSopenharmony_ci    except StopIteration:
101695b41eeSopenharmony_ci        pass
102695b41eeSopenharmony_ci
103695b41eeSopenharmony_ci    return Node(inputs, rule, target, outputs)
104695b41eeSopenharmony_ci
105695b41eeSopenharmony_cidef create_page(body):
106695b41eeSopenharmony_ci    return '''<!DOCTYPE html>
107695b41eeSopenharmony_ci<style>
108695b41eeSopenharmony_cibody {
109695b41eeSopenharmony_ci    font-family: sans;
110695b41eeSopenharmony_ci    font-size: 0.8em;
111695b41eeSopenharmony_ci    margin: 4ex;
112695b41eeSopenharmony_ci}
113695b41eeSopenharmony_cih1 {
114695b41eeSopenharmony_ci    font-weight: normal;
115695b41eeSopenharmony_ci    font-size: 140%;
116695b41eeSopenharmony_ci    text-align: center;
117695b41eeSopenharmony_ci    margin: 0;
118695b41eeSopenharmony_ci}
119695b41eeSopenharmony_cih2 {
120695b41eeSopenharmony_ci    font-weight: normal;
121695b41eeSopenharmony_ci    font-size: 120%;
122695b41eeSopenharmony_ci}
123695b41eeSopenharmony_citt {
124695b41eeSopenharmony_ci    font-family: WebKitHack, monospace;
125695b41eeSopenharmony_ci    white-space: nowrap;
126695b41eeSopenharmony_ci}
127695b41eeSopenharmony_ci.filelist {
128695b41eeSopenharmony_ci  -webkit-columns: auto 2;
129695b41eeSopenharmony_ci}
130695b41eeSopenharmony_ci</style>
131695b41eeSopenharmony_ci''' + body
132695b41eeSopenharmony_ci
133695b41eeSopenharmony_cidef generate_html(node):
134695b41eeSopenharmony_ci    document = ['<h1><tt>%s</tt></h1>' % html_escape(node.target)]
135695b41eeSopenharmony_ci
136695b41eeSopenharmony_ci    if node.inputs:
137695b41eeSopenharmony_ci        document.append('<h2>target is built using rule <tt>%s</tt> of</h2>' %
138695b41eeSopenharmony_ci                        html_escape(node.rule))
139695b41eeSopenharmony_ci        if len(node.inputs) > 0:
140695b41eeSopenharmony_ci            document.append('<div class=filelist>')
141695b41eeSopenharmony_ci            for input, type in sorted(node.inputs):
142695b41eeSopenharmony_ci                extra = ''
143695b41eeSopenharmony_ci                if type:
144695b41eeSopenharmony_ci                    extra = ' (%s)' % html_escape(type)
145695b41eeSopenharmony_ci                document.append('<tt><a href="?%s">%s</a>%s</tt><br>' %
146695b41eeSopenharmony_ci                                (html_escape(input), html_escape(input), extra))
147695b41eeSopenharmony_ci            document.append('</div>')
148695b41eeSopenharmony_ci
149695b41eeSopenharmony_ci    if node.outputs:
150695b41eeSopenharmony_ci        document.append('<h2>dependent edges build:</h2>')
151695b41eeSopenharmony_ci        document.append('<div class=filelist>')
152695b41eeSopenharmony_ci        for output in sorted(node.outputs):
153695b41eeSopenharmony_ci            document.append('<tt><a href="?%s">%s</a></tt><br>' %
154695b41eeSopenharmony_ci                            (html_escape(output), html_escape(output)))
155695b41eeSopenharmony_ci        document.append('</div>')
156695b41eeSopenharmony_ci
157695b41eeSopenharmony_ci    return '\n'.join(document)
158695b41eeSopenharmony_ci
159695b41eeSopenharmony_cidef ninja_dump(target):
160695b41eeSopenharmony_ci    cmd = [args.ninja_command, '-f', args.f, '-t', 'query', target]
161695b41eeSopenharmony_ci    proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
162695b41eeSopenharmony_ci                            universal_newlines=True)
163695b41eeSopenharmony_ci    return proc.communicate() + (proc.returncode,)
164695b41eeSopenharmony_ci
165695b41eeSopenharmony_ciclass RequestHandler(httpserver.BaseHTTPRequestHandler):
166695b41eeSopenharmony_ci    def do_GET(self):
167695b41eeSopenharmony_ci        assert self.path[0] == '/'
168695b41eeSopenharmony_ci        target = unquote(self.path[1:])
169695b41eeSopenharmony_ci
170695b41eeSopenharmony_ci        if target == '':
171695b41eeSopenharmony_ci            self.send_response(302)
172695b41eeSopenharmony_ci            self.send_header('Location', '?' + args.initial_target)
173695b41eeSopenharmony_ci            self.end_headers()
174695b41eeSopenharmony_ci            return
175695b41eeSopenharmony_ci
176695b41eeSopenharmony_ci        if not target.startswith('?'):
177695b41eeSopenharmony_ci            self.send_response(404)
178695b41eeSopenharmony_ci            self.end_headers()
179695b41eeSopenharmony_ci            return
180695b41eeSopenharmony_ci        target = target[1:]
181695b41eeSopenharmony_ci
182695b41eeSopenharmony_ci        ninja_output, ninja_error, exit_code = ninja_dump(target)
183695b41eeSopenharmony_ci        if exit_code == 0:
184695b41eeSopenharmony_ci            page_body = generate_html(parse(ninja_output.strip()))
185695b41eeSopenharmony_ci        else:
186695b41eeSopenharmony_ci            # Relay ninja's error message.
187695b41eeSopenharmony_ci            page_body = '<h1><tt>%s</tt></h1>' % html_escape(ninja_error)
188695b41eeSopenharmony_ci
189695b41eeSopenharmony_ci        self.send_response(200)
190695b41eeSopenharmony_ci        self.end_headers()
191695b41eeSopenharmony_ci        self.wfile.write(create_page(page_body).encode('utf-8'))
192695b41eeSopenharmony_ci
193695b41eeSopenharmony_ci    def log_message(self, format, *args):
194695b41eeSopenharmony_ci        pass  # Swallow console spam.
195695b41eeSopenharmony_ci
196695b41eeSopenharmony_ciparser = argparse.ArgumentParser(prog='ninja -t browse')
197695b41eeSopenharmony_ciparser.add_argument('--port', '-p', default=8000, type=int,
198695b41eeSopenharmony_ci    help='Port number to use (default %(default)d)')
199695b41eeSopenharmony_ciparser.add_argument('--hostname', '-a', default='localhost', type=str,
200695b41eeSopenharmony_ci    help='Hostname to bind to (default %(default)s)')
201695b41eeSopenharmony_ciparser.add_argument('--no-browser', action='store_true',
202695b41eeSopenharmony_ci    help='Do not open a webbrowser on startup.')
203695b41eeSopenharmony_ci
204695b41eeSopenharmony_ciparser.add_argument('--ninja-command', default='ninja',
205695b41eeSopenharmony_ci    help='Path to ninja binary (default %(default)s)')
206695b41eeSopenharmony_ciparser.add_argument('-f', default='build.ninja',
207695b41eeSopenharmony_ci    help='Path to build.ninja file (default %(default)s)')
208695b41eeSopenharmony_ciparser.add_argument('initial_target', default='all', nargs='?',
209695b41eeSopenharmony_ci    help='Initial target to show (default %(default)s)')
210695b41eeSopenharmony_ci
211695b41eeSopenharmony_ciclass HTTPServer(socketserver.ThreadingMixIn, httpserver.HTTPServer):
212695b41eeSopenharmony_ci    # terminate server immediately when Python exits.
213695b41eeSopenharmony_ci    daemon_threads = True
214695b41eeSopenharmony_ci
215695b41eeSopenharmony_ciargs = parser.parse_args()
216695b41eeSopenharmony_ciport = args.port
217695b41eeSopenharmony_cihostname = args.hostname
218695b41eeSopenharmony_cihttpd = HTTPServer((hostname,port), RequestHandler)
219695b41eeSopenharmony_citry:
220695b41eeSopenharmony_ci    if hostname == "":
221695b41eeSopenharmony_ci        hostname = socket.gethostname()
222695b41eeSopenharmony_ci    print('Web server running on %s:%d, ctl-C to abort...' % (hostname,port) )
223695b41eeSopenharmony_ci    print('Web server pid %d' % os.getpid(), file=sys.stderr )
224695b41eeSopenharmony_ci    if not args.no_browser:
225695b41eeSopenharmony_ci        webbrowser.open_new('http://%s:%s' % (hostname, port) )
226695b41eeSopenharmony_ci    httpd.serve_forever()
227695b41eeSopenharmony_ciexcept KeyboardInterrupt:
228695b41eeSopenharmony_ci    print()
229695b41eeSopenharmony_ci    pass  # Swallow console spam.
230695b41eeSopenharmony_ci
231695b41eeSopenharmony_ci
232