081eb8cf5328236353677b11da0714cde6bdbf6d
[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 import urllib2
35 from collections import namedtuple
36
37 Node = namedtuple('Node', ['inputs', 'rule', 'target', 'outputs'])
38
39 # Ideally we'd allow you to navigate to a build edge or a build node,
40 # with appropriate views for each.  But there's no way to *name* a build
41 # edge so we can only display nodes.
42 #
43 # For a given node, it has at most one input edge, which has n
44 # different inputs.  This becomes node.inputs.  (We leave out the
45 # outputs of the input edge due to what follows.)  The node can have
46 # multiple dependent output edges.  Rather than attempting to display
47 # those, they are summarized by taking the union of all their outputs.
48 #
49 # This means there's no single view that shows you all inputs and outputs
50 # of an edge.  But I think it's less confusing than alternatives.
51
52 def match_strip(line, prefix):
53     if not line.startswith(prefix):
54         return (False, line)
55     return (True, line[len(prefix):])
56
57 def parse(text):
58     lines = iter(text.split('\n'))
59
60     target = None
61     rule = None
62     inputs = []
63     outputs = []
64
65     try:
66         target = next(lines)[:-1]  # strip trailing colon
67
68         line = next(lines)
69         (match, rule) = match_strip(line, '  input: ')
70         if match:
71             (match, line) = match_strip(next(lines), '    ')
72             while match:
73                 type = None
74                 (match, line) = match_strip(line, '| ')
75                 if match:
76                     type = 'implicit'
77                 (match, line) = match_strip(line, '|| ')
78                 if match:
79                     type = 'order-only'
80                 inputs.append((line, type))
81                 (match, line) = match_strip(next(lines), '    ')
82
83         match, _ = match_strip(line, '  outputs:')
84         if match:
85             (match, line) = match_strip(next(lines), '    ')
86             while match:
87                 outputs.append(line)
88                 (match, line) = match_strip(next(lines), '    ')
89     except StopIteration:
90         pass
91
92     return Node(inputs, rule, target, outputs)
93
94 def create_page(body):
95     return '''<!DOCTYPE html>
96 <style>
97 body {
98     font-family: sans;
99     font-size: 0.8em;
100     margin: 4ex;
101 }
102 h1 {
103     font-weight: normal;
104     font-size: 140%;
105     text-align: center;
106     margin: 0;
107 }
108 h2 {
109     font-weight: normal;
110     font-size: 120%;
111 }
112 tt {
113     font-family: WebKitHack, monospace;
114     white-space: nowrap;
115 }
116 .filelist {
117   -webkit-columns: auto 2;
118 }
119 </style>
120 ''' + body
121
122 def generate_html(node):
123     document = ['<h1><tt>%s</tt></h1>' % node.target]
124
125     if node.inputs:
126         document.append('<h2>target is built using rule <tt>%s</tt> of</h2>' %
127                         node.rule)
128         if len(node.inputs) > 0:
129             document.append('<div class=filelist>')
130             for input, type in sorted(node.inputs):
131                 extra = ''
132                 if type:
133                     extra = ' (%s)' % type
134                 document.append('<tt><a href="?%s">%s</a>%s</tt><br>' %
135                                 (input, input, extra))
136             document.append('</div>')
137
138     if node.outputs:
139         document.append('<h2>dependent edges build:</h2>')
140         document.append('<div class=filelist>')
141         for output in sorted(node.outputs):
142             document.append('<tt><a href="?%s">%s</a></tt><br>' %
143                             (output, output))
144         document.append('</div>')
145
146     return '\n'.join(document)
147
148 def ninja_dump(target):
149     proc = subprocess.Popen([sys.argv[1], '-t', 'query', target],
150                             stdout=subprocess.PIPE, stderr=subprocess.PIPE,
151                             universal_newlines=True)
152     return proc.communicate() + (proc.returncode,)
153
154 class RequestHandler(httpserver.BaseHTTPRequestHandler):
155     def do_GET(self):
156         assert self.path[0] == '/'
157         target = urllib2.unquote(self.path[1:])
158
159         if target == '':
160             self.send_response(302)
161             self.send_header('Location', '?' + sys.argv[2])
162             self.end_headers()
163             return
164
165         if not target.startswith('?'):
166             self.send_response(404)
167             self.end_headers()
168             return
169         target = target[1:]
170
171         ninja_output, ninja_error, exit_code = ninja_dump(target)
172         if exit_code == 0:
173             page_body = generate_html(parse(ninja_output.strip()))
174         else:
175             # Relay ninja's error message.
176             page_body = '<h1><tt>%s</tt></h1>' % ninja_error
177
178         self.send_response(200)
179         self.end_headers()
180         self.wfile.write(create_page(page_body).encode('utf-8'))
181
182     def log_message(self, format, *args):
183         pass  # Swallow console spam.
184
185 port = int(os.getenv("PORT", '8000'))
186 httpd = httpserver.HTTPServer(('',port), RequestHandler)
187 try:
188     hostname = socket.gethostname()
189     print('Web server running on %s:%d, ctl-C to abort...' % (hostname,port) )
190     print('Web server pid %d' % os.getpid(), file=sys.stderr )
191     webbrowser.open_new('http://%s:%s' % (hostname, port) )
192     httpd.serve_forever()
193 except KeyboardInterrupt:
194     print()
195     pass  # Swallow console spam.
196
197