[lint] add cmakelint to lintrunner (#68191)

Summary:
Pull Request resolved: https://github.com/pytorch/pytorch/pull/68191

+ fix filename of exec_linter

Test Plan: Imported from OSS

Reviewed By: anjali411

Differential Revision: D32364022

Pulled By: suo

fbshipit-source-id: 740892d9580edc348c3e818664fd37f145669fda
diff --git a/.lintrunner.toml b/.lintrunner.toml
index f07be52..9311247 100644
--- a/.lintrunner.toml
+++ b/.lintrunner.toml
@@ -340,7 +340,7 @@
 ]
 command = [
     'python3',
-    'tools/linter/adapters/cppexec_linter.py',
+    'tools/linter/adapters/exec_linter.py',
     '--',
     '@{{PATHSFILE}}',
 ]
@@ -388,3 +388,34 @@
     '--',
     '@{{PATHSFILE}}'
 ]
+
+[[linter]]
+code = 'CMAKE'
+include_patterns = [
+    "**/*.cmake",
+    "**/*.cmake.in",
+    "**/CMakeLists.txt",
+]
+exclude_patterns = [
+    'cmake/Modules/**',
+    'cmake/Modules_CUDA_fix/**',
+    'cmake/Caffe2Config.cmake.in',
+    'aten/src/ATen/ATenConfig.cmake.in',
+    'cmake/Caffe2ConfigVersion.cmake.in',
+    'cmake/TorchConfig.cmake.in',
+    'cmake/TorchConfigVersion.cmake.in',
+    'cmake/cmake_uninstall.cmake.i',
+]
+command = [
+    'python3',
+    'tools/linter/adapters/cmake_linter.py',
+    '--config=.cmakelintrc',
+    '--',
+    '@{{PATHSFILE}}',
+]
+init_command = [
+    'python3',
+    'tools/linter/adapters/pip_init.py',
+    '--dry-run={{DRYRUN}}',
+    'cmakelint==1.4.1',
+]
diff --git a/tools/linter/adapters/cmake_linter.py b/tools/linter/adapters/cmake_linter.py
new file mode 100644
index 0000000..0847f56
--- /dev/null
+++ b/tools/linter/adapters/cmake_linter.py
@@ -0,0 +1,139 @@
+import argparse
+import concurrent.futures
+import json
+import logging
+import os
+import re
+import subprocess
+import time
+from enum import Enum
+from typing import List, NamedTuple, Optional, Pattern
+
+
+LINTER_CODE = "CMAKE"
+
+
+class LintSeverity(str, Enum):
+    ERROR = "error"
+    WARNING = "warning"
+    ADVICE = "advice"
+    DISABLED = "disabled"
+
+
+class LintMessage(NamedTuple):
+    path: Optional[str]
+    line: Optional[int]
+    char: Optional[int]
+    code: str
+    severity: LintSeverity
+    name: str
+    original: Optional[str]
+    replacement: Optional[str]
+    description: Optional[str]
+
+
+# CMakeLists.txt:901: Lines should be <= 80 characters long [linelength]
+RESULTS_RE: Pattern[str] = re.compile(
+    r"""(?mx)
+    ^
+    (?P<file>.*?):
+    (?P<line>\d+):
+    \s(?P<message>.*)
+    \s(?P<code>\[.*\])
+    $
+    """
+)
+
+
+def run_command(
+    args: List[str],
+) -> "subprocess.CompletedProcess[bytes]":
+    logging.debug("$ %s", " ".join(args))
+    start_time = time.monotonic()
+    try:
+        return subprocess.run(
+            args,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+        )
+    finally:
+        end_time = time.monotonic()
+        logging.debug("took %dms", (end_time - start_time) * 1000)
+
+
+def check_file(
+    filename: str,
+    config: str,
+) -> List[LintMessage]:
+    try:
+        proc = run_command(
+            ["cmakelint", f"--config={config}", filename],
+        )
+    except OSError as err:
+        return [
+            LintMessage(
+                path=None,
+                line=None,
+                char=None,
+                code=LINTER_CODE,
+                severity=LintSeverity.ERROR,
+                name="command-failed",
+                original=None,
+                replacement=None,
+                description=(f"Failed due to {err.__class__.__name__}:\n{err}"),
+            )
+        ]
+    stdout = str(proc.stdout, "utf-8").strip()
+    return [
+        LintMessage(
+            path=match["file"],
+            name=match["code"],
+            description=match["message"],
+            line=int(match["line"]),
+            char=None,
+            code=LINTER_CODE,
+            severity=LintSeverity.ERROR,
+            original=None,
+            replacement=None,
+        )
+        for match in RESULTS_RE.finditer(stdout)
+    ]
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser(
+        description="cmakelint runner",
+        fromfile_prefix_chars="@",
+    )
+    parser.add_argument(
+        "--config",
+        required=True,
+        help="location of cmakelint config",
+    )
+    parser.add_argument(
+        "filenames",
+        nargs="+",
+        help="paths to lint",
+    )
+
+    args = parser.parse_args()
+
+    with concurrent.futures.ThreadPoolExecutor(
+        max_workers=os.cpu_count(),
+        thread_name_prefix="Thread",
+    ) as executor:
+        futures = {
+            executor.submit(
+                check_file,
+                filename,
+                args.config,
+            ): filename
+            for filename in args.filenames
+        }
+        for future in concurrent.futures.as_completed(futures):
+            try:
+                for lint_message in future.result():
+                    print(json.dumps(lint_message._asdict()), flush=True)
+            except Exception:
+                logging.critical('Failed at "%s".', futures[future])
+                raise