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