13f770aabSAndy Fiddaman#!@TOOLS_PYTHON@ -Es
28bcea973SRichard Lowe#
38bcea973SRichard Lowe#  This program is free software; you can redistribute it and/or modify
48bcea973SRichard Lowe#  it under the terms of the GNU General Public License version 2
58bcea973SRichard Lowe#  as published by the Free Software Foundation.
68bcea973SRichard Lowe#
78bcea973SRichard Lowe#  This program is distributed in the hope that it will be useful,
88bcea973SRichard Lowe#  but WITHOUT ANY WARRANTY; without even the implied warranty of
98bcea973SRichard Lowe#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
108bcea973SRichard Lowe#  GNU General Public License for more details.
118bcea973SRichard Lowe#
128bcea973SRichard Lowe#  You should have received a copy of the GNU General Public License
138bcea973SRichard Lowe#  along with this program; if not, write to the Free Software
148bcea973SRichard Lowe#  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
158bcea973SRichard Lowe#
168bcea973SRichard Lowe
178bcea973SRichard Lowe#
188bcea973SRichard Lowe# Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
198bcea973SRichard Lowe# Copyright 2008, 2012 Richard Lowe
20955eb5e1SGarrett D'Amore# Copyright 2019 Garrett D'Amore <garrett@damore.org>
2193d2a904SPaul Dagnelie# Copyright (c) 2015, 2016 by Delphix. All rights reserved.
2228e2b3adSHans Rosenfeld# Copyright 2016 Nexenta Systems, Inc.
23972282a0SJohn Levon# Copyright (c) 2019, Joyent, Inc.
2413904da8SAndy Fiddaman# Copyright 2021 OmniOS Community Edition (OmniOSce) Association.
259af2fe54SBill Sommerfeld# Copyright 2024 Bill Sommerfeld
268bcea973SRichard Lowe#
278bcea973SRichard Lowe
28ca13eaa5SAndy Fiddamanfrom __future__ import print_function
29ca13eaa5SAndy Fiddaman
308bcea973SRichard Loweimport getopt
31ca13eaa5SAndy Fiddamanimport io
328bcea973SRichard Loweimport os
338bcea973SRichard Loweimport re
348bcea973SRichard Loweimport subprocess
358bcea973SRichard Loweimport sys
36ff50e8e5SRichard Loweimport tempfile
37*28e88f55SBill Sommerfeldimport textwrap
388bcea973SRichard Lowe
39ca13eaa5SAndy Fiddamanif sys.version_info[0] < 3:
40ca13eaa5SAndy Fiddaman    from cStringIO import StringIO
41ca13eaa5SAndy Fiddamanelse:
42ca13eaa5SAndy Fiddaman    from io import StringIO
438bcea973SRichard Lowe
448bcea973SRichard Lowe#
458bcea973SRichard Lowe# Adjust the load path based on our location and the version of python into
468bcea973SRichard Lowe# which it is being loaded.  This assumes the normal onbld directory
478bcea973SRichard Lowe# structure, where we are in bin/ and the modules are in
488bcea973SRichard Lowe# lib/python(version)?/onbld/Scm/.  If that changes so too must this.
498bcea973SRichard Lowe#
508bcea973SRichard Lowesys.path.insert(1, os.path.join(os.path.dirname(__file__), "..", "lib",
518bcea973SRichard Lowe                                "python%d.%d" % sys.version_info[:2]))
528bcea973SRichard Lowe
538bcea973SRichard Lowe#
548bcea973SRichard Lowe# Add the relative path to usr/src/tools to the load path, such that when run
558bcea973SRichard Lowe# from the source tree we use the modules also within the source tree.
568bcea973SRichard Lowe#
578bcea973SRichard Lowesys.path.insert(2, os.path.join(os.path.dirname(__file__), ".."))
588bcea973SRichard Lowe
59e5587435SJoshua M. Clulowfrom onbld.Scm import Ignore
604ff15898SGordon Rossfrom onbld.Checks import Comments, Copyright, CStyle, HdrChk, WsCheck
6171af3be3SCody Peter Mellofrom onbld.Checks import JStyle, Keywords, ManLint, Mapfile, SpellCheck
6286d41711SAndy Fiddamanfrom onbld.Checks import ShellLint, PkgFmt
638bcea973SRichard Lowe
648bcea973SRichard Loweclass GitError(Exception):
658bcea973SRichard Lowe    pass
668bcea973SRichard Lowe
678bcea973SRichard Lowedef git(command):
688bcea973SRichard Lowe    """Run a command and return a stream containing its stdout (and write its
698bcea973SRichard Lowe    stderr to its stdout)"""
708bcea973SRichard Lowe
718bcea973SRichard Lowe    if type(command) != list:
728bcea973SRichard Lowe        command = command.split()
738bcea973SRichard Lowe
748bcea973SRichard Lowe    command = ["git"] + command
758bcea973SRichard Lowe
76ff50e8e5SRichard Lowe    try:
77ca13eaa5SAndy Fiddaman        tmpfile = tempfile.TemporaryFile(prefix="git-nits", mode="w+b")
78ca13eaa5SAndy Fiddaman    except EnvironmentError as e:
79ff50e8e5SRichard Lowe        raise GitError("Could not create temporary file: %s\n" % e)
80ff50e8e5SRichard Lowe
81ff50e8e5SRichard Lowe    try:
82ff50e8e5SRichard Lowe        p = subprocess.Popen(command,
83ff50e8e5SRichard Lowe                             stdout=tmpfile,
84380fd671SMatthew Ahrens                             stderr=subprocess.PIPE)
85ca13eaa5SAndy Fiddaman    except OSError as e:
86709afb1dSDillon Amburgey        raise GitError("could not execute %s: %s\n" % (command, e))
878bcea973SRichard Lowe
888bcea973SRichard Lowe    err = p.wait()
898bcea973SRichard Lowe    if err != 0:
90380fd671SMatthew Ahrens        raise GitError(p.stderr.read())
918bcea973SRichard Lowe
92ff50e8e5SRichard Lowe    tmpfile.seek(0)
93ca13eaa5SAndy Fiddaman    lines = []
94ca13eaa5SAndy Fiddaman    for l in tmpfile:
95ca13eaa5SAndy Fiddaman        lines.append(l.decode('utf-8', 'replace'))
96ca13eaa5SAndy Fiddaman    return lines
978bcea973SRichard Lowe
988bcea973SRichard Lowedef git_root():
998bcea973SRichard Lowe    """Return the root of the current git workspace"""
1008bcea973SRichard Lowe
1019af2fe54SBill Sommerfeld    p = git('rev-parse --show-toplevel')
1029af2fe54SBill Sommerfeld    dir = p[0].strip()
1038bcea973SRichard Lowe
1049af2fe54SBill Sommerfeld    return os.path.abspath(dir)
1058bcea973SRichard Lowe
1068bcea973SRichard Lowedef git_branch():
1078bcea973SRichard Lowe    """Return the current git branch"""
1088bcea973SRichard Lowe
1098bcea973SRichard Lowe    p = git('branch')
1108bcea973SRichard Lowe
1118bcea973SRichard Lowe    for elt in p:
1128bcea973SRichard Lowe        if elt[0] == '*':
1138bcea973SRichard Lowe            if elt.endswith('(no branch)'):
1148bcea973SRichard Lowe                return None
1158bcea973SRichard Lowe            return elt.split()[1]
1168bcea973SRichard Lowe
1178bcea973SRichard Lowedef git_parent_branch(branch):
1188bcea973SRichard Lowe    """Return the parent of the current git branch.
1198bcea973SRichard Lowe
1208bcea973SRichard Lowe    If this branch tracks a remote branch, return the remote branch which is
1218bcea973SRichard Lowe    tracked.  If not, default to origin/master."""
1228bcea973SRichard Lowe
1238bcea973SRichard Lowe    if not branch:
1248bcea973SRichard Lowe        return None
1258bcea973SRichard Lowe
1264ff15898SGordon Ross    p = git(["for-each-ref", "--format=%(refname:short) %(upstream:short)",
12728e2b3adSHans Rosenfeld            "refs/heads/"])
1288bcea973SRichard Lowe
1298bcea973SRichard Lowe    if not p:
1308bcea973SRichard Lowe        sys.stderr.write("Failed finding git parent branch\n")
131972282a0SJohn Levon        sys.exit(1)
1328bcea973SRichard Lowe
1338bcea973SRichard Lowe    for line in p:
1348bcea973SRichard Lowe        # Git 1.7 will leave a ' ' trailing any non-tracking branch
1358bcea973SRichard Lowe        if ' ' in line and not line.endswith(' \n'):
1368bcea973SRichard Lowe            local, remote = line.split()
1378bcea973SRichard Lowe            if local == branch:
1388bcea973SRichard Lowe                return remote
1398bcea973SRichard Lowe    return 'origin/master'
1408bcea973SRichard Lowe
1413f8945a7SBill Sommerfelddef slices(strlist, sep):
1423f8945a7SBill Sommerfeld    """Yield start & end of each commit within the list of comments"""
1433f8945a7SBill Sommerfeld    low = 0
1443f8945a7SBill Sommerfeld    for i, v in enumerate(strlist):
1453f8945a7SBill Sommerfeld        if v == sep:
1463f8945a7SBill Sommerfeld            yield(low, i)
1473f8945a7SBill Sommerfeld            low = i+1
1483f8945a7SBill Sommerfeld
1493f8945a7SBill Sommerfeld    if low != len(strlist):
1503f8945a7SBill Sommerfeld        yield(low, len(strlist))
1513f8945a7SBill Sommerfeld
1528bcea973SRichard Lowedef git_comments(parent):
1533f8945a7SBill Sommerfeld    """Return the checkin comments for each commit on this git branch,
1543f8945a7SBill Sommerfeld    structured as a list of lists of lines."""
1558bcea973SRichard Lowe
15627495383SRichard Lowe    p = git('log --pretty=tformat:%%B:SEP: %s..' % parent)
1578bcea973SRichard Lowe
1588bcea973SRichard Lowe    if not p:
159972282a0SJohn Levon        sys.stderr.write("No outgoing changesets found - missing -p option?\n");
160972282a0SJohn Levon        sys.exit(1)
1618bcea973SRichard Lowe
1623f8945a7SBill Sommerfeld    return [ [line.strip() for line in p[a:b]]
1633f8945a7SBill Sommerfeld             for (a, b) in slices(p, ':SEP:\n')]
1648bcea973SRichard Lowe
1658bcea973SRichard Lowedef git_file_list(parent, paths=None):
1668bcea973SRichard Lowe    """Return the set of files which have ever changed on this branch.
1678bcea973SRichard Lowe
1688bcea973SRichard Lowe    NB: This includes files which no longer exist, or no longer actually
1698bcea973SRichard Lowe    differ."""
1708bcea973SRichard Lowe
1718bcea973SRichard Lowe    p = git("log --name-only --pretty=format: %s.. %s" %
1728bcea973SRichard Lowe             (parent, ' '.join(paths)))
1738bcea973SRichard Lowe
1748bcea973SRichard Lowe    if not p:
1758bcea973SRichard Lowe        sys.stderr.write("Failed building file-list from git\n")
176972282a0SJohn Levon        sys.exit(1)
1778bcea973SRichard Lowe
1788bcea973SRichard Lowe    ret = set()
1798bcea973SRichard Lowe    for fname in p:
180da88d39fSBill Sommerfeld        fname = fname.strip()
181da88d39fSBill Sommerfeld        if fname and not fname.isspace():
182da88d39fSBill Sommerfeld            ret.add(fname)
1838bcea973SRichard Lowe
184da88d39fSBill Sommerfeld    return sorted(ret)
1858bcea973SRichard Lowe
1868bcea973SRichard Lowedef not_check(root, cmd):
1878bcea973SRichard Lowe    """Return a function which returns True if a file given as an argument
1888bcea973SRichard Lowe    should be excluded from the check named by 'cmd'"""
1898bcea973SRichard Lowe
190ca13eaa5SAndy Fiddaman    ignorefiles = list(filter(os.path.exists,
191c82c4676SGordon Ross                         [os.path.join(root, ".git/info", "%s.NOT" % cmd),
192ca13eaa5SAndy Fiddaman                          os.path.join(root, "exception_lists", cmd)]))
193e5587435SJoshua M. Clulow    return Ignore.ignore(root, ignorefiles)
1948bcea973SRichard Lowe
195955eb5e1SGarrett D'Amoredef gen_files(root, parent, paths, exclude, filter=None):
1968bcea973SRichard Lowe    """Return a function producing file names, relative to the current
1978bcea973SRichard Lowe    directory, of any file changed on this branch (limited to 'paths' if
1988bcea973SRichard Lowe    requested), and excluding files for which exclude returns a true value """
1998bcea973SRichard Lowe
200955eb5e1SGarrett D'Amore    if filter is None:
201955eb5e1SGarrett D'Amore        filter = lambda x: os.path.isfile(x)
202955eb5e1SGarrett D'Amore
2038bcea973SRichard Lowe    def ret(select=None):
2048bcea973SRichard Lowe        if not select:
2058bcea973SRichard Lowe            select = lambda x: True
2068bcea973SRichard Lowe
20738e36c53SJohn Levon        for abspath in git_file_list(parent, paths):
2089af2fe54SBill Sommerfeld            path = os.path.relpath(os.path.join(root, abspath), '.')
20993d2a904SPaul Dagnelie            try:
21038e36c53SJohn Levon                res = git("diff %s HEAD %s" % (parent, path))
211ca13eaa5SAndy Fiddaman            except GitError as e:
21238e36c53SJohn Levon                # This ignores all the errors that can be thrown. Usually, this
21338e36c53SJohn Levon                # means that git returned non-zero because the file doesn't
21438e36c53SJohn Levon                # exist, but it could also fail if git can't create a new file
21538e36c53SJohn Levon                # or it can't be executed.  Such errors are 1) unlikely, and 2)
21638e36c53SJohn Levon                # will be caught by other invocations of git().
21793d2a904SPaul Dagnelie                continue
218ca13eaa5SAndy Fiddaman            empty = not res
219955eb5e1SGarrett D'Amore            if (filter(path) and not empty and
22038e36c53SJohn Levon                select(path) and not exclude(abspath)):
22138e36c53SJohn Levon                yield path
2228bcea973SRichard Lowe    return ret
2238bcea973SRichard Lowe
224955eb5e1SGarrett D'Amoredef gen_links(root, parent, paths, exclude):
225955eb5e1SGarrett D'Amore    """Return a function producing symbolic link names, relative to the current
226955eb5e1SGarrett D'Amore    directory, of any file changed on this branch (limited to 'paths' if
227955eb5e1SGarrett D'Amore    requested), and excluding files for which exclude returns a true value """
228955eb5e1SGarrett D'Amore
229955eb5e1SGarrett D'Amore    return gen_files(root, parent, paths, exclude, lambda x: os.path.islink(x))
230955eb5e1SGarrett D'Amore
231*28e88f55SBill Sommerfelddef gen_none(root, parent, paths, exclude):
232*28e88f55SBill Sommerfeld    """ Return a function returning the empty list """
233*28e88f55SBill Sommerfeld    return lambda x: []
234*28e88f55SBill Sommerfeld
235*28e88f55SBill Sommerfeld# The list of possible checks.   Each is recorded as two-function pair; the
236*28e88f55SBill Sommerfeld# first is the actual checker, and the second is the generator which creates
237*28e88f55SBill Sommerfeld# the list of things that the checker works on.
238*28e88f55SBill Sommerfeld
239*28e88f55SBill Sommerfeldchecks = {}
240*28e88f55SBill Sommerfeldnits_checks = []
241*28e88f55SBill Sommerfeldall_checks = []
242*28e88f55SBill Sommerfeld
243*28e88f55SBill Sommerfelddef add_check(fn, gen):
244*28e88f55SBill Sommerfeld    """ Define a checker and add it to the appropriate lists """
245*28e88f55SBill Sommerfeld    name = fn.__name__
246*28e88f55SBill Sommerfeld    if fn.__doc__ is None:
247*28e88f55SBill Sommerfeld        raise ValueError('Check function lacks a documentation string',
248*28e88f55SBill Sommerfeld                         name)
249*28e88f55SBill Sommerfeld    checks[name] = (fn, gen)
250*28e88f55SBill Sommerfeld    all_checks.append(name)
251*28e88f55SBill Sommerfeld    if gen != gen_none:
252*28e88f55SBill Sommerfeld        nits_checks.append(name)
253*28e88f55SBill Sommerfeld    return fn
254*28e88f55SBill Sommerfeld
255*28e88f55SBill Sommerfelddef filechecker(fn):
256*28e88f55SBill Sommerfeld    """ Decorator which identifies a function as being a file-checker """
257*28e88f55SBill Sommerfeld    return add_check(fn, gen_files)
258*28e88f55SBill Sommerfeld
259*28e88f55SBill Sommerfelddef linkchecker(fn):
260*28e88f55SBill Sommerfeld    """ Decorator which identifies a function as being a symlink-checker """
261*28e88f55SBill Sommerfeld    return add_check(fn, gen_links)
262*28e88f55SBill Sommerfeld
263*28e88f55SBill Sommerfelddef wschecker(fn):
264*28e88f55SBill Sommerfeld    """ Decorator which identifies a function as being a workspace checker """
265*28e88f55SBill Sommerfeld    return add_check(fn, gen_none)
266*28e88f55SBill Sommerfeld
267*28e88f55SBill Sommerfeld@wschecker
2688bcea973SRichard Lowedef comchk(root, parent, flist, output):
269*28e88f55SBill Sommerfeld    "Check that putback comments follow the prescribed format"
2708bcea973SRichard Lowe    output.write("Comments:\n")
2718bcea973SRichard Lowe
2728d226a82SBill Sommerfeld    comments = git_comments(parent)
2733f8945a7SBill Sommerfeld    multi = len(comments) > 1
2743f8945a7SBill Sommerfeld    state = {}
2753f8945a7SBill Sommerfeld
2763f8945a7SBill Sommerfeld    ret = 0
2773f8945a7SBill Sommerfeld    for commit in comments:
2788d226a82SBill Sommerfeld
2793f8945a7SBill Sommerfeld        s = StringIO()
2808bcea973SRichard Lowe
2813f8945a7SBill Sommerfeld        result = Comments.comchk(commit, check_db=True,
2823f8945a7SBill Sommerfeld                                 output=s, bugs=state)
2833f8945a7SBill Sommerfeld        ret |= result
2843f8945a7SBill Sommerfeld
2853f8945a7SBill Sommerfeld        if result != 0:
2863f8945a7SBill Sommerfeld            if multi:
2873f8945a7SBill Sommerfeld                output.write('\n%s\n' % commit[0])
2883f8945a7SBill Sommerfeld            output.write(s.getvalue())
2893f8945a7SBill Sommerfeld
2903f8945a7SBill Sommerfeld    return ret
2918bcea973SRichard Lowe
292*28e88f55SBill Sommerfeld@filechecker
2938bcea973SRichard Lowedef copyright(root, parent, flist, output):
294*28e88f55SBill Sommerfeld    """Check that each source file contains a copyright notice for the current
295*28e88f55SBill Sommerfeldyear. You don't need to fix this if you, the potential new copyright holder,
296*28e88f55SBill Sommerfeldchooses not to."""
2978bcea973SRichard Lowe    ret = 0
2988bcea973SRichard Lowe    output.write("Copyrights:\n")
2998bcea973SRichard Lowe    for f in flist():
300ca13eaa5SAndy Fiddaman        with io.open(f, encoding='utf-8', errors='replace') as fh:
301ca13eaa5SAndy Fiddaman            ret |= Copyright.copyright(fh, output=output)
3028bcea973SRichard Lowe    return ret
3038bcea973SRichard Lowe
304*28e88f55SBill Sommerfeld@filechecker
3058bcea973SRichard Lowedef cstyle(root, parent, flist, output):
306*28e88f55SBill Sommerfeld    "Check that C source files conform to the illumos C style rules"
3078bcea973SRichard Lowe    ret = 0
3088bcea973SRichard Lowe    output.write("C style:\n")
3098bcea973SRichard Lowe    for f in flist(lambda x: x.endswith('.c') or x.endswith('.h')):
310a90997d2SAndy Fiddaman        with io.open(f, mode='rb') as fh:
311ca13eaa5SAndy Fiddaman            ret |= CStyle.cstyle(fh, output=output, picky=True,
3128bcea973SRichard Lowe                             check_posix_types=True,
3138bcea973SRichard Lowe                             check_continuation=True)
3148bcea973SRichard Lowe    return ret
3158bcea973SRichard Lowe
316*28e88f55SBill Sommerfeld@filechecker
317*28e88f55SBill Sommerfelddef hdrchk(root, parent, flist, output):
318*28e88f55SBill Sommerfeld    "Check that C header files conform to the illumos header style rules"
319*28e88f55SBill Sommerfeld    ret = 0
320*28e88f55SBill Sommerfeld    output.write("Header format:\n")
321*28e88f55SBill Sommerfeld    for f in flist(lambda x: x.endswith('.h')):
322*28e88f55SBill Sommerfeld        with io.open(f, encoding='utf-8', errors='replace') as fh:
323*28e88f55SBill Sommerfeld            ret |= HdrChk.hdrchk(fh, lenient=True, output=output)
324*28e88f55SBill Sommerfeld    return ret
325*28e88f55SBill Sommerfeld
326*28e88f55SBill Sommerfeld@filechecker
3278bcea973SRichard Lowedef jstyle(root, parent, flist, output):
328*28e88f55SBill Sommerfeld    """Check that Java source files conform to the illumos Java style rules
329*28e88f55SBill Sommerfeld(which differ from the traditionally recommended Java style)"""
330*28e88f55SBill Sommerfeld
3318bcea973SRichard Lowe    ret = 0
3328bcea973SRichard Lowe    output.write("Java style:\n")
3338bcea973SRichard Lowe    for f in flist(lambda x: x.endswith('.java')):
33410811751SAndy Fiddaman        with io.open(f, mode='rb') as fh:
335ca13eaa5SAndy Fiddaman            ret |= JStyle.jstyle(fh, output=output, picky=True)
3368bcea973SRichard Lowe    return ret
3378bcea973SRichard Lowe
338*28e88f55SBill Sommerfeld@filechecker
339*28e88f55SBill Sommerfelddef keywords(root, parent, flist, output):
340*28e88f55SBill Sommerfeld    """Check that no source files contain unexpanded SCCS keywords.
341*28e88f55SBill SommerfeldIt is possible that this check may false positive on certain inputs.
342*28e88f55SBill SommerfeldIt is generally obvious when this is the case.
343*28e88f55SBill Sommerfeld
344*28e88f55SBill SommerfeldThis check does not check for expanded SCCS keywords, though the common
345*28e88f55SBill Sommerfeld'ident'-style lines should be removed regardless of whether they are
346*28e88f55SBill Sommerfeldexpanded."""
347*28e88f55SBill Sommerfeld
348*28e88f55SBill Sommerfeld    ret = 0
349*28e88f55SBill Sommerfeld    output.write("SCCS Keywords:\n")
350*28e88f55SBill Sommerfeld    for f in flist():
351*28e88f55SBill Sommerfeld        with io.open(f, encoding='utf-8', errors='replace') as fh:
352*28e88f55SBill Sommerfeld            ret |= Keywords.keywords(fh, output=output)
353*28e88f55SBill Sommerfeld    return ret
354*28e88f55SBill Sommerfeld
355*28e88f55SBill Sommerfeld@filechecker
35695c635efSGarrett D'Amoredef manlint(root, parent, flist, output):
357*28e88f55SBill Sommerfeld    "Check for problems with man pages."
358*28e88f55SBill Sommerfeld
35995c635efSGarrett D'Amore    ret = 0
36071af3be3SCody Peter Mello    output.write("Man page format/spelling:\n")
36195c635efSGarrett D'Amore    ManfileRE = re.compile(r'.*\.[0-9][a-z]*$', re.IGNORECASE)
36295c635efSGarrett D'Amore    for f in flist(lambda x: ManfileRE.match(x)):
363a90997d2SAndy Fiddaman        with io.open(f, mode='rb') as fh:
364ca13eaa5SAndy Fiddaman            ret |= ManLint.manlint(fh, output=output, picky=True)
365ca13eaa5SAndy Fiddaman            ret |= SpellCheck.spellcheck(fh, output=output)
36695c635efSGarrett D'Amore    return ret
36795c635efSGarrett D'Amore
368*28e88f55SBill Sommerfeld@filechecker
369*28e88f55SBill Sommerfelddef mapfilechk(root, parent, flist, output):
370*28e88f55SBill Sommerfeld    """Check that linker mapfiles contain a comment directing anyone
371*28e88f55SBill Sommerfeldediting to read the directions in usr/lib/README.mapfiles."""
372*28e88f55SBill Sommerfeld
373*28e88f55SBill Sommerfeld    ret = 0
374*28e88f55SBill Sommerfeld    # We are interested in examining any file that has the following
375*28e88f55SBill Sommerfeld    # in its final path segment:
376*28e88f55SBill Sommerfeld    #    - Contains the word 'mapfile'
377*28e88f55SBill Sommerfeld    #    - Begins with 'map.'
378*28e88f55SBill Sommerfeld    #    - Ends with '.map'
379*28e88f55SBill Sommerfeld    # We don't want to match unless these things occur in final path segment
380*28e88f55SBill Sommerfeld    # because directory names with these strings don't indicate a mapfile.
381*28e88f55SBill Sommerfeld    # We also ignore files with suffixes that tell us that the files
382*28e88f55SBill Sommerfeld    # are not mapfiles.
383*28e88f55SBill Sommerfeld    MapfileRE = re.compile(r'.*((mapfile[^/]*)|(/map\.+[^/]*)|(\.map))$',
384*28e88f55SBill Sommerfeld        re.IGNORECASE)
385*28e88f55SBill Sommerfeld    NotMapSuffixRE = re.compile(r'.*\.[ch]$', re.IGNORECASE)
386*28e88f55SBill Sommerfeld
387*28e88f55SBill Sommerfeld    output.write("Mapfile comments:\n")
388*28e88f55SBill Sommerfeld
389*28e88f55SBill Sommerfeld    for f in flist(lambda x: MapfileRE.match(x) and not
390*28e88f55SBill Sommerfeld                   NotMapSuffixRE.match(x)):
391*28e88f55SBill Sommerfeld        with io.open(f, encoding='utf-8', errors='replace') as fh:
392*28e88f55SBill Sommerfeld            ret |= Mapfile.mapfilechk(fh, output=output)
393*28e88f55SBill Sommerfeld    return ret
394*28e88f55SBill Sommerfeld
395*28e88f55SBill Sommerfeld@filechecker
39613904da8SAndy Fiddamandef shelllint(root, parent, flist, output):
397*28e88f55SBill Sommerfeld    """Check shell scripts for common errors."""
39813904da8SAndy Fiddaman    ret = 0
39913904da8SAndy Fiddaman    output.write("Shell lint:\n")
40013904da8SAndy Fiddaman
40113904da8SAndy Fiddaman    def isshell(x):
40213904da8SAndy Fiddaman        (_, ext) = os.path.splitext(x)
40313904da8SAndy Fiddaman        if ext in ['.sh', '.ksh']:
40413904da8SAndy Fiddaman            return True
40513904da8SAndy Fiddaman        if ext == '':
40613904da8SAndy Fiddaman            with io.open(x, mode='r', errors='ignore') as fh:
40713904da8SAndy Fiddaman                if re.match(r'^#.*\bk?sh\b', fh.readline()):
40813904da8SAndy Fiddaman                    return True
40913904da8SAndy Fiddaman        return False
41013904da8SAndy Fiddaman
41113904da8SAndy Fiddaman    for f in flist(isshell):
41213904da8SAndy Fiddaman        with io.open(f, mode='rb') as fh:
41313904da8SAndy Fiddaman            ret |= ShellLint.lint(fh, output=output)
41413904da8SAndy Fiddaman
41513904da8SAndy Fiddaman    return ret
41613904da8SAndy Fiddaman
417*28e88f55SBill Sommerfeld@filechecker
41886d41711SAndy Fiddamandef pkgfmt(root, parent, flist, output):
419*28e88f55SBill Sommerfeld    """Check package manifests for common errors."""
42086d41711SAndy Fiddaman    ret = 0
42186d41711SAndy Fiddaman    output.write("Package manifests:\n")
42286d41711SAndy Fiddaman
42325b05a3eSAndy Fiddaman    for f in flist(lambda x: x.endswith('.p5m')):
42486d41711SAndy Fiddaman        with io.open(f, mode='rb') as fh:
42586d41711SAndy Fiddaman            ret |= PkgFmt.check(fh, output=output)
42686d41711SAndy Fiddaman
42786d41711SAndy Fiddaman    return ret
42886d41711SAndy Fiddaman
429955eb5e1SGarrett D'Amoredef iswinreserved(name):
430955eb5e1SGarrett D'Amore    reserved = [
431955eb5e1SGarrett D'Amore        'con', 'prn', 'aux', 'nul',
432955eb5e1SGarrett D'Amore        'com1', 'com2', 'com3', 'com4', 'com5',
433955eb5e1SGarrett D'Amore        'com6', 'com7', 'com8', 'com9', 'com0',
434955eb5e1SGarrett D'Amore        'lpt1', 'lpt2', 'lpt3', 'lpt4', 'lpt5',
435955eb5e1SGarrett D'Amore        'lpt6', 'lpt7', 'lpt8', 'lpt9', 'lpt0' ]
436955eb5e1SGarrett D'Amore    l = name.lower()
437955eb5e1SGarrett D'Amore    for r in reserved:
438955eb5e1SGarrett D'Amore        if l == r or l.startswith(r+"."):
439955eb5e1SGarrett D'Amore            return True
440955eb5e1SGarrett D'Amore    return False
441955eb5e1SGarrett D'Amore
442955eb5e1SGarrett D'Amoredef haswinspecial(name):
443955eb5e1SGarrett D'Amore    specials = '<>:"\\|?*'
444955eb5e1SGarrett D'Amore    for c in name:
445955eb5e1SGarrett D'Amore        if c in specials:
446955eb5e1SGarrett D'Amore            return True
447955eb5e1SGarrett D'Amore    return False
448955eb5e1SGarrett D'Amore
449*28e88f55SBill Sommerfeld@filechecker
450955eb5e1SGarrett D'Amoredef winnames(root, parent, flist, output):
451*28e88f55SBill Sommerfeld    "Check for filenames which can't be used in a Windows filesystem."
452955eb5e1SGarrett D'Amore    ret = 0
453955eb5e1SGarrett D'Amore    output.write("Illegal filenames (Windows):\n")
454955eb5e1SGarrett D'Amore    for f in flist():
455955eb5e1SGarrett D'Amore        if haswinspecial(f):
456955eb5e1SGarrett D'Amore            output.write("  "+f+": invalid character in name\n")
457955eb5e1SGarrett D'Amore            ret |= 1
458955eb5e1SGarrett D'Amore            continue
459955eb5e1SGarrett D'Amore
460955eb5e1SGarrett D'Amore        parts = f.split('/')
461955eb5e1SGarrett D'Amore        for p in parts:
462955eb5e1SGarrett D'Amore            if iswinreserved(p):
463955eb5e1SGarrett D'Amore                output.write("  "+f+": reserved file name\n")
464955eb5e1SGarrett D'Amore                ret |= 1
465955eb5e1SGarrett D'Amore                break
466955eb5e1SGarrett D'Amore
467955eb5e1SGarrett D'Amore    return ret
468955eb5e1SGarrett D'Amore
469*28e88f55SBill Sommerfeld@filechecker
470*28e88f55SBill Sommerfelddef wscheck(root, parent, flist, output):
471*28e88f55SBill Sommerfeld    "Check for whitespace issues such as mixed tabs/spaces in source files."
472*28e88f55SBill Sommerfeld    ret = 0
473*28e88f55SBill Sommerfeld    output.write("white space nits:\n")
474*28e88f55SBill Sommerfeld    for f in flist():
475*28e88f55SBill Sommerfeld        with io.open(f, encoding='utf-8', errors='replace') as fh:
476*28e88f55SBill Sommerfeld            ret |= WsCheck.wscheck(fh, output=output)
477*28e88f55SBill Sommerfeld    return ret
478*28e88f55SBill Sommerfeld
479*28e88f55SBill Sommerfeld@linkchecker
480*28e88f55SBill Sommerfelddef symlinks(root, parent, flist, output):
481*28e88f55SBill Sommerfeld    "Check for committed symlinks (there shouldn't be any)."
482*28e88f55SBill Sommerfeld    ret = 0
483*28e88f55SBill Sommerfeld    output.write("Symbolic links:\n")
484*28e88f55SBill Sommerfeld    for f in flist():
485*28e88f55SBill Sommerfeld        output.write("  "+f+"\n")
486*28e88f55SBill Sommerfeld        ret |= 1
487*28e88f55SBill Sommerfeld    return ret
488*28e88f55SBill Sommerfeld
489*28e88f55SBill Sommerfelddef run_checks(root, parent, checklist, paths=''):
490*28e88f55SBill Sommerfeld    """Run the checks named in 'checklist',
4918bcea973SRichard Lowe    and report results for any which fail.
4928bcea973SRichard Lowe
4938bcea973SRichard Lowe    Return failure if any of them did.
4948bcea973SRichard Lowe
495*28e88f55SBill Sommerfeld    NB: the check names also name the NOT
4968bcea973SRichard Lowe    file which excepts files from them."""
4978bcea973SRichard Lowe
4988bcea973SRichard Lowe    ret = 0
4998bcea973SRichard Lowe
500*28e88f55SBill Sommerfeld    for check in checklist:
501*28e88f55SBill Sommerfeld        (cmd, gen) = checks[check]
5028bcea973SRichard Lowe
503955eb5e1SGarrett D'Amore        s = StringIO()
504955eb5e1SGarrett D'Amore
505*28e88f55SBill Sommerfeld        exclude = not_check(root, check)
506*28e88f55SBill Sommerfeld        result = cmd(root, parent, gen(root, parent, paths, exclude),
507955eb5e1SGarrett D'Amore                     output=s)
508955eb5e1SGarrett D'Amore        ret |= result
509955eb5e1SGarrett D'Amore
510955eb5e1SGarrett D'Amore        if result != 0:
511955eb5e1SGarrett D'Amore            print(s.getvalue())
512955eb5e1SGarrett D'Amore
5138bcea973SRichard Lowe    return ret
5148bcea973SRichard Lowe
515*28e88f55SBill Sommerfelddef print_checks():
516*28e88f55SBill Sommerfeld
517*28e88f55SBill Sommerfeld    for c in all_checks:
518*28e88f55SBill Sommerfeld        print(textwrap.fill(
519*28e88f55SBill Sommerfeld            "%-11s %s" % (c, checks[c][0].__doc__),
520*28e88f55SBill Sommerfeld            width=78,
521*28e88f55SBill Sommerfeld            subsequent_indent=' '*12), '\n')
5228bcea973SRichard Lowe
5238bcea973SRichard Lowedef main(cmd, args):
5248bcea973SRichard Lowe    parent_branch = None
525*28e88f55SBill Sommerfeld
526*28e88f55SBill Sommerfeld    checklist = []
5278bcea973SRichard Lowe
5288bcea973SRichard Lowe    try:
529*28e88f55SBill Sommerfeld        opts, args = getopt.getopt(args, 'lb:c:p:')
530ca13eaa5SAndy Fiddaman    except getopt.GetoptError as e:
5318bcea973SRichard Lowe        sys.stderr.write(str(e) + '\n')
532*28e88f55SBill Sommerfeld        sys.stderr.write("Usage: %s [-l] [-c check] [-p branch] [path...]\n"
533*28e88f55SBill Sommerfeld                         % cmd)
5348bcea973SRichard Lowe        sys.exit(1)
5358bcea973SRichard Lowe
5368bcea973SRichard Lowe    for opt, arg in opts:
537*28e88f55SBill Sommerfeld        if opt == '-l':
538*28e88f55SBill Sommerfeld            print_checks()
539*28e88f55SBill Sommerfeld            sys.exit(0)
54042a3762dSJoshua M. Clulow        # We accept "-b" as an alias of "-p" for backwards compatibility.
541*28e88f55SBill Sommerfeld        elif opt == '-p' or opt == '-b':
5428bcea973SRichard Lowe            parent_branch = arg
543eabe844aSJohn Levon        elif opt == '-c':
544*28e88f55SBill Sommerfeld            if arg not in checks:
545*28e88f55SBill Sommerfeld                sys.stderr.write("Unknown check '%s'\n" % arg)
546*28e88f55SBill Sommerfeld                sys.exit(1)
547*28e88f55SBill Sommerfeld            checklist.append(arg)
5488bcea973SRichard Lowe
5498bcea973SRichard Lowe    if not parent_branch:
5508bcea973SRichard Lowe        parent_branch = git_parent_branch(git_branch())
5518bcea973SRichard Lowe
552*28e88f55SBill Sommerfeld    if len(checklist) == 0:
553eabe844aSJohn Levon        if cmd == 'git-pbchk':
554*28e88f55SBill Sommerfeld            if args:
555*28e88f55SBill Sommerfeld                sys.stderr.write("only complete workspaces may be pbchk'd\n");
556*28e88f55SBill Sommerfeld                sys.exit(1)
557*28e88f55SBill Sommerfeld            checklist = all_checks
558eabe844aSJohn Levon        else:
559*28e88f55SBill Sommerfeld            checklist = nits_checks
560*28e88f55SBill Sommerfeld
561*28e88f55SBill Sommerfeld    run_checks(git_root(), parent_branch, checklist, args)
5628bcea973SRichard Lowe
5638bcea973SRichard Loweif __name__ == '__main__':
5648bcea973SRichard Lowe    try:
5658bcea973SRichard Lowe        main(os.path.basename(sys.argv[0]), sys.argv[1:])
566ca13eaa5SAndy Fiddaman    except GitError as e:
5678bcea973SRichard Lowe        sys.stderr.write("failed to run git:\n %s\n" % str(e))
5688bcea973SRichard Lowe        sys.exit(1)
569