Add a script and lint check for maximum path lengths. (#10367)

Windows has difficulty building the project when directory and file
paths get too long. This adds a lint check for maximum directory path
length as a nudge to keep us from adding any paths longer than our
current longest.

| | |
| ---- | ---- |
| Sample failure |
https://github.com/iree-org/iree/actions/runs/3040322825/jobs/4896242190
|
| Sample success |
https://github.com/iree-org/iree/actions/runs/3040651662/jobs/4896945763
|

skip-ci
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 5813290..e07e9ef 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -128,3 +128,11 @@
         run: git fetch --no-tags --prune --depth=1 origin "${GITHUB_BASE_REF?}:${GITHUB_BASE_REF?}"
       - name: yamllint
         run: ./build_tools/scripts/run_yamllint.sh "${GITHUB_BASE_REF?}"
+
+  path_lengths:
+    runs-on: ubuntu-20.04
+    steps:
+      - name: Checking out repository
+        uses: actions/checkout@7884fcad6b5d53d10323aee724dc68d8b9096a2e # v2
+      - name: Running check_path_lengths
+        run: ./build_tools/scripts/check_path_lengths.py
diff --git a/build_tools/scripts/check_path_lengths.py b/build_tools/scripts/check_path_lengths.py
new file mode 100755
index 0000000..645ba7d
--- /dev/null
+++ b/build_tools/scripts/check_path_lengths.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env python3
+# Copyright 2022 The IREE Authors
+#
+# Licensed under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+# This scans the IREE source tree for long path lengths, which are problematic
+# on Windows: https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
+#
+# We ultimately care that the build system is happy, but CMake on Windows in
+# particular does not actually give early or easy to understand error messages,
+# and developers/CI using Linux may still want to see warnings. We'll use
+# relative directory path length as a reasonable heuristic for "will the build
+# system be happy?", since CMake tends to create paths like this:
+# `iree/compiler/.../Foo/CMakeFiles/iree_compiler_Foo_Foo.objects.dir/bar.obj`.
+# Note that 'Foo' appears three times in that path, so that's typically the best
+# place to trim characters (and not file names).
+#
+# To check that all relative paths are shorter than the default limit:
+#   python check_path_lengths.py
+#
+# To check that all relative paths are shorter than a custom limit:
+#   python check_path_lengths.py --limit=50
+
+import argparse
+import os
+import pathlib
+import sys
+
+
+def parse_arguments():
+  parser = argparse.ArgumentParser(description="Path length checker")
+  # The default limit was selected based on repository state when this script
+  # was added. If the max path length decreases, consider lowering this too.
+  parser.add_argument("--limit",
+                      help="Path length limit (inclusive)",
+                      type=int,
+                      default=75)
+  parser.add_argument(
+      "--include_tests",
+      help=
+      "Includes /test directories. False by default as these don't usually generate problematic files during the build",
+      action="store_true",
+      default=False)
+  parser.add_argument("--verbose",
+                      help="Outputs detailed information about path lengths",
+                      action="store_true",
+                      default=False)
+  args = parser.parse_args()
+  return args
+
+
+def main(args):
+  repo_root = pathlib.Path(__file__).parent.parent.parent
+
+  # Just look at the compiler directory for now, since it has historically had
+  # by far the longest paths.
+  walk_root = os.path.join(repo_root, "compiler")
+
+  longest_path_length = -1
+  long_paths = []
+  short_paths = []
+  for dirpath, dirnames, _ in os.walk(walk_root):
+    # Don't descend into test directories, since they typically don't generate
+    # object files or binaries that could trip up the build system.
+    if not args.include_tests and "test" in dirnames:
+      dirnames.remove("test")
+
+    path = pathlib.Path(dirpath).relative_to(repo_root).as_posix()
+    if len(path) > args.limit:
+      long_paths.append(path)
+    else:
+      short_paths.append(path)
+    longest_path_length = max(longest_path_length, len(path))
+  long_paths.sort(key=len)
+  short_paths.sort(key=len)
+
+  if args.verbose and short_paths:
+    print(f"These paths are shorter than the limit of {args.limit} characters:")
+    for path in short_paths:
+      print("{:3d}, {}".format(len(path), path))
+
+  if long_paths:
+    print(f"These paths are longer than the limit of {args.limit} characters:")
+    for path in long_paths:
+      print("{:3d}, {}".format(len(path), path))
+    print(
+        f"Error: {len(long_paths)} source paths are longer than {args.limit} characters."
+    )
+    print("  Long paths can be problematic when building on Windows.")
+    print("  Please look at the output above and trim the paths.")
+    sys.exit(1)
+  else:
+    print(f"All path lengths are under the limit of {args.limit} characters.")
+
+
+if __name__ == "__main__":
+  main(parse_arguments())
diff --git a/build_tools/scripts/lint.sh b/build_tools/scripts/lint.sh
index 06ac125..ebab0dd 100755
--- a/build_tools/scripts/lint.sh
+++ b/build_tools/scripts/lint.sh
@@ -120,6 +120,9 @@
   echo "'yamllint' not found. Skipping check"
 fi
 
+echo "***** Path Lengths *****"
+./build_tools/scripts/check_path_lengths.py
+
 if (( "${FINAL_RET}" != 0 )); then
   echo "Encountered failures. Check error messages and changes to the working" \
        "directory and git index (which may contain fixes) and try again."