• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2
3"""
4Command to print info about makefiles remaining to be converted to soong.
5
6See usage / argument parsing below for commandline options.
7"""
8
9import argparse
10import csv
11import itertools
12import json
13import os
14import re
15import sys
16
17DIRECTORY_PATTERNS = [x.split("/") for x in (
18  "device/*",
19  "frameworks/*",
20  "hardware/*",
21  "packages/*",
22  "vendor/*",
23  "*",
24)]
25
26def match_directory_group(pattern, filename):
27  match = []
28  filename = filename.split("/")
29  if len(filename) < len(pattern):
30    return None
31  for i in range(len(pattern)):
32    pattern_segment = pattern[i]
33    filename_segment = filename[i]
34    if pattern_segment == "*" or pattern_segment == filename_segment:
35      match.append(filename_segment)
36    else:
37      return None
38  if match:
39    return os.path.sep.join(match)
40  else:
41    return None
42
43def directory_group(filename):
44  for pattern in DIRECTORY_PATTERNS:
45    match = match_directory_group(pattern, filename)
46    if match:
47      return match
48  return os.path.dirname(filename)
49
50class Analysis(object):
51  def __init__(self, filename, line_matches):
52    self.filename = filename;
53    self.line_matches = line_matches
54
55def analyze_lines(filename, lines, func):
56  line_matches = []
57  for i in range(len(lines)):
58    line = lines[i]
59    stripped = line.strip()
60    if stripped.startswith("#"):
61      continue
62    if func(stripped):
63      line_matches.append((i+1, line))
64  if line_matches:
65    return Analysis(filename, line_matches);
66
67def analyze_has_conditional(line):
68  return (line.startswith("ifeq") or line.startswith("ifneq")
69          or line.startswith("ifdef") or line.startswith("ifndef"))
70
71NORMAL_INCLUDES = [re.compile(pattern) for pattern in (
72  "include \$+\(CLEAR_VARS\)", # These are in defines which are tagged separately
73  "include \$+\(BUILD_.*\)",
74  "include \$\(call first-makefiles-under, *\$\(LOCAL_PATH\)\)",
75  "include \$\(call all-subdir-makefiles\)",
76  "include \$\(all-subdir-makefiles\)",
77  "include \$\(call all-makefiles-under, *\$\(LOCAL_PATH\)\)",
78  "include \$\(call all-makefiles-under, *\$\(call my-dir\).*\)",
79  "include \$\(BUILD_SYSTEM\)/base_rules.mk", # called out separately
80  "include \$\(call all-named-subdir-makefiles,.*\)",
81  "include \$\(subdirs\)",
82)]
83def analyze_has_wacky_include(line):
84  if not (line.startswith("include") or line.startswith("-include")
85          or line.startswith("sinclude")):
86    return False
87  for matcher in NORMAL_INCLUDES:
88    if matcher.fullmatch(line):
89      return False
90  return True
91
92BASE_RULES_RE = re.compile("include \$\(BUILD_SYSTEM\)/base_rules.mk")
93
94class Analyzer(object):
95  def __init__(self, title, func):
96    self.title = title;
97    self.func = func
98
99
100ANALYZERS = (
101  Analyzer("ifeq / ifneq", analyze_has_conditional),
102  Analyzer("Wacky Includes", analyze_has_wacky_include),
103  Analyzer("Calls base_rules", lambda line: BASE_RULES_RE.fullmatch(line)),
104  Analyzer("Calls define", lambda line: line.startswith("define ")),
105  Analyzer("Has ../", lambda line: "../" in line),
106  Analyzer("dist-for-&#8203;goals", lambda line: "dist-for-goals" in line),
107  Analyzer(".PHONY", lambda line: ".PHONY" in line),
108  Analyzer("render-&#8203;script", lambda line: ".rscript" in line),
109  Analyzer("vts src", lambda line: ".vts" in line),
110  Analyzer("COPY_&#8203;HEADERS", lambda line: "LOCAL_COPY_HEADERS" in line),
111)
112
113class Summary(object):
114  def __init__(self):
115    self.makefiles = dict()
116    self.directories = dict()
117
118  def Add(self, makefile):
119    self.makefiles[makefile.filename] = makefile
120    self.directories.setdefault(directory_group(makefile.filename), []).append(makefile)
121
122class Makefile(object):
123  def __init__(self, filename):
124    self.filename = filename
125
126    # Analyze the file
127    with open(filename, "r", errors="ignore") as f:
128      try:
129        lines = f.readlines()
130      except UnicodeDecodeError as ex:
131        sys.stderr.write("Filename: %s\n" % filename)
132        raise ex
133    lines = [line.strip() for line in lines]
134
135    self.analyses = dict([(analyzer, analyze_lines(filename, lines, analyzer.func)) for analyzer
136        in ANALYZERS])
137
138def find_android_mk():
139  cwd = os.getcwd()
140  for root, dirs, files in os.walk(cwd):
141    for filename in files:
142      if filename == "Android.mk":
143        yield os.path.join(root, filename)[len(cwd) + 1:]
144    for ignore in (".git", ".repo"):
145      if ignore in dirs:
146        dirs.remove(ignore)
147
148def is_aosp(dirname):
149  for d in ("device/sample", "hardware/interfaces", "hardware/libhardware",
150          "hardware/ril"):
151    if dirname.startswith(d):
152      return True
153  for d in ("device/", "hardware/", "vendor/"):
154    if dirname.startswith(d):
155      return False
156  return True
157
158def is_google(dirname):
159  for d in ("device/google",
160            "hardware/google",
161            "test/sts",
162            "vendor/auto",
163            "vendor/google",
164            "vendor/unbundled_google",
165            "vendor/widevine",
166            "vendor/xts"):
167    if dirname.startswith(d):
168      return True
169  return False
170
171def is_clean(makefile):
172  for analysis in makefile.analyses.values():
173    if analysis:
174      return False
175  return True
176
177def clean_and_only_blocked_by_clean(soong, all_makefiles, makefile):
178  if not is_clean(makefile):
179    return False
180  modules = soong.reverse_makefiles[makefile.filename]
181  for module in modules:
182    for dep in soong.transitive_deps(module):
183      for filename in soong.makefiles.get(dep, []):
184        m = all_makefiles.get(filename)
185        if m and not is_clean(m):
186          return False
187  return True
188
189class Annotations(object):
190  def __init__(self):
191    self.entries = []
192    self.count = 0
193
194  def Add(self, makefiles, modules):
195    self.entries.append((makefiles, modules))
196    self.count += 1
197    return self.count-1
198
199class SoongData(object):
200  def __init__(self, reader):
201    """Read the input file and store the modules and dependency mappings.
202    """
203    self.problems = dict()
204    self.deps = dict()
205    self.reverse_deps = dict()
206    self.module_types = dict()
207    self.makefiles = dict()
208    self.reverse_makefiles = dict()
209    self.installed = dict()
210    self.reverse_installed = dict()
211    self.modules = set()
212
213    for (module, module_type, problem, dependencies, makefiles, installed) in reader:
214      self.modules.add(module)
215      makefiles = [f for f in makefiles.strip().split(' ') if f != ""]
216      self.module_types[module] = module_type
217      self.problems[module] = problem
218      self.deps[module] = [d for d in dependencies.strip().split(' ') if d != ""]
219      for dep in self.deps[module]:
220        if not dep in self.reverse_deps:
221          self.reverse_deps[dep] = []
222        self.reverse_deps[dep].append(module)
223      self.makefiles[module] = makefiles
224      for f in makefiles:
225        self.reverse_makefiles.setdefault(f, []).append(module)
226      for f in installed.strip().split(' '):
227        self.installed[f] = module
228        self.reverse_installed.setdefault(module, []).append(f)
229
230  def transitive_deps(self, module):
231    results = set()
232    def traverse(module):
233      for dep in self.deps.get(module, []):
234        if not dep in results:
235          results.add(dep)
236          traverse(module)
237    traverse(module)
238    return results
239
240  def contains_unblocked_modules(self, filename):
241    for m in self.reverse_makefiles[filename]:
242      if len(self.deps[m]) == 0:
243        return True
244    return False
245
246  def contains_blocked_modules(self, filename):
247    for m in self.reverse_makefiles[filename]:
248      if len(self.deps[m]) > 0:
249        return True
250    return False
251
252def count_deps(depsdb, module, seen):
253  """Based on the depsdb, count the number of transitive dependencies.
254
255  You can pass in an reversed dependency graph to count the number of
256  modules that depend on the module."""
257  count = 0
258  seen.append(module)
259  if module in depsdb:
260    for dep in depsdb[module]:
261      if dep in seen:
262        continue
263      count += 1 + count_deps(depsdb, dep, seen)
264  return count
265
266OTHER_PARTITON = "_other"
267HOST_PARTITON = "_host"
268
269def get_partition_from_installed(HOST_OUT_ROOT, PRODUCT_OUT, filename):
270  host_prefix = HOST_OUT_ROOT + "/"
271  device_prefix = PRODUCT_OUT + "/"
272
273  if filename.startswith(host_prefix):
274    return HOST_PARTITON
275
276  elif filename.startswith(device_prefix):
277    index = filename.find("/", len(device_prefix))
278    if index < 0:
279      return OTHER_PARTITON
280    return filename[len(device_prefix):index]
281
282  return OTHER_PARTITON
283
284def format_module_link(module):
285  return "<a class='ModuleLink' href='#module_%s'>%s</a>" % (module, module)
286
287def format_module_list(modules):
288  return "".join(["<div>%s</div>" % format_module_link(m) for m in modules])
289
290def print_analysis_header(link, title):
291  print("""
292    <a name="%(link)s"></a>
293    <h2>%(title)s</h2>
294    <table>
295      <tr>
296        <th class="RowTitle">Directory</th>
297        <th class="Count">Total</th>
298        <th class="Count Clean">Easy</th>
299        <th class="Count Clean">Unblocked Clean</th>
300        <th class="Count Unblocked">Unblocked</th>
301        <th class="Count Blocked">Blocked</th>
302        <th class="Count Clean">Clean</th>
303  """ % {
304    "link": link,
305    "title": title
306  })
307  for analyzer in ANALYZERS:
308    print("""<th class="Count Warning">%s</th>""" % analyzer.title)
309  print("      </tr>")
310
311def main():
312  parser = argparse.ArgumentParser(description="Info about remaining Android.mk files.")
313  parser.add_argument("--device", type=str, required=True,
314                      help="TARGET_DEVICE")
315  parser.add_argument("--title", type=str,
316                      help="page title")
317  parser.add_argument("--codesearch", type=str,
318                      default="https://cs.android.com/android/platform/superproject/+/master:",
319                      help="page title")
320  parser.add_argument("--out_dir", type=str,
321                      default=None,
322                      help="Equivalent of $OUT_DIR, which will also be checked if"
323                        + " --out_dir is unset. If neither is set, default is"
324                        + " 'out'.")
325  parser.add_argument("--mode", type=str,
326                      default="html",
327                      help="output format: csv or html")
328
329  args = parser.parse_args()
330
331  # Guess out directory name
332  if not args.out_dir:
333    args.out_dir = os.getenv("OUT_DIR", "out")
334  while args.out_dir.endswith("/") and len(args.out_dir) > 1:
335    args.out_dir = args.out_dir[:-1]
336
337  TARGET_DEVICE = args.device
338  global HOST_OUT_ROOT
339  HOST_OUT_ROOT = args.out_dir + "/host"
340  global PRODUCT_OUT
341  PRODUCT_OUT = args.out_dir + "/target/product/%s" % TARGET_DEVICE
342
343  # Read target information
344  # TODO: Pull from configurable location. This is also slightly different because it's
345  # only a single build, where as the tree scanning we do below is all Android.mk files.
346  with open("%s/obj/PACKAGING/soong_conversion_intermediates/soong_conv_data"
347      % PRODUCT_OUT, "r", errors="ignore") as csvfile:
348    soong = SoongData(csv.reader(csvfile))
349
350  # Read the makefiles
351  all_makefiles = dict()
352  for filename, modules in soong.reverse_makefiles.items():
353    if filename.startswith(args.out_dir + "/"):
354      continue
355    all_makefiles[filename] = Makefile(filename)
356
357  if args.mode == "html":
358    HtmlProcessor(args=args, soong=soong, all_makefiles=all_makefiles).execute()
359  elif args.mode == "csv":
360    CsvProcessor(args=args, soong=soong, all_makefiles=all_makefiles).execute()
361
362class HtmlProcessor(object):
363  def __init__(self, args, soong, all_makefiles):
364    self.args = args
365    self.soong = soong
366    self.all_makefiles = all_makefiles
367    self.annotations = Annotations()
368
369  def execute(self):
370    if self.args.title:
371      page_title = self.args.title
372    else:
373      page_title = "Remaining Android.mk files"
374
375    # Which modules are installed where
376    modules_by_partition = dict()
377    partitions = set()
378    for installed, module in self.soong.installed.items():
379      partition = get_partition_from_installed(HOST_OUT_ROOT, PRODUCT_OUT, installed)
380      modules_by_partition.setdefault(partition, []).append(module)
381      partitions.add(partition)
382
383    print("""
384    <html>
385      <head>
386        <title>%(page_title)s</title>
387        <style type="text/css">
388          body, table {
389            font-family: Roboto, sans-serif;
390            font-size: 9pt;
391          }
392          body {
393            margin: 0;
394            padding: 0;
395            display: flex;
396            flex-direction: column;
397            height: 100vh;
398          }
399          #container {
400            flex: 1;
401            display: flex;
402            flex-direction: row;
403            overflow: hidden;
404          }
405          #tables {
406            padding: 0 20px 40px 20px;
407            overflow: scroll;
408            flex: 2 2 600px;
409          }
410          #details {
411            display: none;
412            overflow: scroll;
413            flex: 1 1 650px;
414            padding: 0 20px 0 20px;
415          }
416          h1 {
417            margin: 16px 0 16px 20px;
418          }
419          h2 {
420            margin: 12px 0 4px 0;
421          }
422          .RowTitle {
423            text-align: left;
424            width: 200px;
425            min-width: 200px;
426          }
427          .Count {
428            text-align: center;
429            width: 60px;
430            min-width: 60px;
431            max-width: 60px;
432          }
433          th.Clean,
434          th.Unblocked {
435            background-color: #1e8e3e;
436          }
437          th.Blocked {
438            background-color: #d93025;
439          }
440          th.Warning {
441            background-color: #e8710a;
442          }
443          th {
444            background-color: #1a73e8;
445            color: white;
446            font-weight: bold;
447          }
448          td.Unblocked {
449            background-color: #81c995;
450          }
451          td.Blocked {
452            background-color: #f28b82;
453          }
454          td, th {
455            padding: 2px 4px;
456            border-right: 2px solid white;
457          }
458          tr.TotalRow td {
459            background-color: white;
460            border-right-color: white;
461          }
462          tr.AospDir td {
463            background-color: #e6f4ea;
464            border-right-color: #e6f4ea;
465          }
466          tr.GoogleDir td {
467            background-color: #e8f0fe;
468            border-right-color: #e8f0fe;
469          }
470          tr.PartnerDir td {
471            background-color: #fce8e6;
472            border-right-color: #fce8e6;
473          }
474          table {
475            border-spacing: 0;
476            border-collapse: collapse;
477          }
478          div.Makefile {
479            margin: 12px 0 0 0;
480          }
481          div.Makefile:first {
482            margin-top: 0;
483          }
484          div.FileModules {
485            padding: 4px 0 0 20px;
486          }
487          td.LineNo {
488            vertical-align: baseline;
489            padding: 6px 0 0 20px;
490            width: 50px;
491            vertical-align: baseline;
492          }
493          td.LineText {
494            vertical-align: baseline;
495            font-family: monospace;
496            padding: 6px 0 0 0;
497          }
498          a.CsLink {
499            font-family: monospace;
500          }
501          div.Help {
502            width: 550px;
503          }
504          table.HelpColumns tr {
505            border-bottom: 2px solid white;
506          }
507          .ModuleName {
508            vertical-align: baseline;
509            padding: 6px 0 0 20px;
510            width: 275px;
511          }
512          .ModuleDeps {
513            vertical-align: baseline;
514            padding: 6px 0 0 0;
515          }
516          table#Modules td {
517            vertical-align: baseline;
518          }
519          tr.Alt {
520            background-color: #ececec;
521          }
522          tr.Alt td {
523            border-right-color: #ececec;
524          }
525          .AnalysisCol {
526            width: 300px;
527            padding: 2px;
528            line-height: 21px;
529          }
530          .Analysis {
531            color: white;
532            font-weight: bold;
533            background-color: #e8710a;
534            border-radius: 6px;
535            margin: 4px;
536            padding: 2px 6px;
537            white-space: nowrap;
538          }
539          .Nav {
540            margin: 4px 0 16px 20px;
541          }
542          .NavSpacer {
543            display: inline-block;
544            width: 6px;
545          }
546          .ModuleDetails {
547            margin-top: 20px;
548          }
549          .ModuleDetails td {
550            vertical-align: baseline;
551          }
552        </style>
553      </head>
554      <body>
555        <h1>%(page_title)s</h1>
556        <div class="Nav">
557          <a href='#help'>Help</a>
558          <span class='NavSpacer'></span><span class='NavSpacer'> </span>
559          Partitions:
560    """ % {
561      "page_title": page_title,
562    })
563    for partition in sorted(partitions):
564      print("<a href='#partition_%s'>%s</a><span class='NavSpacer'></span>" % (partition, partition))
565
566    print("""
567          <span class='NavSpacer'></span><span class='NavSpacer'> </span>
568          <a href='#summary'>Overall Summary</a>
569        </div>
570        <div id="container">
571          <div id="tables">
572          <a name="help"></a>
573          <div class="Help">
574            <p>
575            This page analyzes the remaining Android.mk files in the Android Source tree.
576            <p>
577            The modules are first broken down by which of the device filesystem partitions
578            they are installed to. This also includes host tools and testcases which don't
579            actually reside in their own partition but convenitely group together.
580            <p>
581            The makefiles for each partition are further are grouped into a set of directories
582            aritrarily picked to break down the problem size by owners.
583            <ul style="width: 300px">
584              <li style="background-color: #e6f4ea">AOSP directories are colored green.</li>
585              <li style="background-color: #e8f0fe">Google directories are colored blue.</li>
586              <li style="background-color: #fce8e6">Other partner directories are colored red.</li>
587            </ul>
588            Each of the makefiles are scanned for issues that are likely to come up during
589            conversion to soong.  Clicking the number in each cell shows additional information,
590            including the line that triggered the warning.
591            <p>
592            <table class="HelpColumns">
593              <tr>
594                <th>Total</th>
595                <td>The total number of makefiles in this each directory.</td>
596              </tr>
597              <tr>
598                <th class="Clean">Easy</th>
599                <td>The number of makefiles that have no warnings themselves, and also
600                    none of their dependencies have warnings either.</td>
601              </tr>
602              <tr>
603                <th class="Clean">Unblocked Clean</th>
604                <td>The number of makefiles that are both Unblocked and Clean.</td>
605              </tr>
606
607              <tr>
608                <th class="Unblocked">Unblocked</th>
609                <td>Makefiles containing one or more modules that don't have any
610                    additional dependencies pending before conversion.</td>
611              </tr>
612              <tr>
613                <th class="Blocked">Blocked</th>
614                <td>Makefiles containiong one or more modules which <i>do</i> have
615                    additional prerequesite depenedencies that are not yet converted.</td>
616              </tr>
617              <tr>
618                <th class="Clean">Clean</th>
619                <td>The number of makefiles that have none of the following warnings.</td>
620              </tr>
621              <tr>
622                <th class="Warning">ifeq / ifneq</th>
623                <td>Makefiles that use <code>ifeq</code> or <code>ifneq</code>. i.e.
624                conditionals.</td>
625              </tr>
626              <tr>
627                <th class="Warning">Wacky Includes</th>
628                <td>Makefiles that <code>include</code> files other than the standard build-system
629                    defined template and macros.</td>
630              </tr>
631              <tr>
632                <th class="Warning">Calls base_rules</th>
633                <td>Makefiles that include base_rules.mk directly.</td>
634              </tr>
635              <tr>
636                <th class="Warning">Calls define</th>
637                <td>Makefiles that define their own macros. Some of these are easy to convert
638                    to soong <code>defaults</code>, but others are complex.</td>
639              </tr>
640              <tr>
641                <th class="Warning">Has ../</th>
642                <td>Makefiles containing the string "../" outside of a comment. These likely
643                    access files outside their directories.</td>
644              </tr>
645              <tr>
646                <th class="Warning">dist-for-goals</th>
647                <td>Makefiles that call <code>dist-for-goals</code> directly.</td>
648              </tr>
649              <tr>
650                <th class="Warning">.PHONY</th>
651                <td>Makefiles that declare .PHONY targets.</td>
652              </tr>
653              <tr>
654                <th class="Warning">renderscript</th>
655                <td>Makefiles defining targets that depend on <code>.rscript</code> source files.</td>
656              </tr>
657              <tr>
658                <th class="Warning">vts src</th>
659                <td>Makefiles defining targets that depend on <code>.vts</code> source files.</td>
660              </tr>
661              <tr>
662                <th class="Warning">COPY_HEADERS</th>
663                <td>Makefiles using LOCAL_COPY_HEADERS.</td>
664              </tr>
665            </table>
666            <p>
667            Following the list of directories is a list of the modules that are installed on
668            each partition. Potential issues from their makefiles are listed, as well as the
669            total number of dependencies (both blocking that module and blocked by that module)
670            and the list of direct dependencies.  Note: The number is the number of all transitive
671            dependencies and the list of modules is only the direct dependencies.
672          </div>
673    """)
674
675    overall_summary = Summary()
676
677    # For each partition
678    for partition in sorted(partitions):
679      modules = modules_by_partition[partition]
680
681      makefiles = set(itertools.chain.from_iterable(
682          [self.soong.makefiles[module] for module in modules]))
683
684      # Read makefiles
685      summary = Summary()
686      for filename in makefiles:
687        makefile = self.all_makefiles.get(filename)
688        if makefile:
689          summary.Add(makefile)
690          overall_summary.Add(makefile)
691
692      # Categorize directories by who is responsible
693      aosp_dirs = []
694      google_dirs = []
695      partner_dirs = []
696      for dirname in sorted(summary.directories.keys()):
697        if is_aosp(dirname):
698          aosp_dirs.append(dirname)
699        elif is_google(dirname):
700          google_dirs.append(dirname)
701        else:
702          partner_dirs.append(dirname)
703
704      print_analysis_header("partition_" + partition, partition)
705
706      for dirgroup, rowclass in [(aosp_dirs, "AospDir"),
707                                 (google_dirs, "GoogleDir"),
708                                 (partner_dirs, "PartnerDir"),]:
709        for dirname in dirgroup:
710          self.print_analysis_row(summary, modules,
711                               dirname, rowclass, summary.directories[dirname])
712
713      self.print_analysis_row(summary, modules,
714                           "Total", "TotalRow",
715                           set(itertools.chain.from_iterable(summary.directories.values())))
716      print("""
717        </table>
718      """)
719
720      module_details = [(count_deps(self.soong.deps, m, []),
721                         -count_deps(self.soong.reverse_deps, m, []), m)
722                 for m in modules]
723      module_details.sort()
724      module_details = [m[2] for m in module_details]
725      print("""
726        <table class="ModuleDetails">""")
727      print("<tr>")
728      print("  <th>Module Name</th>")
729      print("  <th>Issues</th>")
730      print("  <th colspan='2'>Blocked By</th>")
731      print("  <th colspan='2'>Blocking</th>")
732      print("</tr>")
733      altRow = True
734      for module in module_details:
735        analyses = set()
736        for filename in self.soong.makefiles[module]:
737          makefile = summary.makefiles.get(filename)
738          if makefile:
739            for analyzer, analysis in makefile.analyses.items():
740              if analysis:
741                analyses.add(analyzer.title)
742
743        altRow = not altRow
744        print("<tr class='%s'>" % ("Alt" if altRow else "",))
745        print("  <td><a name='module_%s'></a>%s</td>" % (module, module))
746        print("  <td class='AnalysisCol'>%s</td>" % " ".join(["<span class='Analysis'>%s</span>" % title
747            for title in analyses]))
748        print("  <td>%s</td>" % count_deps(self.soong.deps, module, []))
749        print("  <td>%s</td>" % format_module_list(self.soong.deps.get(module, [])))
750        print("  <td>%s</td>" % count_deps(self.soong.reverse_deps, module, []))
751        print("  <td>%s</td>" % format_module_list(self.soong.reverse_deps.get(module, [])))
752        print("</tr>")
753      print("""</table>""")
754
755    print_analysis_header("summary", "Overall Summary")
756
757    modules = [module for installed, module in self.soong.installed.items()]
758    self.print_analysis_row(overall_summary, modules,
759                         "All Makefiles", "TotalRow",
760                         set(itertools.chain.from_iterable(overall_summary.directories.values())))
761    print("""
762        </table>
763    """)
764
765    print("""
766      <script type="text/javascript">
767      function close_details() {
768        document.getElementById('details').style.display = 'none';
769      }
770
771      class LineMatch {
772        constructor(lineno, text) {
773          this.lineno = lineno;
774          this.text = text;
775        }
776      }
777
778      class Analysis {
779        constructor(filename, modules, line_matches) {
780          this.filename = filename;
781          this.modules = modules;
782          this.line_matches = line_matches;
783        }
784      }
785
786      class Module {
787        constructor(deps) {
788          this.deps = deps;
789        }
790      }
791
792      function make_module_link(module) {
793        var a = document.createElement('a');
794        a.className = 'ModuleLink';
795        a.innerText = module;
796        a.href = '#module_' + module;
797        return a;
798      }
799
800      function update_details(id) {
801        document.getElementById('details').style.display = 'block';
802
803        var analyses = ANALYSIS[id];
804
805        var details = document.getElementById("details_data");
806        while (details.firstChild) {
807            details.removeChild(details.firstChild);
808        }
809
810        for (var i=0; i<analyses.length; i++) {
811          var analysis = analyses[i];
812
813          var makefileDiv = document.createElement('div');
814          makefileDiv.className = 'Makefile';
815          details.appendChild(makefileDiv);
816
817          var fileA = document.createElement('a');
818          makefileDiv.appendChild(fileA);
819          fileA.className = 'CsLink';
820          fileA.href = '%(codesearch)s' + analysis.filename;
821          fileA.innerText = analysis.filename;
822          fileA.target = "_blank";
823
824          if (analysis.modules.length > 0) {
825            var moduleTable = document.createElement('table');
826            details.appendChild(moduleTable);
827
828            for (var j=0; j<analysis.modules.length; j++) {
829              var moduleRow = document.createElement('tr');
830              moduleTable.appendChild(moduleRow);
831
832              var moduleNameCell = document.createElement('td');
833              moduleRow.appendChild(moduleNameCell);
834              moduleNameCell.className = 'ModuleName';
835              moduleNameCell.appendChild(make_module_link(analysis.modules[j]));
836
837              var moduleData = MODULE_DATA[analysis.modules[j]];
838              console.log(moduleData);
839
840              var depCell = document.createElement('td');
841              moduleRow.appendChild(depCell);
842
843              if (moduleData.deps.length == 0) {
844                depCell.className = 'ModuleDeps Unblocked';
845                depCell.innerText = 'UNBLOCKED';
846              } else {
847                depCell.className = 'ModuleDeps Blocked';
848
849                for (var k=0; k<moduleData.deps.length; k++) {
850                  depCell.appendChild(make_module_link(moduleData.deps[k]));
851                  depCell.appendChild(document.createElement('br'));
852                }
853              }
854            }
855          }
856
857          if (analysis.line_matches.length > 0) {
858            var lineTable = document.createElement('table');
859            details.appendChild(lineTable);
860
861            for (var j=0; j<analysis.line_matches.length; j++) {
862              var line_match = analysis.line_matches[j];
863
864              var lineRow = document.createElement('tr');
865              lineTable.appendChild(lineRow);
866
867              var linenoCell = document.createElement('td');
868              lineRow.appendChild(linenoCell);
869              linenoCell.className = 'LineNo';
870
871              var linenoA = document.createElement('a');
872              linenoCell.appendChild(linenoA);
873              linenoA.className = 'CsLink';
874              linenoA.href = '%(codesearch)s' + analysis.filename
875                  + ';l=' + line_match.lineno;
876              linenoA.innerText = line_match.lineno;
877              linenoA.target = "_blank";
878
879              var textCell = document.createElement('td');
880              lineRow.appendChild(textCell);
881              textCell.className = 'LineText';
882              textCell.innerText = line_match.text;
883            }
884          }
885        }
886      }
887
888      var ANALYSIS = [
889      """ % {
890          "codesearch": self.args.codesearch,
891      })
892    for entry, mods in self.annotations.entries:
893      print("  [")
894      for analysis in entry:
895        print("    new Analysis('%(filename)s', %(modules)s, [%(line_matches)s])," % {
896          "filename": analysis.filename,
897          #"modules": json.dumps([m for m in mods if m in filename in self.soong.makefiles[m]]),
898          "modules": json.dumps(
899              [m for m in self.soong.reverse_makefiles[analysis.filename] if m in mods]),
900          "line_matches": ", ".join([
901              "new LineMatch(%d, %s)" % (lineno, json.dumps(text))
902              for lineno, text in analysis.line_matches]),
903        })
904      print("  ],")
905    print("""
906      ];
907      var MODULE_DATA = {
908    """)
909    for module in self.soong.modules:
910      print("      '%(name)s': new Module(%(deps)s)," % {
911        "name": module,
912        "deps": json.dumps(self.soong.deps[module]),
913      })
914    print("""
915      };
916      </script>
917
918    """)
919
920    print("""
921        </div> <!-- id=tables -->
922        <div id="details">
923          <div style="text-align: right;">
924            <a href="javascript:close_details();">
925              <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
926            </a>
927          </div>
928          <div id="details_data"></div>
929        </div>
930      </body>
931    </html>
932    """)
933
934  def traverse_ready_makefiles(self, summary, makefiles):
935    return [Analysis(makefile.filename, []) for makefile in makefiles
936        if clean_and_only_blocked_by_clean(self.soong, self.all_makefiles, makefile)]
937
938  def print_analysis_row(self, summary, modules, rowtitle, rowclass, makefiles):
939    all_makefiles = [Analysis(makefile.filename, []) for makefile in makefiles]
940    clean_makefiles = [Analysis(makefile.filename, []) for makefile in makefiles
941        if is_clean(makefile)]
942    easy_makefiles = self.traverse_ready_makefiles(summary, makefiles)
943    unblocked_clean_makefiles = [Analysis(makefile.filename, []) for makefile in makefiles
944        if (self.soong.contains_unblocked_modules(makefile.filename)
945            and is_clean(makefile))]
946    unblocked_makefiles = [Analysis(makefile.filename, []) for makefile in makefiles
947        if self.soong.contains_unblocked_modules(makefile.filename)]
948    blocked_makefiles = [Analysis(makefile.filename, []) for makefile in makefiles
949        if self.soong.contains_blocked_modules(makefile.filename)]
950
951    print("""
952      <tr class="%(rowclass)s">
953        <td class="RowTitle">%(rowtitle)s</td>
954        <td class="Count">%(makefiles)s</td>
955        <td class="Count">%(easy)s</td>
956        <td class="Count">%(unblocked_clean)s</td>
957        <td class="Count">%(unblocked)s</td>
958        <td class="Count">%(blocked)s</td>
959        <td class="Count">%(clean)s</td>
960    """ % {
961      "rowclass": rowclass,
962      "rowtitle": rowtitle,
963      "makefiles": self.make_annotation_link(all_makefiles, modules),
964      "unblocked": self.make_annotation_link(unblocked_makefiles, modules),
965      "blocked": self.make_annotation_link(blocked_makefiles, modules),
966      "clean": self.make_annotation_link(clean_makefiles, modules),
967      "unblocked_clean": self.make_annotation_link(unblocked_clean_makefiles, modules),
968      "easy": self.make_annotation_link(easy_makefiles, modules),
969    })
970
971    for analyzer in ANALYZERS:
972      analyses = [m.analyses.get(analyzer) for m in makefiles if m.analyses.get(analyzer)]
973      print("""<td class="Count">%s</td>"""
974          % self.make_annotation_link(analyses, modules))
975
976    print("      </tr>")
977
978  def make_annotation_link(self, analysis, modules):
979    if analysis:
980      return "<a href='javascript:update_details(%d)'>%s</a>" % (
981        self.annotations.Add(analysis, modules),
982        len(analysis)
983      )
984    else:
985      return "";
986
987class CsvProcessor(object):
988  def __init__(self, args, soong, all_makefiles):
989    self.args = args
990    self.soong = soong
991    self.all_makefiles = all_makefiles
992
993  def execute(self):
994    csvout = csv.writer(sys.stdout)
995
996    # Title row
997    row = ["Filename", "Module", "Partitions", "Easy", "Unblocked Clean", "Unblocked",
998           "Blocked", "Clean"]
999    for analyzer in ANALYZERS:
1000      row.append(analyzer.title)
1001    csvout.writerow(row)
1002
1003    # Makefile & module data
1004    for filename in sorted(self.all_makefiles.keys()):
1005      makefile = self.all_makefiles[filename]
1006      for module in self.soong.reverse_makefiles[filename]:
1007        row = [filename, module]
1008        # Partitions
1009        row.append(";".join(sorted(set([get_partition_from_installed(HOST_OUT_ROOT, PRODUCT_OUT,
1010                                         installed)
1011                                        for installed
1012                                        in self.soong.reverse_installed.get(module, [])]))))
1013        # Easy
1014        row.append(1
1015            if clean_and_only_blocked_by_clean(self.soong, self.all_makefiles, makefile)
1016            else "")
1017        # Unblocked Clean
1018        row.append(1
1019            if (self.soong.contains_unblocked_modules(makefile.filename) and is_clean(makefile))
1020            else "")
1021        # Unblocked
1022        row.append(1 if self.soong.contains_unblocked_modules(makefile.filename) else "")
1023        # Blocked
1024        row.append(1 if self.soong.contains_blocked_modules(makefile.filename) else "")
1025        # Clean
1026        row.append(1 if is_clean(makefile) else "")
1027        # Analysis
1028        for analyzer in ANALYZERS:
1029          row.append(1 if makefile.analyses.get(analyzer) else "")
1030        # Write results
1031        csvout.writerow(row)
1032
1033if __name__ == "__main__":
1034  main()
1035
1036