|
| 1 | +import hashlib |
| 2 | +import subprocess |
| 3 | +import sys |
| 4 | +from pathlib import Path |
| 5 | + |
| 6 | +import click |
| 7 | +import spin |
| 8 | + |
| 9 | + |
| 10 | +def file_digest(file, algorithm: str): |
| 11 | + try: |
| 12 | + return hashlib.file_digest(file, algorithm) |
| 13 | + except AttributeError: |
| 14 | + pass # Fallback to manual implementation below |
| 15 | + hash = hashlib.new(algorithm) |
| 16 | + while chunk := file.read(8192): |
| 17 | + hash.update(chunk) |
| 18 | + return hash |
| 19 | + |
| 20 | + |
| 21 | +def _hash_file(file): |
| 22 | + with open(file, "rb") as f: |
| 23 | + hash = file_digest(f, "sha256") |
| 24 | + return hash.hexdigest() |
| 25 | + |
| 26 | + |
| 27 | +def _hash_files(files): |
| 28 | + hashes = {file: _hash_file(file) for file in files} |
| 29 | + return hashes |
| 30 | + |
| 31 | + |
| 32 | +def _read_hashes(hash_file: Path): |
| 33 | + if not hash_file.exists(): |
| 34 | + return {} |
| 35 | + with hash_file.open("r") as f: |
| 36 | + lines = f.readlines() |
| 37 | + hashes = {} |
| 38 | + for line in lines: |
| 39 | + hash = line[:64] |
| 40 | + file = line[66:].strip() |
| 41 | + hashes[file] = hash |
| 42 | + return hashes |
| 43 | + |
| 44 | + |
| 45 | +def _updated_hashes(hash_file, files_to_hash): |
| 46 | + old_hashes = _read_hashes(hash_file) |
| 47 | + new_hashes = _hash_files(files_to_hash) |
| 48 | + if new_hashes != old_hashes: |
| 49 | + return new_hashes |
| 50 | + return None |
| 51 | + |
| 52 | + |
| 53 | +@click.command() |
| 54 | +def regenerate_version(): |
| 55 | + """Regenerate version.py.""" |
| 56 | + cmd = [ |
| 57 | + sys.executable, |
| 58 | + "-m", |
| 59 | + "tools.generate_torch_version", |
| 60 | + "--is-debug=false", |
| 61 | + ] |
| 62 | + spin.util.run(cmd) |
| 63 | + |
| 64 | + |
| 65 | +TYPE_STUBS = [ |
| 66 | + ( |
| 67 | + "Pytorch type stubs", |
| 68 | + Path(".lintbin/.pytorch-type-stubs.sha256"), |
| 69 | + [ |
| 70 | + "aten/src/ATen/native/native_functions.yaml", |
| 71 | + "aten/src/ATen/native/tags.yaml", |
| 72 | + "tools/autograd/deprecated.yaml", |
| 73 | + ], |
| 74 | + [ |
| 75 | + sys.executable, |
| 76 | + "-m", |
| 77 | + "tools.pyi.gen_pyi", |
| 78 | + "--native-functions-path", |
| 79 | + "aten/src/ATen/native/native_functions.yaml", |
| 80 | + "--tags-path", |
| 81 | + "aten/src/ATen/native/tags.yaml", |
| 82 | + "--deprecated-functions-path", |
| 83 | + "tools/autograd/deprecated.yaml", |
| 84 | + ], |
| 85 | + ), |
| 86 | + ( |
| 87 | + "Datapipes type stubs", |
| 88 | + None, |
| 89 | + [], |
| 90 | + [ |
| 91 | + sys.executable, |
| 92 | + "torch/utils/data/datapipes/gen_pyi.py", |
| 93 | + ], |
| 94 | + ), |
| 95 | +] |
| 96 | + |
| 97 | + |
| 98 | +@click.command() |
| 99 | +def regenerate_type_stubs(): |
| 100 | + """Regenerate type stubs.""" |
| 101 | + for name, hash_file, files_to_hash, cmd in TYPE_STUBS: |
| 102 | + if hash_file: |
| 103 | + if hashes := _updated_hashes(hash_file, files_to_hash): |
| 104 | + click.echo( |
| 105 | + f"Changes detected in type stub files for {name}. Regenerating..." |
| 106 | + ) |
| 107 | + spin.util.run(cmd) |
| 108 | + hash_file.parent.mkdir(parents=True, exist_ok=True) |
| 109 | + with hash_file.open("w") as f: |
| 110 | + for file, hash in hashes.items(): |
| 111 | + f.write(f"{hash} {file}\n") |
| 112 | + click.echo("Type stubs and hashes updated.") |
| 113 | + else: |
| 114 | + click.echo(f"No changes detected in type stub files for {name}.") |
| 115 | + else: |
| 116 | + click.echo(f"No hash file for {name}. Regenerating...") |
| 117 | + spin.util.run(cmd) |
| 118 | + click.echo("Type stubs regenerated.") |
| 119 | + |
| 120 | + |
| 121 | +@click.command() |
| 122 | +def regenerate_clangtidy_files(): |
| 123 | + """Regenerate clang-tidy files.""" |
| 124 | + cmd = [ |
| 125 | + sys.executable, |
| 126 | + "-m", |
| 127 | + "tools.linter.clang_tidy.generate_build_files", |
| 128 | + ] |
| 129 | + spin.util.run(cmd) |
| 130 | + |
| 131 | + |
| 132 | +#: These linters are expected to need less than 3s cpu time total |
| 133 | +VERY_FAST_LINTERS = { |
| 134 | + "ATEN_CPU_GPU_AGNOSTIC", |
| 135 | + "BAZEL_LINTER", |
| 136 | + "C10_NODISCARD", |
| 137 | + "C10_UNUSED", |
| 138 | + "CALL_ONCE", |
| 139 | + "CMAKE_MINIMUM_REQUIRED", |
| 140 | + "CONTEXT_DECORATOR", |
| 141 | + "COPYRIGHT", |
| 142 | + "CUBINCLUDE", |
| 143 | + "DEPLOY_DETECTION", |
| 144 | + "ERROR_PRONE_ISINSTANCE", |
| 145 | + "EXEC", |
| 146 | + "HEADER_ONLY_LINTER", |
| 147 | + "IMPORT_LINTER", |
| 148 | + "INCLUDE", |
| 149 | + "LINTRUNNER_VERSION", |
| 150 | + "MERGE_CONFLICTLESS_CSV", |
| 151 | + "META_NO_CREATE_UNBACKED", |
| 152 | + "NEWLINE", |
| 153 | + "NOQA", |
| 154 | + "NO_WORKFLOWS_ON_FORK", |
| 155 | + "ONCE_FLAG", |
| 156 | + "PYBIND11_INCLUDE", |
| 157 | + "PYBIND11_SPECIALIZATION", |
| 158 | + "PYPIDEP", |
| 159 | + "PYPROJECT", |
| 160 | + "RAWCUDA", |
| 161 | + "RAWCUDADEVICE", |
| 162 | + "ROOT_LOGGING", |
| 163 | + "TABS", |
| 164 | + "TESTOWNERS", |
| 165 | + "TYPEIGNORE", |
| 166 | + "TYPENOSKIP", |
| 167 | + "WORKFLOWSYNC", |
| 168 | +} |
| 169 | + |
| 170 | + |
| 171 | +#: These linters are expected to take a few seconds, but less than 10s cpu time total |
| 172 | +FAST_LINTERS = { |
| 173 | + "CMAKE", |
| 174 | + "DOCSTRING_LINTER", |
| 175 | + "GHA", |
| 176 | + "NATIVEFUNCTIONS", |
| 177 | + "RUFF", |
| 178 | + "SET_LINTER", |
| 179 | + "SHELLCHECK", |
| 180 | + "SPACES", |
| 181 | +} |
| 182 | + |
| 183 | + |
| 184 | +#: These linters are expected to take more than 10s cpu time total; |
| 185 | +#: some need more than 1 hour. |
| 186 | +SLOW_LINTERS = { |
| 187 | + "ACTIONLINT", |
| 188 | + "CLANGFORMAT", |
| 189 | + "CLANGTIDY", |
| 190 | + "CODESPELL", |
| 191 | + "FLAKE8", |
| 192 | + "GB_REGISTRY", |
| 193 | + "PYFMT", |
| 194 | + "PYREFLY", |
| 195 | + "TEST_DEVICE_BIAS", |
| 196 | + "TEST_HAS_MAIN", |
| 197 | +} |
| 198 | + |
| 199 | + |
| 200 | +ALL_LINTERS = VERY_FAST_LINTERS | FAST_LINTERS | SLOW_LINTERS |
| 201 | + |
| 202 | + |
| 203 | +LINTRUNNER_CACHE_INFO = ( |
| 204 | + Path(".lintbin/.lintrunner.sha256"), |
| 205 | + [ |
| 206 | + "requirements.txt", |
| 207 | + "pyproject.toml", |
| 208 | + ".lintrunner.toml", |
| 209 | + ], |
| 210 | +) |
| 211 | + |
| 212 | + |
| 213 | +LINTRUNNER_BASE_CMD = [ |
| 214 | + "uvx", |
| 215 | + "--python", |
| 216 | + "3.10", |
| 217 | + |
| 218 | +] |
| 219 | + |
| 220 | + |
| 221 | +@click.command() |
| 222 | +def setup_lint(): |
| 223 | + """Set up lintrunner with current CI version.""" |
| 224 | + cmd = LINTRUNNER_BASE_CMD + ["init"] |
| 225 | + subprocess.run(cmd, check=True, capture_output=True, text=True) |
| 226 | + |
| 227 | + |
| 228 | +def _check_linters(): |
| 229 | + cmd = LINTRUNNER_BASE_CMD + ["list"] |
| 230 | + ret = spin.util.run(cmd, output=False, stderr=subprocess.PIPE) |
| 231 | + linters = {l.strip() for l in ret.stdout.decode().strip().split("\n")[1:]} |
| 232 | + unknown_linters = linters - ALL_LINTERS |
| 233 | + missing_linters = ALL_LINTERS - linters |
| 234 | + if unknown_linters: |
| 235 | + click.secho( |
| 236 | + f"Unknown linters found; please add them to the correct category " |
| 237 | + f"in .spin/cmds.py: {', '.join(unknown_linters)}", |
| 238 | + fg="yellow", |
| 239 | + ) |
| 240 | + if missing_linters: |
| 241 | + click.secho( |
| 242 | + f"Missing linters found; please update the corresponding category " |
| 243 | + f"in .spin/cmds.py: {', '.join(missing_linters)}", |
| 244 | + fg="yellow", |
| 245 | + ) |
| 246 | + return unknown_linters, missing_linters |
| 247 | + |
| 248 | + |
| 249 | +@spin.util.extend_command( |
| 250 | + setup_lint, |
| 251 | + doc=f""" |
| 252 | + If configuration has changed, update lintrunner. |
| 253 | +
|
| 254 | + Compares the stored old hashes of configuration files with new ones and |
| 255 | + performs setup via setup-lint if the hashes have changed. |
| 256 | + Hashes are stored in {LINTRUNNER_CACHE_INFO[0]}; the following files are |
| 257 | + considered: {", ".join(LINTRUNNER_CACHE_INFO[1])}. |
| 258 | + """, |
| 259 | +) |
| 260 | +@click.pass_context |
| 261 | +def lazy_setup_lint(ctx, parent_callback, **kwargs): |
| 262 | + if hashes := _updated_hashes(*LINTRUNNER_CACHE_INFO): |
| 263 | + click.echo( |
| 264 | + "Changes detected in lint configuration files. Setting up linting tools..." |
| 265 | + ) |
| 266 | + parent_callback(**kwargs) |
| 267 | + hash_file = LINTRUNNER_CACHE_INFO[0] |
| 268 | + hash_file.parent.mkdir(parents=True, exist_ok=True) |
| 269 | + with hash_file.open("w") as f: |
| 270 | + for file, hash in hashes.items(): |
| 271 | + f.write(f"{hash} {file}\n") |
| 272 | + click.echo("Linting tools set up and hashes updated.") |
| 273 | + else: |
| 274 | + click.echo("No changes detected in lint configuration files. Skipping setup.") |
| 275 | + click.echo("Regenerating version...") |
| 276 | + ctx.invoke(regenerate_version) |
| 277 | + click.echo("Regenerating type stubs...") |
| 278 | + ctx.invoke(regenerate_type_stubs) |
| 279 | + click.echo("Done.") |
| 280 | + _check_linters() |
| 281 | + |
| 282 | + |
| 283 | +@click.command() |
| 284 | +@click.option("-a", "--apply-patches", is_flag=True) |
| 285 | +@click.pass_context |
| 286 | +def lint(ctx, apply_patches, **kwargs): |
| 287 | + """Lint all files.""" |
| 288 | + ctx.invoke(lazy_setup_lint) |
| 289 | + all_files_linters = VERY_FAST_LINTERS | FAST_LINTERS |
| 290 | + changed_files_linters = SLOW_LINTERS |
| 291 | + cmd = LINTRUNNER_BASE_CMD |
| 292 | + if apply_patches: |
| 293 | + cmd += ["--apply-patches"] |
| 294 | + all_files_cmd = cmd + [ |
| 295 | + "--take", |
| 296 | + ",".join(all_files_linters), |
| 297 | + "--all-files", |
| 298 | + ] |
| 299 | + spin.util.run(all_files_cmd) |
| 300 | + changed_files_cmd = cmd + [ |
| 301 | + "--take", |
| 302 | + ",".join(changed_files_linters), |
| 303 | + ] |
| 304 | + spin.util.run(changed_files_cmd) |
| 305 | + |
| 306 | + |
| 307 | +@click.command() |
| 308 | +@click.pass_context |
| 309 | +def fixlint(ctx, **kwargs): |
| 310 | + """Autofix all files.""" |
| 311 | + ctx.invoke(lint, apply_patches=True) |
| 312 | + |
| 313 | + |
| 314 | +@click.command() |
| 315 | +@click.option("-a", "--apply-patches", is_flag=True) |
| 316 | +@click.pass_context |
| 317 | +def quicklint(ctx, apply_patches, **kwargs): |
| 318 | + """Lint changed files.""" |
| 319 | + ctx.invoke(lazy_setup_lint) |
| 320 | + cmd = LINTRUNNER_BASE_CMD |
| 321 | + if apply_patches: |
| 322 | + cmd += ["--apply-patches"] |
| 323 | + spin.util.run(cmd) |
| 324 | + |
| 325 | + |
| 326 | +@click.command() |
| 327 | +@click.pass_context |
| 328 | +def quickfix(ctx, **kwargs): |
| 329 | + """Autofix changed files.""" |
| 330 | + ctx.invoke(quicklint, apply_patches=True) |
0 commit comments