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