browse.py: Python 3 compatibility
[platform/upstream/ninja.git] / src / browse.py
1 #!/usr/bin/env python
2 #
3 # Copyright 2001 Google Inc. All Rights Reserved.
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 #     http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 """Simple web server for browsing dependency graph data.
18
19 This script is inlined into the final executable and spawned by
20 it when needed.
21 """
22
23 from __future__ import print_function
24
25 try:
26     import http.server as httpserver
27 except ImportError:
28     import BaseHTTPServer as httpserver
29 import os
30 import socket
31 import subprocess
32 import sys
33 import webbrowser
34 try:
35     from urllib.request import unquote
36 except ImportError:
37     from urllib2 import unquote
38 from collections import namedtuple
39
40 Node = namedtuple('Node', ['inputs', 'rule', 'target', 'outputs'])
41
42 # Ideally we'd allow you to navigate to a build edge or a build node,
43 # with appropriate views for each.  But there's no way to *name* a build
44 # edge so we can only display nodes.
45 #
46 # For a given node, it has at most one input edge, which has n
47 # different inputs.  This becomes node.inputs.  (We leave out the
48 # outputs of the input edge due to what follows.)  The node can have
49 # multiple dependent output edges.  Rather than attempting to display
50 # those, they are summarized by taking the union of all their outputs.
51 #
52 # This means there's no single view that shows you all inputs and outputs
53 # of an edge.  But I think it's less confusing than alternatives.
54
55 def match_strip(line, prefix):
56     if not line.startswith(prefix):
57         return (False, line)
58     return (True, line[len(prefix):])
59
60 def parse(text):
61     lines = iter(text.split('\n'))
62
63     target = None
64     rule = None
65     inputs = []
66     outputs = []
67
68     try:
69         target = next(lines)[:-1]  # strip trailing colon
70
71         line = next(lines)
72         (match, rule) = match_strip(line, '  input: ')
73         if match:
74             (match, line) = match_strip(next(lines), '    ')
75             while match:
76                 type = None
77                 (match, line) = match_strip(line, '| ')
78                 if match:
79                     type = 'implicit'
80                 (match, line) = match_strip(line, '|| ')
81                 if match:
82                     type = 'order-only'
83                 inputs.append((line, type))
84                 (match, line) = match_strip(next(lines), '    ')
85
86         match, _ = match_strip(line, '  outputs:')
87         if match:
88             (match, line) = match_strip(next(lines), '    ')
89             while match:
90                 outputs.append(line)
91                 (match, line) = match_strip(next(lines), '    ')
92     except StopIteration:
93         pass
94
95     return Node(inputs, rule, target, outputs)
96
97 def create_page(body):
98     return '''<!DOCTYPE html>
99 <style>
100 body {
101     font-family: sans;
102     font-size: 0.8em;
103     margin: 4ex;
104 }
105 h1 {
106     font-weight: normal;
107     font-size: 140%;
108     text-align: center;
109     margin: 0;
110 }
111 h2 {
112     font-weight: normal;
113     font-size: 120%;
114 }
115 tt {
116     font-family: WebKitHack, monospace;
117     white-space: nowrap;
118 }
119 .filelist {
120   -webkit-columns: auto 2;
121 }
122 </style>
123 ''' + body
124
125 def generate_html(node):
126     document = ['<h1><tt>%s</tt></h1>' % node.target]
127
128     if node.inputs:
129         document.append('<h2>target is built using rule <tt>%s</tt> of</h2>' %
130                         node.rule)
131         if len(node.inputs) > 0:
132             document.append('<div class=filelist>')
133             for input, type in sorted(node.inputs):
134                 extra = ''
135                 if type:
136                     extra = ' (%s)' % type
137                 document.append('<tt><a href="?%s">%s</a>%s</tt><br>' %
138                                 (input, input, extra))
139             document.append('</div>')
140
141     if node.outputs:
142         document.append('<h2>dependent edges build:</h2>')
143         document.append('<div class=filelist>')
144         for output in sorted(node.outputs):
145             document.append('<tt><a href="?%s">%s</a></tt><br>' %
146                             (output, output))
147         document.append('</div>')
148
149     return '\n'.join(document)
150
151 def ninja_dump(target):
152     proc = subprocess.Popen([sys.argv[1], '-t', 'query', target],
153                             stdout=subprocess.PIPE, stderr=subprocess.PIPE,
154                             universal_newlines=True)
155     return proc.communicate() + (proc.returncode,)
156
157 class RequestHandler(httpserver.BaseHTTPRequestHandler):
158     def do_GET(self):
159         assert self.path[0] == '/'
160         target = unquote(self.path[1:])
161
162         if target == '':
163             self.send_response(302)
164             self.send_header('Location', '?' + sys.argv[2])
165             self.end_headers()
166             return
167
168         if not target.startswith('?'):
169             self.send_response(404)
170             self.end_headers()
171             return
172         target = target[1:]
173
174         ninja_output, ninja_error, exit_code = ninja_dump(target)
175         if exit_code == 0:
176             page_body = generate_html(parse(ninja_output.strip()))
177         else:
178             # Relay ninja's error message.
179             page_body = '<h1><tt>%s</tt></h1>' % ninja_error
180
181         self.send_response(200)
182         self.end_headers()
183         self.wfile.write(create_page(page_body).encode('utf-8'))
184
185     def log_message(self, format, *args):
186         pass  # Swallow console spam.
187
188 port = int(os.getenv("PORT", '8000'))
189 httpd = httpserver.HTTPServer(('',port), RequestHandler)
190 try:
191     hostname = socket.gethostname()
192     print('Web server running on %s:%d, ctl-C to abort...' % (hostname,port) )
193     print('Web server pid %d' % os.getpid(), file=sys.stderr )
194     webbrowser.open_new('http://%s:%s' % (hostname, port) )
195     httpd.serve_forever()
196 except KeyboardInterrupt:
197     print()
198     pass  # Swallow console spam.
199
200