| #!/usr/bin/env python3 |
| # Copyright 2017 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Linter for various README.md files.""" |
| |
| import difflib |
| import logging |
| import os |
| from pathlib import Path |
| import re |
| |
| |
| _HACK_VAR_TO_DISABLE_ISORT = "hack" |
| # pylint: disable=wrong-import-position |
| import chromite_init # pylint: disable=unused-import |
| |
| from chromite.lib import commandline |
| from chromite.lib import git |
| from chromite.lib import osutils |
| |
| |
| TOP_DIR = Path(__file__).resolve().parent.parent |
| |
| |
| def GetActiveProjects(): |
| """Return the list of active projects.""" |
| # Look at all the paths (files & dirs) in the top of the git repo. This way |
| # we ignore local directories devs created that aren't actually committed. |
| cmd = ["ls-tree", "--name-only", "-z", "HEAD"] |
| result = git.RunGit(TOP_DIR, cmd) |
| |
| # Split the output on NULs to avoid whitespace/etc... issues. |
| paths = result.stdout.split("\0") |
| |
| # ls-tree -z will include a trailing NUL on all entries, not just |
| # separation, so filter it out if found (in case ls-tree behavior changes |
| # on us). |
| for path in [x for x in paths if x]: |
| if os.path.isdir(os.path.join(TOP_DIR, path)): |
| yield path |
| |
| |
| def CheckTopLevel(): |
| """Check the top level README.md list.""" |
| ret = 0 |
| path = os.path.join(TOP_DIR, "README.md") |
| data = osutils.ReadFile(path).splitlines() |
| |
| # Look for the directory header. |
| try: |
| i = data.index("# Local Project Directory") |
| except ValueError: |
| logging.error("README.md index out of sync") |
| return 1 |
| data = data[i + 1 :] |
| |
| # Pull out all the linked projects. |
| listed_projs = [] |
| listed_dirs = [] |
| for line in data: |
| # Break once we hit the end of the table. |
| if line.startswith("#"): |
| break |
| |
| m = re.match(r"^[|] \[([^]]+)*\]\(\./([^)]*)/\)", line) |
| if m: |
| listed_projs.append(m.group(1)) |
| listed_dirs.append(m.group(2)) |
| logging.debug("README.md projects: %s", listed_projs) |
| |
| sorted_projs = sorted(listed_projs) |
| if listed_projs != sorted_projs: |
| ret = 1 |
| lines = list( |
| difflib.unified_diff(listed_projs, sorted_projs, lineterm="") |
| ) |
| logging.error( |
| "README.md project listing should be kept sorted:\n%s", |
| "\n".join(lines[2:]), |
| ) |
| |
| # Check the list in README.md for outdated entries. |
| old_dirs = [] |
| for project_dir in listed_dirs: |
| path = os.path.join(TOP_DIR, project_dir) |
| if not os.path.isdir(path): |
| old_dirs.append(path) |
| if old_dirs: |
| ret = 1 |
| logging.error("README.md: found stale project entries: %s", old_dirs) |
| |
| # Check the local source repo for missing entries. |
| existing_projs = sorted(GetActiveProjects()) |
| logging.debug("Found active projects: %s", existing_projs) |
| new_projs = [] |
| for proj in existing_projs: |
| path = os.path.join(TOP_DIR, proj) |
| if os.path.isdir(path) and proj not in listed_projs: |
| new_projs.append(proj) |
| if new_projs: |
| ret = 1 |
| logging.error("README.md: document new projects: %s", new_projs) |
| |
| return ret |
| |
| |
| def CheckSubdirs(): |
| """Check the subdir README.md files exist.""" |
| # Legacy projects that don't have a README.md file. |
| # Someone should write some docs :D. |
| LEGACYLIST = ( |
| "attestation", # TODO(b/262375413) |
| "avtest_label_detect", |
| "cros-disks", # TODO(b/262376624) |
| "image-burner", |
| "init", |
| "libchromeos-ui", |
| "libcontainer", |
| "modem-utilities", |
| "mtpd", # TODO(b/262376388) |
| "timberslide", |
| "tpm_manager", # TODO(b/262375897) |
| "trim", |
| ) |
| |
| ret = 0 |
| for proj in GetActiveProjects(): |
| readme = os.path.join(TOP_DIR, proj, "README.md") |
| if os.path.exists(readme): |
| if proj in LEGACYLIST: |
| logging.error( |
| '*** Project "%s" is in no-README LEGACYLIST, but ' |
| "actually has one. Please remove it from " |
| "LEGACYLIST!", |
| proj, |
| ) |
| else: |
| if not proj in LEGACYLIST: |
| logging.error('*** Project "%s" needs a README.md file', proj) |
| ret = 1 |
| |
| # Make sure the list doesn't get stale itself. |
| old_projects = set(LEGACYLIST) - set(GetActiveProjects()) |
| if old_projects: |
| logging.error( |
| "*** %s:LEGACYLIST contains old entries %s. Please remove them.", |
| __file__, |
| old_projects, |
| ) |
| ret = 1 |
| |
| return ret |
| |
| |
| def GetParser(): |
| """Return an argument parser.""" |
| parser = commandline.ArgumentParser(description=__doc__) |
| parser.add_argument( |
| "--extensions", |
| default="gyp,gypi", |
| help="Comma delimited file extensions to check. " |
| "(default: %(default)s)", |
| ) |
| parser.add_argument("files", nargs="*", help="Files to run lint.") |
| return parser |
| |
| |
| def main(argv): |
| parser = GetParser() |
| opts = parser.parse_args(argv) |
| opts.Freeze() |
| |
| return CheckTopLevel() | CheckSubdirs() |
| |
| |
| if __name__ == "__main__": |
| commandline.ScriptWrapperMain(lambda _: main) |