| #!/usr/bin/env python3 |
| # -*- coding: utf-8 -*- |
| # Copyright 2017 The Chromium OS Authors. All rights reserved. |
| # 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.""" |
| |
| from __future__ import print_function |
| |
| import difflib |
| import os |
| import re |
| import sys |
| |
| TOP_DIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) |
| |
| # Find chromite! |
| sys.path.insert(0, os.path.join(TOP_DIR, '..', '..')) |
| |
| from chromite.lib import commandline |
| from chromite.lib import git |
| from chromite.lib import cros_logging as logging |
| from chromite.lib import osutils |
| |
| |
| 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 seperation, |
| # 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', |
| 'avtest_label_detect', |
| 'buffet', |
| 'cros-disks', |
| 'fitpicker', |
| 'image-burner', |
| 'init', |
| 'libchromeos-ui', |
| 'libcontainer', |
| 'lorgnette', |
| 'modem-utilities', |
| 'mtpd', |
| 'salsa', |
| 'thd', |
| 'timberslide', |
| 'tpm_manager', |
| 'trim', |
| 'userfeedback', |
| 'userspace_touchpad', |
| 'vpn-manager', |
| ) |
| |
| 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 |
| 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) |