• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2
3from enum import Enum
4from pathlib import Path
5from typing import Sequence
6from typing import Tuple
7from fontTools import ttLib
8import tempfile
9import subprocess
10import json
11import argparse
12import contextlib
13import os
14import re
15import sys
16
17# list of specific files to be ignored.
18IGNORE_FILE_NAME = [
19  # Exclude myself
20  "generate_notice.py",
21
22  # License files
23  "LICENSE",
24  "LICENSE_APACHE2.TXT",
25  "LICENSE_FSFAP.TXT",
26  "LICENSE_GPLv2.TXT",
27  "LICENSE_GPLv2_WITH_AUTOCONF_EXCEPTION.TXT",
28  "LICENSE_GPLv3_WITH_AUTOCONF_EXCEPTION.TXT",
29  "LICENSE_HPND_SELL_VARIANT.TXT",
30  "LICENSE_ISC.TXT",
31  "LICENSE_MIT_MODERN_VARIANT.TXT",
32  "LICENSE_OFL.TXT",
33  "METADATA",
34  "MODULE_LICENSE_MIT",
35  "NOTICE",
36
37  # dictionary which has Copyright word
38  "perf/texts/en-words.txt",
39
40  # broken unreadable font file for fuzzing target
41  "test/fuzzing/fonts/sbix-extents.ttf",
42]
43
44IGNORE_DIR_IF_NO_COPYRIGHT = [
45    "test",
46    "perf",
47]
48
49NO_COPYRIGHT_FILES = [
50  ".ci/build-win32.sh",
51  ".ci/build-win64.sh",
52  ".ci/deploy-docs.sh",
53  ".ci/publish_release_artifact.sh",
54  ".ci/requirements-fonttools.in",
55  ".ci/requirements-fonttools.txt",
56  ".ci/requirements.in",
57  ".ci/requirements.txt",
58  ".ci/win32-cross-file.txt",
59  ".ci/win64-cross-file.txt",
60  ".circleci/config.yml",
61  ".clang-format",
62  ".codecov.yml",
63  ".editorconfig",
64  ".github/dependabot.yml",
65  ".github/workflows/arm-ci.yml",
66  ".github/workflows/cifuzz.yml",
67  ".github/workflows/configs-build.yml",
68  ".github/workflows/coverity-scan.yml",
69  ".github/workflows/linux-ci.yml",
70  ".github/workflows/macos-ci.yml",
71  ".github/workflows/msvc-ci.yml",
72  ".github/workflows/msys2-ci.yml",
73  ".github/workflows/scorecard.yml",
74  "AUTHORS",
75  "BUILD.md",
76  "CMakeLists.txt",
77  "CONFIG.md",
78  "Makefile.am",
79  "NEWS",
80  "OWNERS",
81  "README",
82  "README.android",
83  "README.md",
84  "README.mingw.md",
85  "README.python.md",
86  "RELEASING.md",
87  "SECURITY.md",
88  "TESTING.md",
89  "TEST_MAPPING",
90  "THANKS",
91  "autogen.sh",
92  "configure.ac",
93  "docs/HarfBuzz.png",
94  "docs/HarfBuzz.svg",
95  "docs/Makefile.am",
96  "docs/features.dot",
97  "docs/harfbuzz-docs.xml",
98  "docs/harfbuzz-overrides.txt",
99  "docs/harfbuzz-sections.txt",
100  "docs/meson.build",
101  "docs/repacker.md",
102  "docs/serializer.md",
103  "docs/subset-preprocessing.md",
104  "docs/usermanual-buffers-language-script-and-direction.xml",
105  "docs/usermanual-clusters.xml",
106  "docs/usermanual-fonts-and-faces.xml",
107  "docs/usermanual-getting-started.xml",
108  "docs/usermanual-glyph-information.xml",
109  "docs/usermanual-install-harfbuzz.xml",
110  "docs/usermanual-integration.xml",
111  "docs/usermanual-object-model.xml",
112  "docs/usermanual-opentype-features.xml",
113  "docs/usermanual-shaping-concepts.xml",
114  "docs/usermanual-utilities.xml",
115  "docs/usermanual-what-is-harfbuzz.xml",
116  "docs/version.xml.in",
117  "docs/wasm-shaper.md",
118  "harfbuzz.doap",
119  "meson.build",
120  "meson_options.txt",
121  "mingw-configure.sh",
122  "replace-enum-strings.cmake",
123  "src/ArabicPUASimplified.txt",
124  "src/ArabicPUATraditional.txt",
125  "src/Makefile.am",
126  "src/Makefile.sources",
127  "src/OT/Layout/GPOS/Anchor.hh",
128  "src/OT/Layout/GPOS/AnchorFormat1.hh",
129  "src/OT/Layout/GPOS/AnchorFormat2.hh",
130  "src/OT/Layout/GPOS/AnchorFormat3.hh",
131  "src/OT/Layout/GPOS/AnchorMatrix.hh",
132  "src/OT/Layout/GPOS/ChainContextPos.hh",
133  "src/OT/Layout/GPOS/Common.hh",
134  "src/OT/Layout/GPOS/ContextPos.hh",
135  "src/OT/Layout/GPOS/CursivePos.hh",
136  "src/OT/Layout/GPOS/CursivePosFormat1.hh",
137  "src/OT/Layout/GPOS/ExtensionPos.hh",
138  "src/OT/Layout/GPOS/GPOS.hh",
139  "src/OT/Layout/GPOS/LigatureArray.hh",
140  "src/OT/Layout/GPOS/MarkArray.hh",
141  "src/OT/Layout/GPOS/MarkBasePos.hh",
142  "src/OT/Layout/GPOS/MarkBasePosFormat1.hh",
143  "src/OT/Layout/GPOS/MarkLigPos.hh",
144  "src/OT/Layout/GPOS/MarkLigPosFormat1.hh",
145  "src/OT/Layout/GPOS/MarkMarkPos.hh",
146  "src/OT/Layout/GPOS/MarkMarkPosFormat1.hh",
147  "src/OT/Layout/GPOS/MarkRecord.hh",
148  "src/OT/Layout/GPOS/PairPos.hh",
149  "src/OT/Layout/GPOS/PairPosFormat1.hh",
150  "src/OT/Layout/GPOS/PairPosFormat2.hh",
151  "src/OT/Layout/GPOS/PairSet.hh",
152  "src/OT/Layout/GPOS/PairValueRecord.hh",
153  "src/OT/Layout/GPOS/PosLookup.hh",
154  "src/OT/Layout/GPOS/PosLookupSubTable.hh",
155  "src/OT/Layout/GPOS/SinglePos.hh",
156  "src/OT/Layout/GPOS/SinglePosFormat1.hh",
157  "src/OT/Layout/GPOS/SinglePosFormat2.hh",
158  "src/OT/Layout/GPOS/ValueFormat.hh",
159  "src/OT/Layout/GSUB/AlternateSet.hh",
160  "src/OT/Layout/GSUB/AlternateSubst.hh",
161  "src/OT/Layout/GSUB/AlternateSubstFormat1.hh",
162  "src/OT/Layout/GSUB/ChainContextSubst.hh",
163  "src/OT/Layout/GSUB/Common.hh",
164  "src/OT/Layout/GSUB/ContextSubst.hh",
165  "src/OT/Layout/GSUB/ExtensionSubst.hh",
166  "src/OT/Layout/GSUB/GSUB.hh",
167  "src/OT/Layout/GSUB/Ligature.hh",
168  "src/OT/Layout/GSUB/LigatureSet.hh",
169  "src/OT/Layout/GSUB/LigatureSubst.hh",
170  "src/OT/Layout/GSUB/LigatureSubstFormat1.hh",
171  "src/OT/Layout/GSUB/MultipleSubst.hh",
172  "src/OT/Layout/GSUB/MultipleSubstFormat1.hh",
173  "src/OT/Layout/GSUB/ReverseChainSingleSubst.hh",
174  "src/OT/Layout/GSUB/ReverseChainSingleSubstFormat1.hh",
175  "src/OT/Layout/GSUB/Sequence.hh",
176  "src/OT/Layout/GSUB/SingleSubst.hh",
177  "src/OT/Layout/GSUB/SingleSubstFormat1.hh",
178  "src/OT/Layout/GSUB/SingleSubstFormat2.hh",
179  "src/OT/Layout/GSUB/SubstLookup.hh",
180  "src/OT/Layout/GSUB/SubstLookupSubTable.hh",
181  "src/OT/glyf/CompositeGlyph.hh",
182  "src/OT/glyf/Glyph.hh",
183  "src/OT/glyf/GlyphHeader.hh",
184  "src/OT/glyf/SimpleGlyph.hh",
185  "src/OT/glyf/SubsetGlyph.hh",
186  "src/OT/glyf/VarCompositeGlyph.hh",
187  "src/OT/glyf/composite-iter.hh",
188  "src/OT/glyf/coord-setter.hh",
189  "src/OT/glyf/glyf-helpers.hh",
190  "src/OT/glyf/glyf.hh",
191  "src/OT/glyf/loca.hh",
192  "src/OT/glyf/path-builder.hh",
193  "src/addTable.py",
194  "src/check-c-linkage-decls.py",
195  "src/check-externs.py",
196  "src/check-header-guards.py",
197  "src/check-includes.py",
198  "src/check-libstdc++.py",
199  "src/check-static-inits.py",
200  "src/check-symbols.py",
201  "src/fix_get_types.py",
202  "src/gen-arabic-joining-list.py",
203  "src/gen-arabic-pua.py",
204  "src/gen-arabic-table.py",
205  "src/gen-def.py",
206  "src/gen-emoji-table.py",
207  "src/gen-harfbuzzcc.py",
208  "src/gen-hb-version.py",
209  "src/gen-indic-table.py",
210  "src/gen-os2-unicode-ranges.py",
211  "src/gen-ragel-artifacts.py",
212  "src/gen-tag-table.py",
213  "src/gen-ucd-table.py",
214  "src/gen-use-table.py",
215  "src/gen-vowel-constraints.py",
216  "src/harfbuzz-cairo.pc.in",
217  "src/harfbuzz-config.cmake.in",
218  "src/harfbuzz-gobject.pc.in",
219  "src/harfbuzz-icu.pc.in",
220  "src/harfbuzz-subset.cc",
221  "src/harfbuzz-subset.pc.in",
222  "src/harfbuzz.cc",
223  "src/harfbuzz.pc.in",
224  "src/hb-ot-shaper-arabic-joining-list.hh",
225  "src/hb-ot-shaper-arabic-pua.hh",
226  "src/hb-ot-shaper-arabic-table.hh",
227  "src/hb-ot-shaper-indic-table.cc",
228  "src/hb-ot-shaper-use-table.hh",
229  "src/hb-ot-shaper-vowel-constraints.cc",
230  "src/hb-ot-tag-table.hh",
231  "src/hb-ucd-table.hh",
232  "src/hb-unicode-emoji-table.hh",
233  "src/justify.py",
234  "src/meson.build",
235  "src/ms-use/IndicPositionalCategory-Additional.txt",
236  "src/ms-use/IndicShapingInvalidCluster.txt",
237  "src/ms-use/IndicSyllabicCategory-Additional.txt",
238  "src/relative_to.py",
239  "src/sample.py",
240  "src/test-use-table.cc",
241  "src/update-unicode-tables.make",
242  "src/wasm/graphite/Makefile",
243  "src/wasm/graphite/shape.cc",
244  "src/wasm/rust/harfbuzz-wasm/Cargo.toml",
245  "src/wasm/rust/harfbuzz-wasm/src/lib.rs",
246  "src/wasm/sample/c/Makefile",
247  "src/wasm/sample/c/shape-fallback.cc",
248  "src/wasm/sample/c/shape-ot.cc",
249  "src/wasm/sample/rust/hello-wasm/Cargo.toml",
250  "src/wasm/sample/rust/hello-wasm/src/lib.rs",
251  "subprojects/.gitignore",
252  "subprojects/cairo.wrap",
253  "subprojects/freetype2.wrap",
254  "subprojects/glib.wrap",
255  "subprojects/google-benchmark.wrap",
256  "subprojects/packagefiles/ragel/meson.build",
257  "subprojects/ragel.wrap",
258  "util/Makefile.am",
259  "util/Makefile.sources",
260  "util/meson.build",
261]
262
263class CommentType(Enum):
264  C_STYLE_BLOCK = 1  # /* ... */
265  C_STYLE_BLOCK_AS_LINE = 2  # /* ... */ but uses multiple lines of block comments.
266  C_STYLE_LINE = 3 # // ...
267  SCRIPT_STYLE_HASH = 4 #  # ...
268  OPENTYPE_NAME = 5
269  OPENTYPE_COLLECTION_NAME = 6
270  UNKNOWN = 10000
271
272
273# Helper function of showing error message and immediate exit.
274def fatal(msg: str):
275  sys.stderr.write(str(msg))
276  sys.stderr.write("\n")
277  sys.exit(1)
278
279
280def warn(msg: str):
281  sys.stderr.write(str(msg))
282  sys.stderr.write("\n")
283
284def debug(msg: str):
285  # sys.stderr.write(str(msg))
286  # sys.stderr.write("\n")
287  pass
288
289
290def cleanup_and_join(out_lines: Sequence[str]):
291  while not out_lines[-1].strip():
292    out_lines.pop(-1)
293
294  # If all lines starts from empty space, strip it out.
295  while all([len(x) == 0 or x[0] == ' ' for x in out_lines]):
296    out_lines = [x[1:] for x in out_lines]
297
298  if not out_lines:
299    fatal("Failed to get copyright info")
300  return "\n".join(out_lines)
301
302
303def get_comment_type(copyright_line: str, path_str: str) -> CommentType:
304  # vms_make.com contains multiple copyright header as a string constants.
305  if copyright_line.startswith("#"):
306    return CommentType.SCRIPT_STYLE_HASH
307  if copyright_line.startswith("//"):
308    return CommentType.C_STYLE_LINE
309  return CommentType.C_STYLE_BLOCK
310
311def extract_copyright_font(path_str: str) -> str:
312  path = Path(path_str)
313  if path.suffix in ['.ttf', '.otf', '.dfont']:
314    return extract_from_opentype_name(path, 0)
315  elif path.suffix in ['.ttc', '.otc']:
316    return extract_from_opentype_collection_name(path)
317
318
319# Extract copyright notice and returns next index.
320def extract_copyright_at(lines: Sequence[str], i: int, path: str) -> Tuple[str, int]:
321  commentType = get_comment_type(lines[i], path)
322
323  if commentType == CommentType.C_STYLE_BLOCK:
324    return extract_from_c_style_block_at(lines, i, path)
325  elif commentType == CommentType.C_STYLE_LINE:
326    return extract_from_c_style_lines_at(lines, i, path)
327  elif commentType == CommentType.SCRIPT_STYLE_HASH:
328    return extract_from_script_hash_at(lines, i, path)
329  else:
330    fatal("Uknown comment style: %s" % lines[i])
331
332def extract_from_opentype_collection_name(path: str) -> str:
333
334  with open(path, mode="rb") as f:
335    head = f.read(12)
336
337  if head[0:4].decode() != 'ttcf':
338    fatal('Invalid magic number for TTC file: %s' % path)
339  numFonts = int.from_bytes(head[8:12], byteorder="big")
340
341  licenses = set()
342  for i in range(0, numFonts):
343    license = extract_from_opentype_name(path, i)
344    licenses.add(license)
345
346  return '\n\n'.join(licenses)
347
348def extract_from_opentype_name(path: str, index: int) -> str:
349
350  def get_preferred_name(nameID: int, ttf):
351    def get_score(platID: int, encID: int):
352      if platID == 3 and encID == 10:
353        return 0
354      elif platID == 0 and encID == 6:
355        return 1
356      elif platID == 0 and encID == 4:
357        return 2
358      elif platID == 3 and encID == 1:
359        return 3
360      elif platID == 0 and encID == 3:
361        return 4
362      elif platID == 0 and encID == 2:
363        return 5
364      elif platID == 0 and encID == 1:
365        return 6
366      elif platID == 0 and encID == 0:
367        return 7
368      else:
369        return 10000
370
371    best_score = 1000000
372    best_name = None
373
374    if 'name' not in ttf:
375      return None
376
377    for name in ttf['name'].names:
378      if name.nameID != nameID:
379        continue
380
381      score = get_score(name.platformID, name.platEncID)
382      if score < best_score:
383        best_score = score
384        best_name = name
385
386    return best_name
387
388  def get_notice_from_cff(ttf):
389    if 'CFF ' not in ttf:
390      return None
391
392    # Looks like there is no way of getting Notice line in CFF table.
393    # Use the line that has "Copyright" in the string pool.
394    cff = ttf['CFF '].cff
395    for string in cff.strings:
396      if 'Copyright' in string:
397        return string
398    return None
399
400  with contextlib.closing(ttLib.TTFont(path, 0, fontNumber=index)) as ttf:
401    copyright = get_preferred_name(0, ttf)
402    if not copyright:
403      copyright = get_notice_from_cff(ttf)
404    if not copyright:
405      return None
406
407    license_description = get_preferred_name(13, ttf)
408
409    if license_description:
410      copyright = str(copyright) + "\n\n" + str(license_description)
411    else:
412      copyright = str(copyright)
413
414    license_url = get_preferred_name(14, ttf)
415
416    if license_url:
417      copyright = str(copyright) + "\n\n" + str(license_url)
418    else:
419      copyright = str(copyright)
420
421    return copyright
422
423def extract_from_c_style_lines_at(
424    lines: Sequence[str], i: int, path: str) -> Tuple[str, int]:
425  def is_copyright_end(line):
426    if line.startswith("//"):
427      return False
428    else:
429      return True
430  start = i
431  while i < len(lines):
432    if is_copyright_end(lines[i]):
433      break
434    i += 1
435  end = i
436
437  if start == end:
438    fatal("Failed to get copyright info")
439
440  out_lines = []
441  for line in lines[start:end]:
442    if line.startswith("//# "):  # Andorid.bp uses //# style
443      out_lines.append(line[4:])
444    elif line.startswith("//#"):  # Andorid.bp uses //# style
445      out_lines.append(line[3:])
446    elif line.startswith("// "):
447      out_lines.append(line[3:])
448    elif line == "//":
449      out_lines.append(line[2:])
450    else:
451      out_lines.append(line)
452
453  return (cleanup_and_join(out_lines), i + 1)
454
455
456def extract_from_script_hash_at(
457    lines: Sequence[str], i: int, path: str) -> Tuple[str, int]:
458  if lines[i].strip()[0] != "#":
459    return (None, i + 1)
460  def is_copyright_end(lines: str, i: int) -> bool:
461    if "#" not in lines[i]:
462      return True
463    # treat double spacing as end of license header
464    if lines[i] == "#" and lines[i+1] == "#":
465      return True
466    return False
467
468  start = i
469  while i < len(lines):
470    if is_copyright_end(lines, i):
471      break
472    i += 1
473  end = i
474
475  if start == end:
476    fatal("Failed to get copyright info")
477
478  out_lines = []
479  for line in lines[start:end]:
480    if line.startswith("# "):
481      out_lines.append(line[2:])
482    elif line == "#":
483      out_lines.append(line[1:])
484    else:
485      out_lines.append(line)
486
487  return (cleanup_and_join(out_lines), i + 1)
488
489
490def extract_from_c_style_block_at(
491    lines: Sequence[str], i: int, path: str) -> Tuple[str, int]:
492
493  def is_copyright_end(lines: str, i: int) -> bool:
494    if "*/" in lines[i]:
495      return True
496    if lines[i] == " *" and lines[i + 1] == " *":
497      return True
498    if lines[i] == "" and lines[i + 1] == "":
499      return True
500    return False
501
502  start = i
503  i += 1 # include at least one line
504  while i < len(lines):
505    if is_copyright_end(lines, i):
506      break
507    i += 1
508  end = i + 1
509
510  out_lines = []
511  for line in lines[start:end]:
512    clean_line = line
513
514    # Strip begining "/*" chars
515    if clean_line.startswith("/* "):
516      clean_line = clean_line[3:]
517    if clean_line == "/*":
518      clean_line = clean_line[2:]
519
520    # Strip ending "*/" chars
521    if clean_line.endswith(" */"):
522      clean_line = clean_line[:-3]
523    if clean_line.endswith("*/"):
524      clean_line = clean_line[:-2]
525
526    # Strip starting " *" chars
527    if clean_line.startswith(" * "):
528      clean_line = clean_line[3:]
529    if clean_line == " *":
530      clean_line = clean_line[2:]
531
532    # hb-aots-tester.cpp has underline separater which can be dropped.
533    if path.endswith("test/shape/data/aots/hb-aots-tester.cpp"):
534      clean_line = clean_line.replace("_", "")
535
536    # Strip trailing spaces
537    clean_line = clean_line.rstrip()
538
539    out_lines.append(clean_line)
540
541  return (cleanup_and_join(out_lines), i + 1)
542
543
544# Returns true if the line shows the start of copyright notice.
545def is_copyright_line(line: str, path: str) -> bool:
546  if "Copyright" not in line:
547    return False
548
549  # For avoiding unexpected mismatches, exclude quoted Copyright string.
550  if "`Copyright'" in line:
551    return False
552  if "\"Copyright\"" in line:
553    return False
554
555  if "OpCode_Copyright" in line:
556    return False
557
558  if path.endswith("src/hb-ot-name.h") and "HB_OT_NAME_ID_COPYRIGHT" in line:
559    return False
560
561  return True
562
563def assert_mandatory_copyright(path_str: str):
564  path = Path(path_str)
565  toplevel_dir = str(path).split(os.sep)[0]
566
567  if toplevel_dir in IGNORE_DIR_IF_NO_COPYRIGHT:
568    return
569
570  fatal("%s does not contain Copyright line" % path)
571
572
573# Extract the copyright notice and put it into copyrights arg.
574def do_file(path: str, copyrights: set, no_copyright_files: set):
575  raw = Path(path).read_bytes()
576  basename = os.path.basename(path)
577  dirname = os.path.dirname(path)
578
579  is_font = (dirname.endswith('./test/fuzzing/fonts') or
580             Path(path).suffix in ['.ttf', '.otf', '.dfont', '.ttc', '.otc'])
581
582  if is_font:
583    notice = extract_copyright_font(path)
584    if not notice:
585      assert_mandatory_copyright(path)
586      return
587
588    if not notice in copyrights:
589      copyrights[notice] = []
590    copyrights[notice].append(path)
591  else:
592    try:
593      content = raw.decode("utf-8")
594    except UnicodeDecodeError:
595      content = raw.decode("iso-8859-1")
596
597    if not "Copyright" in content:
598      if path in no_copyright_files:
599        no_copyright_files.remove(path)
600      else:
601        assert_mandatory_copyright(path)
602      return
603
604    lines = content.splitlines()
605
606    # The COPYING in the in-house dir has full OFL license with description.
607    # Use the OFL license description body.
608    if path.endswith("test/shape/data/in-house/COPYING") or path.endswith("test/COPYING"):
609      notice = cleanup_and_join(lines[9:])
610      copyrights.setdefault(notice, [])
611      copyrights[notice].append(path)
612      return
613
614    # The COPYING in the top dir has MIT-Modern-Variant license with description.
615    # Use the entire file as a license notice.
616    if path.endswith("COPYING") and str(Path(path)) == 'COPYING':
617      notice = cleanup_and_join(lines)
618      copyrights.setdefault(notice, [])
619      copyrights[notice].append(path)
620      return
621
622    i = 0
623    license_found = False
624    while i < len(lines):
625      if is_copyright_line(lines[i], path):
626        (notice, nexti) = extract_copyright_at(lines, i, path)
627        if notice:
628          copyrights.setdefault(notice, [])
629          copyrights[notice].append(path)
630          license_found = True
631
632        i = nexti
633      else:
634        i += 1
635
636    if not license_found:
637      assert_mandatory_copyright(path)
638
639def do_check(path, format):
640  if not path.endswith('/'): # make sure the path ends with slash
641    path = path + '/'
642
643  file_to_ignore = set([os.path.join(path, x) for x in IGNORE_FILE_NAME])
644  no_copyright_files = set([os.path.join(path, x) for x in NO_COPYRIGHT_FILES])
645  copyrights = {}
646
647  for directory, sub_directories, filenames in os.walk(path):
648    # skip .git directory
649    if ".git" in sub_directories:
650      sub_directories.remove(".git")
651
652    for fname in filenames:
653      fpath = os.path.join(directory, fname)
654      if fpath in file_to_ignore:
655        file_to_ignore.remove(fpath)
656        continue
657
658      do_file(fpath, copyrights, no_copyright_files)
659
660  if len(file_to_ignore) != 0:
661    fatal("Following files are listed in IGNORE_FILE_NAME but doesn't exists,.\n"
662          + "\n".join(file_to_ignore))
663
664  if len(no_copyright_files) != 0:
665    fatal("Following files are listed in NO_COPYRIGHT_FILES but doesn't exists.\n"
666          + "\n".join(no_copyright_files))
667
668  if format == Format.notice:
669    print_notice(copyrights, False)
670  elif format == Format.notice_with_filename:
671    print_notice(copyrights, True)
672  elif format == Format.html:
673    print_html(copyrights)
674  elif format == Format.json:
675    print_json(copyrights)
676
677def print_html(copyrights):
678  print('<html>')
679  print("""
680  <head>
681    <style>
682      table {
683        font-family: monospace
684      }
685
686      table tr td {
687        padding: 10px 10px 10px 10px
688      }
689    </style>
690  </head>
691  """)
692  print('<body>')
693  print('<table border="1" style="border-collapse:collapse">')
694  for notice in sorted(copyrights.keys()):
695    files = sorted(copyrights[notice])
696
697    print('<tr>')
698    print('<td>')
699    print('<ul>')
700    for file in files:
701      print('<li>%s</li>' % file)
702    print('</ul>')
703    print('</td>')
704
705    print('<td>')
706    print('<p>%s</p>' % notice.replace('\n', '<br>'))
707    print('</td>')
708
709    print('</tr>')
710
711
712  print('</table>')
713  print('</body></html>')
714
715def print_notice(copyrights, print_file):
716  # print the copyright in sorted order for stable output.
717  for notice in sorted(copyrights.keys()):
718    if print_file:
719      files = sorted(copyrights[notice])
720      print("\n".join(files))
721      print()
722    print(notice)
723    print()
724    print("-" * 67)
725    print()
726
727def print_json(copyrights):
728  print(json.dumps(copyrights))
729
730class Format(Enum):
731  notice = 'notice'
732  notice_with_filename = 'notice_with_filename'
733  html = 'html'
734  json = 'json'
735
736  def __str__(self):
737    return self.value
738
739def main():
740  parser = argparse.ArgumentParser(description="Collect notice headers.")
741  parser.add_argument("--format", dest="format", type=Format, choices=list(Format),
742                      default=Format.notice, help="print filename before the license notice")
743  parser.add_argument("--target", dest="target", action='store',
744                      required=True, help="target directory to collect notice headers")
745  res = parser.parse_args()
746  do_check(res.target, res.format)
747
748if __name__ == "__main__":
749  main()
750
751