1*760c253cSXin Li#!/usr/bin/env python3 2*760c253cSXin Li# 3*760c253cSXin Li# Copyright 2019 The ChromiumOS Authors 4*760c253cSXin Li# Use of this source code is governed by a BSD-style license that can be 5*760c253cSXin Li# found in the LICENSE file. 6*760c253cSXin Li 7*760c253cSXin Li"""Runs presubmit checks against a bundle of files.""" 8*760c253cSXin Li 9*760c253cSXin Liimport argparse 10*760c253cSXin Liimport dataclasses 11*760c253cSXin Liimport datetime 12*760c253cSXin Liimport multiprocessing 13*760c253cSXin Liimport multiprocessing.pool 14*760c253cSXin Liimport os 15*760c253cSXin Lifrom pathlib import Path 16*760c253cSXin Liimport re 17*760c253cSXin Liimport shlex 18*760c253cSXin Liimport shutil 19*760c253cSXin Liimport subprocess 20*760c253cSXin Liimport sys 21*760c253cSXin Liimport textwrap 22*760c253cSXin Liimport threading 23*760c253cSXin Liimport traceback 24*760c253cSXin Lifrom typing import ( 25*760c253cSXin Li Dict, 26*760c253cSXin Li Iterable, 27*760c253cSXin Li List, 28*760c253cSXin Li NamedTuple, 29*760c253cSXin Li Optional, 30*760c253cSXin Li Sequence, 31*760c253cSXin Li Tuple, 32*760c253cSXin Li Union, 33*760c253cSXin Li) 34*760c253cSXin Li 35*760c253cSXin Li 36*760c253cSXin Li# This was originally had many packages in it (notably scipy) 37*760c253cSXin Li# but due to changes in how scipy is built, we can no longer install 38*760c253cSXin Li# it in the chroot. See b/284489250 39*760c253cSXin Li# 40*760c253cSXin Li# For type checking Python code, we also need mypy. This isn't 41*760c253cSXin Li# listed here because (1) only very few files are actually type checked, 42*760c253cSXin Li# so we don't pull the dependency in unless needed, and (2) mypy 43*760c253cSXin Li# may be installed through other means than pip. 44*760c253cSXin LiPIP_DEPENDENCIES = ("numpy",) 45*760c253cSXin Li 46*760c253cSXin Li 47*760c253cSXin Li# Each checker represents an independent check that's done on our sources. 48*760c253cSXin Li# 49*760c253cSXin Li# They should: 50*760c253cSXin Li# - never write to stdout/stderr or read from stdin directly 51*760c253cSXin Li# - return either a CheckResult, or a list of [(subcheck_name, CheckResult)] 52*760c253cSXin Li# - ideally use thread_pool to check things concurrently 53*760c253cSXin Li# - though it's important to note that these *also* live on the threadpool 54*760c253cSXin Li# we've provided. It's the caller's responsibility to guarantee that at 55*760c253cSXin Li# least ${number_of_concurrently_running_checkers}+1 threads are present 56*760c253cSXin Li# in the pool. In order words, blocking on results from the provided 57*760c253cSXin Li# threadpool is OK. 58*760c253cSXin LiCheckResult = NamedTuple( 59*760c253cSXin Li "CheckResult", 60*760c253cSXin Li ( 61*760c253cSXin Li ("ok", bool), 62*760c253cSXin Li ("output", str), 63*760c253cSXin Li ("autofix_commands", List[List[str]]), 64*760c253cSXin Li ), 65*760c253cSXin Li) 66*760c253cSXin Li 67*760c253cSXin Li 68*760c253cSXin LiCommand = Sequence[Union[str, os.PathLike]] 69*760c253cSXin LiCheckResults = Union[List[Tuple[str, CheckResult]], CheckResult] 70*760c253cSXin Li 71*760c253cSXin Li 72*760c253cSXin Li# The files and directories on which we run the mypy typechecker. The paths are 73*760c253cSXin Li# relative to the root of the toolchain-utils repository. 74*760c253cSXin LiMYPY_CHECKED_PATHS = ( 75*760c253cSXin Li "afdo_tools/update_kernel_afdo.py", 76*760c253cSXin Li "check_portable_toolchains.py", 77*760c253cSXin Li "cros_utils/bugs.py", 78*760c253cSXin Li "cros_utils/bugs_test.py", 79*760c253cSXin Li "cros_utils/tiny_render.py", 80*760c253cSXin Li "llvm_tools", 81*760c253cSXin Li "pgo_tools", 82*760c253cSXin Li "pgo_tools_rust/pgo_rust.py", 83*760c253cSXin Li "rust_tools", 84*760c253cSXin Li "toolchain_utils_githooks/check-presubmit.py", 85*760c253cSXin Li) 86*760c253cSXin Li 87*760c253cSXin Li 88*760c253cSXin Lidef run_command_unchecked( 89*760c253cSXin Li command: Command, 90*760c253cSXin Li cwd: Optional[str] = None, 91*760c253cSXin Li env: Optional[Dict[str, str]] = None, 92*760c253cSXin Li) -> Tuple[int, str]: 93*760c253cSXin Li """Runs a command in the given dir, returning its exit code and stdio.""" 94*760c253cSXin Li p = subprocess.run( 95*760c253cSXin Li command, 96*760c253cSXin Li check=False, 97*760c253cSXin Li cwd=cwd, 98*760c253cSXin Li stdin=subprocess.DEVNULL, 99*760c253cSXin Li stdout=subprocess.PIPE, 100*760c253cSXin Li stderr=subprocess.STDOUT, 101*760c253cSXin Li env=env, 102*760c253cSXin Li encoding="utf-8", 103*760c253cSXin Li errors="replace", 104*760c253cSXin Li ) 105*760c253cSXin Li return p.returncode, p.stdout 106*760c253cSXin Li 107*760c253cSXin Li 108*760c253cSXin Lidef has_executable_on_path(exe: str) -> bool: 109*760c253cSXin Li """Returns whether we have `exe` somewhere on our $PATH""" 110*760c253cSXin Li return shutil.which(exe) is not None 111*760c253cSXin Li 112*760c253cSXin Li 113*760c253cSXin Lidef remove_deleted_files(files: Iterable[str]) -> List[str]: 114*760c253cSXin Li return [f for f in files if os.path.exists(f)] 115*760c253cSXin Li 116*760c253cSXin Li 117*760c253cSXin Lidef is_file_executable(file_path: str) -> bool: 118*760c253cSXin Li return os.access(file_path, os.X_OK) 119*760c253cSXin Li 120*760c253cSXin Li 121*760c253cSXin Li# As noted in our docs, some of our Python code depends on modules that sit in 122*760c253cSXin Li# toolchain-utils/. Add that to PYTHONPATH to ensure that things like `cros 123*760c253cSXin Li# lint` are kept happy. 124*760c253cSXin Lidef env_with_pythonpath(toolchain_utils_root: str) -> Dict[str, str]: 125*760c253cSXin Li env = dict(os.environ) 126*760c253cSXin Li if "PYTHONPATH" in env: 127*760c253cSXin Li env["PYTHONPATH"] += ":" + toolchain_utils_root 128*760c253cSXin Li else: 129*760c253cSXin Li env["PYTHONPATH"] = toolchain_utils_root 130*760c253cSXin Li return env 131*760c253cSXin Li 132*760c253cSXin Li 133*760c253cSXin Li@dataclasses.dataclass(frozen=True) 134*760c253cSXin Liclass MyPyInvocation: 135*760c253cSXin Li """An invocation of mypy.""" 136*760c253cSXin Li 137*760c253cSXin Li command: List[str] 138*760c253cSXin Li # Entries to add to PYTHONPATH, formatted for direct use in the PYTHONPATH 139*760c253cSXin Li # env var. 140*760c253cSXin Li pythonpath_additions: str 141*760c253cSXin Li 142*760c253cSXin Li 143*760c253cSXin Lidef get_mypy() -> Optional[MyPyInvocation]: 144*760c253cSXin Li """Finds the mypy executable and returns a command to invoke it. 145*760c253cSXin Li 146*760c253cSXin Li If mypy cannot be found and we're inside the chroot, this 147*760c253cSXin Li function installs mypy and returns a command to invoke it. 148*760c253cSXin Li 149*760c253cSXin Li If mypy cannot be found and we're outside the chroot, this 150*760c253cSXin Li returns None. 151*760c253cSXin Li 152*760c253cSXin Li Returns: 153*760c253cSXin Li An optional tuple containing: 154*760c253cSXin Li - the command to invoke mypy, and 155*760c253cSXin Li - any environment variables to set when invoking mypy 156*760c253cSXin Li """ 157*760c253cSXin Li if has_executable_on_path("mypy"): 158*760c253cSXin Li return MyPyInvocation(command=["mypy"], pythonpath_additions="") 159*760c253cSXin Li pip = get_pip() 160*760c253cSXin Li if not pip: 161*760c253cSXin Li assert not is_in_chroot() 162*760c253cSXin Li return None 163*760c253cSXin Li 164*760c253cSXin Li def get_from_pip() -> Optional[MyPyInvocation]: 165*760c253cSXin Li rc, output = run_command_unchecked(pip + ["show", "mypy"]) 166*760c253cSXin Li if rc: 167*760c253cSXin Li return None 168*760c253cSXin Li 169*760c253cSXin Li m = re.search(r"^Location: (.*)", output, re.MULTILINE) 170*760c253cSXin Li if not m: 171*760c253cSXin Li return None 172*760c253cSXin Li 173*760c253cSXin Li pythonpath = m.group(1) 174*760c253cSXin Li return MyPyInvocation( 175*760c253cSXin Li command=[ 176*760c253cSXin Li "python3", 177*760c253cSXin Li "-m", 178*760c253cSXin Li "mypy", 179*760c253cSXin Li ], 180*760c253cSXin Li pythonpath_additions=pythonpath, 181*760c253cSXin Li ) 182*760c253cSXin Li 183*760c253cSXin Li from_pip = get_from_pip() 184*760c253cSXin Li if from_pip: 185*760c253cSXin Li return from_pip 186*760c253cSXin Li 187*760c253cSXin Li if is_in_chroot(): 188*760c253cSXin Li assert pip is not None 189*760c253cSXin Li subprocess.check_call(pip + ["install", "--user", "mypy"]) 190*760c253cSXin Li return get_from_pip() 191*760c253cSXin Li return None 192*760c253cSXin Li 193*760c253cSXin Li 194*760c253cSXin Lidef get_pip() -> Optional[List[str]]: 195*760c253cSXin Li """Finds pip and returns a command to invoke it. 196*760c253cSXin Li 197*760c253cSXin Li If pip cannot be found, this function attempts to install 198*760c253cSXin Li pip and returns a command to invoke it. 199*760c253cSXin Li 200*760c253cSXin Li If pip cannot be found, this function returns None. 201*760c253cSXin Li """ 202*760c253cSXin Li have_pip = can_import_py_module("pip") 203*760c253cSXin Li if not have_pip: 204*760c253cSXin Li print("Autoinstalling `pip`...") 205*760c253cSXin Li subprocess.check_call(["python", "-m", "ensurepip"]) 206*760c253cSXin Li have_pip = can_import_py_module("pip") 207*760c253cSXin Li 208*760c253cSXin Li if have_pip: 209*760c253cSXin Li return ["python", "-m", "pip"] 210*760c253cSXin Li return None 211*760c253cSXin Li 212*760c253cSXin Li 213*760c253cSXin Lidef get_check_result_or_catch( 214*760c253cSXin Li task: multiprocessing.pool.ApplyResult, 215*760c253cSXin Li) -> CheckResult: 216*760c253cSXin Li """Returns the result of task(); if that raises, returns a CheckResult. 217*760c253cSXin Li 218*760c253cSXin Li The task is expected to return a CheckResult on get(). 219*760c253cSXin Li """ 220*760c253cSXin Li try: 221*760c253cSXin Li return task.get() 222*760c253cSXin Li except Exception: 223*760c253cSXin Li return CheckResult( 224*760c253cSXin Li ok=False, 225*760c253cSXin Li output="Check exited with an unexpected exception:\n%s" 226*760c253cSXin Li % traceback.format_exc(), 227*760c253cSXin Li autofix_commands=[], 228*760c253cSXin Li ) 229*760c253cSXin Li 230*760c253cSXin Li 231*760c253cSXin Lidef check_isort( 232*760c253cSXin Li toolchain_utils_root: str, python_files: Iterable[str] 233*760c253cSXin Li) -> CheckResult: 234*760c253cSXin Li """Subchecker of check_py_format. Checks python file formats with isort""" 235*760c253cSXin Li chromite = Path("/mnt/host/source/chromite") 236*760c253cSXin Li isort = chromite / "scripts" / "isort" 237*760c253cSXin Li config_file = chromite / ".isort.cfg" 238*760c253cSXin Li 239*760c253cSXin Li if not (isort.exists() and config_file.exists()): 240*760c253cSXin Li return CheckResult( 241*760c253cSXin Li ok=True, 242*760c253cSXin Li output="isort not found; skipping", 243*760c253cSXin Li autofix_commands=[], 244*760c253cSXin Li ) 245*760c253cSXin Li 246*760c253cSXin Li config_file_flag = f"--settings-file={config_file}" 247*760c253cSXin Li command = [str(isort), "-c", config_file_flag] + list(python_files) 248*760c253cSXin Li exit_code, stdout_and_stderr = run_command_unchecked( 249*760c253cSXin Li command, cwd=toolchain_utils_root 250*760c253cSXin Li ) 251*760c253cSXin Li 252*760c253cSXin Li # isort fails when files have broken formatting. 253*760c253cSXin Li if not exit_code: 254*760c253cSXin Li return CheckResult( 255*760c253cSXin Li ok=True, 256*760c253cSXin Li output="", 257*760c253cSXin Li autofix_commands=[], 258*760c253cSXin Li ) 259*760c253cSXin Li 260*760c253cSXin Li bad_files = [] 261*760c253cSXin Li bad_file_re = re.compile( 262*760c253cSXin Li r"^ERROR: (.*) Imports are incorrectly sorted and/or formatted\.$" 263*760c253cSXin Li ) 264*760c253cSXin Li for line in stdout_and_stderr.splitlines(): 265*760c253cSXin Li m = bad_file_re.match(line) 266*760c253cSXin Li if m: 267*760c253cSXin Li (file_name,) = m.groups() 268*760c253cSXin Li bad_files.append(file_name.strip()) 269*760c253cSXin Li 270*760c253cSXin Li if not bad_files: 271*760c253cSXin Li return CheckResult( 272*760c253cSXin Li ok=False, 273*760c253cSXin Li output=f"`{shlex.join(command)}` failed; stdout/stderr:\n" 274*760c253cSXin Li f"{stdout_and_stderr}", 275*760c253cSXin Li autofix_commands=[], 276*760c253cSXin Li ) 277*760c253cSXin Li 278*760c253cSXin Li autofix = [str(isort), config_file_flag] + bad_files 279*760c253cSXin Li return CheckResult( 280*760c253cSXin Li ok=False, 281*760c253cSXin Li output="The following file(s) have formatting errors: %s" % bad_files, 282*760c253cSXin Li autofix_commands=[autofix], 283*760c253cSXin Li ) 284*760c253cSXin Li 285*760c253cSXin Li 286*760c253cSXin Lidef check_black( 287*760c253cSXin Li toolchain_utils_root: str, black: Path, python_files: Iterable[str] 288*760c253cSXin Li) -> CheckResult: 289*760c253cSXin Li """Subchecker of check_py_format. Checks python file formats with black""" 290*760c253cSXin Li # Folks have been bitten by accidentally using multiple formatter 291*760c253cSXin Li # versions in the past. This is an issue, since newer versions of 292*760c253cSXin Li # black may format things differently. Make the version obvious. 293*760c253cSXin Li command: Command = [black, "--version"] 294*760c253cSXin Li exit_code, stdout_and_stderr = run_command_unchecked( 295*760c253cSXin Li command, cwd=toolchain_utils_root 296*760c253cSXin Li ) 297*760c253cSXin Li if exit_code: 298*760c253cSXin Li return CheckResult( 299*760c253cSXin Li ok=False, 300*760c253cSXin Li output="Failed getting black version; " 301*760c253cSXin Li f"stdstreams: {stdout_and_stderr}", 302*760c253cSXin Li autofix_commands=[], 303*760c253cSXin Li ) 304*760c253cSXin Li 305*760c253cSXin Li black_version = stdout_and_stderr.strip() 306*760c253cSXin Li black_invocation: List[str] = [str(black), "--line-length=80"] 307*760c253cSXin Li command = black_invocation + ["--check"] + list(python_files) 308*760c253cSXin Li exit_code, stdout_and_stderr = run_command_unchecked( 309*760c253cSXin Li command, cwd=toolchain_utils_root 310*760c253cSXin Li ) 311*760c253cSXin Li # black fails when files are poorly formatted. 312*760c253cSXin Li if exit_code == 0: 313*760c253cSXin Li return CheckResult( 314*760c253cSXin Li ok=True, 315*760c253cSXin Li output=f"Using {black_version!r}, no issues were found.", 316*760c253cSXin Li autofix_commands=[], 317*760c253cSXin Li ) 318*760c253cSXin Li 319*760c253cSXin Li # Output format looks something like: 320*760c253cSXin Li # f'{complaints}\nOh no!{emojis}\n{summary}' 321*760c253cSXin Li # Whittle it down to complaints. 322*760c253cSXin Li complaints = stdout_and_stderr.split("\nOh no!", 1) 323*760c253cSXin Li if len(complaints) != 2: 324*760c253cSXin Li return CheckResult( 325*760c253cSXin Li ok=False, 326*760c253cSXin Li output=f"Unparseable `black` output:\n{stdout_and_stderr}", 327*760c253cSXin Li autofix_commands=[], 328*760c253cSXin Li ) 329*760c253cSXin Li 330*760c253cSXin Li bad_files = [] 331*760c253cSXin Li errors = [] 332*760c253cSXin Li refmt_prefix = "would reformat " 333*760c253cSXin Li for line in complaints[0].strip().splitlines(): 334*760c253cSXin Li line = line.strip() 335*760c253cSXin Li if line.startswith("error:"): 336*760c253cSXin Li errors.append(line) 337*760c253cSXin Li continue 338*760c253cSXin Li 339*760c253cSXin Li if not line.startswith(refmt_prefix): 340*760c253cSXin Li return CheckResult( 341*760c253cSXin Li ok=False, 342*760c253cSXin Li output=f"Unparseable `black` output:\n{stdout_and_stderr}", 343*760c253cSXin Li autofix_commands=[], 344*760c253cSXin Li ) 345*760c253cSXin Li 346*760c253cSXin Li bad_files.append(line[len(refmt_prefix) :].strip()) 347*760c253cSXin Li 348*760c253cSXin Li # If black had internal errors that it could handle, print them out and exit 349*760c253cSXin Li # without an autofix. 350*760c253cSXin Li if errors: 351*760c253cSXin Li err_str = "\n".join(errors) 352*760c253cSXin Li return CheckResult( 353*760c253cSXin Li ok=False, 354*760c253cSXin Li output=f"Using {black_version!r} had the following errors:\n" 355*760c253cSXin Li f"{err_str}", 356*760c253cSXin Li autofix_commands=[], 357*760c253cSXin Li ) 358*760c253cSXin Li 359*760c253cSXin Li autofix = black_invocation + bad_files 360*760c253cSXin Li return CheckResult( 361*760c253cSXin Li ok=False, 362*760c253cSXin Li output=f"Using {black_version!r}, these file(s) have formatting " 363*760c253cSXin Li f"errors: {bad_files}", 364*760c253cSXin Li autofix_commands=[autofix], 365*760c253cSXin Li ) 366*760c253cSXin Li 367*760c253cSXin Li 368*760c253cSXin Lidef check_mypy( 369*760c253cSXin Li toolchain_utils_root: str, 370*760c253cSXin Li mypy: MyPyInvocation, 371*760c253cSXin Li files: Iterable[str], 372*760c253cSXin Li) -> CheckResult: 373*760c253cSXin Li """Checks type annotations using mypy.""" 374*760c253cSXin Li fixed_env = env_with_pythonpath(toolchain_utils_root) 375*760c253cSXin Li if mypy.pythonpath_additions: 376*760c253cSXin Li new_pythonpath = ( 377*760c253cSXin Li f"{mypy.pythonpath_additions}:{fixed_env['PYTHONPATH']}" 378*760c253cSXin Li ) 379*760c253cSXin Li fixed_env["PYTHONPATH"] = new_pythonpath 380*760c253cSXin Li 381*760c253cSXin Li # Show the version number, mainly for troubleshooting purposes. 382*760c253cSXin Li cmd = mypy.command + ["--version"] 383*760c253cSXin Li exit_code, output = run_command_unchecked( 384*760c253cSXin Li cmd, cwd=toolchain_utils_root, env=fixed_env 385*760c253cSXin Li ) 386*760c253cSXin Li if exit_code: 387*760c253cSXin Li return CheckResult( 388*760c253cSXin Li ok=False, 389*760c253cSXin Li output=f"Failed getting mypy version; stdstreams: {output}", 390*760c253cSXin Li autofix_commands=[], 391*760c253cSXin Li ) 392*760c253cSXin Li # Prefix output with the version information. 393*760c253cSXin Li prefix = f"Using {output.strip()}, " 394*760c253cSXin Li 395*760c253cSXin Li cmd = mypy.command + ["--follow-imports=silent"] + list(files) 396*760c253cSXin Li exit_code, output = run_command_unchecked( 397*760c253cSXin Li cmd, cwd=toolchain_utils_root, env=fixed_env 398*760c253cSXin Li ) 399*760c253cSXin Li if exit_code == 0: 400*760c253cSXin Li return CheckResult( 401*760c253cSXin Li ok=True, 402*760c253cSXin Li output=f"{output}{prefix}checks passed", 403*760c253cSXin Li autofix_commands=[], 404*760c253cSXin Li ) 405*760c253cSXin Li else: 406*760c253cSXin Li return CheckResult( 407*760c253cSXin Li ok=False, 408*760c253cSXin Li output=f"{output}{prefix}type errors were found", 409*760c253cSXin Li autofix_commands=[], 410*760c253cSXin Li ) 411*760c253cSXin Li 412*760c253cSXin Li 413*760c253cSXin Lidef check_python_file_headers(python_files: Iterable[str]) -> CheckResult: 414*760c253cSXin Li """Subchecker of check_py_format. Checks python #!s""" 415*760c253cSXin Li add_hashbang = [] 416*760c253cSXin Li remove_hashbang = [] 417*760c253cSXin Li 418*760c253cSXin Li for python_file in python_files: 419*760c253cSXin Li needs_hashbang = is_file_executable(python_file) 420*760c253cSXin Li with open(python_file, encoding="utf-8") as f: 421*760c253cSXin Li has_hashbang = f.read(2) == "#!" 422*760c253cSXin Li if needs_hashbang == has_hashbang: 423*760c253cSXin Li continue 424*760c253cSXin Li 425*760c253cSXin Li if needs_hashbang: 426*760c253cSXin Li add_hashbang.append(python_file) 427*760c253cSXin Li else: 428*760c253cSXin Li remove_hashbang.append(python_file) 429*760c253cSXin Li 430*760c253cSXin Li autofix = [] 431*760c253cSXin Li output = [] 432*760c253cSXin Li if add_hashbang: 433*760c253cSXin Li output.append( 434*760c253cSXin Li "The following files have no #!, but need one: %s" % add_hashbang 435*760c253cSXin Li ) 436*760c253cSXin Li autofix.append(["sed", "-i", "1i#!/usr/bin/env python3"] + add_hashbang) 437*760c253cSXin Li 438*760c253cSXin Li if remove_hashbang: 439*760c253cSXin Li output.append( 440*760c253cSXin Li "The following files have a #!, but shouldn't: %s" % remove_hashbang 441*760c253cSXin Li ) 442*760c253cSXin Li autofix.append(["sed", "-i", "1d"] + remove_hashbang) 443*760c253cSXin Li 444*760c253cSXin Li if not output: 445*760c253cSXin Li return CheckResult( 446*760c253cSXin Li ok=True, 447*760c253cSXin Li output="", 448*760c253cSXin Li autofix_commands=[], 449*760c253cSXin Li ) 450*760c253cSXin Li return CheckResult( 451*760c253cSXin Li ok=False, 452*760c253cSXin Li output="\n".join(output), 453*760c253cSXin Li autofix_commands=autofix, 454*760c253cSXin Li ) 455*760c253cSXin Li 456*760c253cSXin Li 457*760c253cSXin Lidef check_py_format( 458*760c253cSXin Li toolchain_utils_root: str, 459*760c253cSXin Li thread_pool: multiprocessing.pool.ThreadPool, 460*760c253cSXin Li files: Iterable[str], 461*760c253cSXin Li) -> CheckResults: 462*760c253cSXin Li """Runs black on files to check for style bugs. Also checks for #!s.""" 463*760c253cSXin Li black = "black" 464*760c253cSXin Li if not has_executable_on_path(black): 465*760c253cSXin Li return CheckResult( 466*760c253cSXin Li ok=False, 467*760c253cSXin Li output="black isn't available on your $PATH. Please either " 468*760c253cSXin Li "enter a chroot, or place depot_tools on your $PATH.", 469*760c253cSXin Li autofix_commands=[], 470*760c253cSXin Li ) 471*760c253cSXin Li 472*760c253cSXin Li python_files = [f for f in remove_deleted_files(files) if f.endswith(".py")] 473*760c253cSXin Li if not python_files: 474*760c253cSXin Li return CheckResult( 475*760c253cSXin Li ok=True, 476*760c253cSXin Li output="no python files to check", 477*760c253cSXin Li autofix_commands=[], 478*760c253cSXin Li ) 479*760c253cSXin Li 480*760c253cSXin Li tasks = [ 481*760c253cSXin Li ( 482*760c253cSXin Li "check_black", 483*760c253cSXin Li thread_pool.apply_async( 484*760c253cSXin Li check_black, (toolchain_utils_root, black, python_files) 485*760c253cSXin Li ), 486*760c253cSXin Li ), 487*760c253cSXin Li ( 488*760c253cSXin Li "check_isort", 489*760c253cSXin Li thread_pool.apply_async( 490*760c253cSXin Li check_isort, (toolchain_utils_root, python_files) 491*760c253cSXin Li ), 492*760c253cSXin Li ), 493*760c253cSXin Li ( 494*760c253cSXin Li "check_file_headers", 495*760c253cSXin Li thread_pool.apply_async(check_python_file_headers, (python_files,)), 496*760c253cSXin Li ), 497*760c253cSXin Li ] 498*760c253cSXin Li return [(name, get_check_result_or_catch(task)) for name, task in tasks] 499*760c253cSXin Li 500*760c253cSXin Li 501*760c253cSXin Lidef file_is_relative_to(file: Path, potential_parent: Path) -> bool: 502*760c253cSXin Li """file.is_relative_to(potential_parent), but for Python < 3.9.""" 503*760c253cSXin Li try: 504*760c253cSXin Li file.relative_to(potential_parent) 505*760c253cSXin Li return True 506*760c253cSXin Li except ValueError: 507*760c253cSXin Li return False 508*760c253cSXin Li 509*760c253cSXin Li 510*760c253cSXin Lidef is_file_in_any_of(file: Path, files_and_dirs: List[Path]) -> bool: 511*760c253cSXin Li """Returns whether `files_and_dirs` encompasses `file`. 512*760c253cSXin Li 513*760c253cSXin Li `files_and_dirs` is considered to encompass `file` if `files_and_dirs` 514*760c253cSXin Li contains `file` directly, or if it contains a directory that is a parent of 515*760c253cSXin Li `file`. 516*760c253cSXin Li 517*760c253cSXin Li Args: 518*760c253cSXin Li file: a path to check 519*760c253cSXin Li files_and_dirs: a list of directories to check 520*760c253cSXin Li """ 521*760c253cSXin Li # This could technically be made sublinear, but it's running at most a few 522*760c253cSXin Li # dozen times on a `files_and_dirs` that's currently < 10 elems. 523*760c253cSXin Li return any( 524*760c253cSXin Li file == x or file_is_relative_to(file, x) for x in files_and_dirs 525*760c253cSXin Li ) 526*760c253cSXin Li 527*760c253cSXin Li 528*760c253cSXin Lidef check_py_types( 529*760c253cSXin Li toolchain_utils_root: str, 530*760c253cSXin Li thread_pool: multiprocessing.pool.ThreadPool, 531*760c253cSXin Li files: Iterable[str], 532*760c253cSXin Li) -> CheckResults: 533*760c253cSXin Li """Runs static type checking for files in MYPY_CHECKED_FILES.""" 534*760c253cSXin Li path_root = Path(toolchain_utils_root) 535*760c253cSXin Li check_locations = [path_root / x for x in MYPY_CHECKED_PATHS] 536*760c253cSXin Li to_check = [ 537*760c253cSXin Li x 538*760c253cSXin Li for x in files 539*760c253cSXin Li if x.endswith(".py") and is_file_in_any_of(Path(x), check_locations) 540*760c253cSXin Li ] 541*760c253cSXin Li 542*760c253cSXin Li if not to_check: 543*760c253cSXin Li return CheckResult( 544*760c253cSXin Li ok=True, 545*760c253cSXin Li output="no python files to typecheck", 546*760c253cSXin Li autofix_commands=[], 547*760c253cSXin Li ) 548*760c253cSXin Li 549*760c253cSXin Li mypy = get_mypy() 550*760c253cSXin Li if not mypy: 551*760c253cSXin Li return CheckResult( 552*760c253cSXin Li ok=False, 553*760c253cSXin Li output="mypy not found. Please either enter a chroot " 554*760c253cSXin Li "or install mypy", 555*760c253cSXin Li autofix_commands=[], 556*760c253cSXin Li ) 557*760c253cSXin Li 558*760c253cSXin Li tasks = [ 559*760c253cSXin Li ( 560*760c253cSXin Li "check_mypy", 561*760c253cSXin Li thread_pool.apply_async( 562*760c253cSXin Li check_mypy, (toolchain_utils_root, mypy, to_check) 563*760c253cSXin Li ), 564*760c253cSXin Li ), 565*760c253cSXin Li ] 566*760c253cSXin Li return [(name, get_check_result_or_catch(task)) for name, task in tasks] 567*760c253cSXin Li 568*760c253cSXin Li 569*760c253cSXin Lidef find_chromeos_root_directory() -> Optional[str]: 570*760c253cSXin Li return os.getenv("CHROMEOS_ROOT_DIRECTORY") 571*760c253cSXin Li 572*760c253cSXin Li 573*760c253cSXin Lidef check_cros_lint( 574*760c253cSXin Li toolchain_utils_root: str, 575*760c253cSXin Li thread_pool: multiprocessing.pool.ThreadPool, 576*760c253cSXin Li files: Iterable[str], 577*760c253cSXin Li) -> CheckResults: 578*760c253cSXin Li """Runs `cros lint`""" 579*760c253cSXin Li 580*760c253cSXin Li fixed_env = env_with_pythonpath(toolchain_utils_root) 581*760c253cSXin Li 582*760c253cSXin Li # We have to support users who don't have a chroot. So we either run `cros 583*760c253cSXin Li # lint` (if it's been made available to us), or we try a mix of 584*760c253cSXin Li # pylint+golint. 585*760c253cSXin Li def try_run_cros_lint(cros_binary: str) -> Optional[CheckResult]: 586*760c253cSXin Li exit_code, output = run_command_unchecked( 587*760c253cSXin Li [cros_binary, "lint", "--"] + list(files), 588*760c253cSXin Li toolchain_utils_root, 589*760c253cSXin Li env=fixed_env, 590*760c253cSXin Li ) 591*760c253cSXin Li 592*760c253cSXin Li # This is returned specifically if cros couldn't find the ChromeOS tree 593*760c253cSXin Li # root. 594*760c253cSXin Li if exit_code == 127: 595*760c253cSXin Li return None 596*760c253cSXin Li 597*760c253cSXin Li return CheckResult( 598*760c253cSXin Li ok=exit_code == 0, 599*760c253cSXin Li output=output, 600*760c253cSXin Li autofix_commands=[], 601*760c253cSXin Li ) 602*760c253cSXin Li 603*760c253cSXin Li cros_lint = try_run_cros_lint("cros") 604*760c253cSXin Li if cros_lint is not None: 605*760c253cSXin Li return cros_lint 606*760c253cSXin Li 607*760c253cSXin Li cros_root = find_chromeos_root_directory() 608*760c253cSXin Li if cros_root: 609*760c253cSXin Li cros_lint = try_run_cros_lint( 610*760c253cSXin Li os.path.join(cros_root, "chromite/bin/cros") 611*760c253cSXin Li ) 612*760c253cSXin Li if cros_lint is not None: 613*760c253cSXin Li return cros_lint 614*760c253cSXin Li 615*760c253cSXin Li tasks = [] 616*760c253cSXin Li 617*760c253cSXin Li def check_result_from_command(command: List[str]) -> CheckResult: 618*760c253cSXin Li exit_code, output = run_command_unchecked( 619*760c253cSXin Li command, toolchain_utils_root, env=fixed_env 620*760c253cSXin Li ) 621*760c253cSXin Li return CheckResult( 622*760c253cSXin Li ok=exit_code == 0, 623*760c253cSXin Li output=output, 624*760c253cSXin Li autofix_commands=[], 625*760c253cSXin Li ) 626*760c253cSXin Li 627*760c253cSXin Li python_files = [f for f in remove_deleted_files(files) if f.endswith(".py")] 628*760c253cSXin Li if python_files: 629*760c253cSXin Li 630*760c253cSXin Li def run_pylint() -> CheckResult: 631*760c253cSXin Li # pylint is required. Fail hard if it DNE. 632*760c253cSXin Li return check_result_from_command(["pylint"] + python_files) 633*760c253cSXin Li 634*760c253cSXin Li tasks.append(("pylint", thread_pool.apply_async(run_pylint))) 635*760c253cSXin Li 636*760c253cSXin Li go_files = [f for f in remove_deleted_files(files) if f.endswith(".go")] 637*760c253cSXin Li if go_files: 638*760c253cSXin Li 639*760c253cSXin Li def run_golint() -> CheckResult: 640*760c253cSXin Li if has_executable_on_path("golint"): 641*760c253cSXin Li return check_result_from_command( 642*760c253cSXin Li ["golint", "-set_exit_status"] + go_files 643*760c253cSXin Li ) 644*760c253cSXin Li 645*760c253cSXin Li complaint = ( 646*760c253cSXin Li "WARNING: go linting disabled. golint is not on your $PATH.\n" 647*760c253cSXin Li "Please either enter a chroot, or install go locally. " 648*760c253cSXin Li "Continuing." 649*760c253cSXin Li ) 650*760c253cSXin Li return CheckResult( 651*760c253cSXin Li ok=True, 652*760c253cSXin Li output=complaint, 653*760c253cSXin Li autofix_commands=[], 654*760c253cSXin Li ) 655*760c253cSXin Li 656*760c253cSXin Li tasks.append(("golint", thread_pool.apply_async(run_golint))) 657*760c253cSXin Li 658*760c253cSXin Li complaint = ( 659*760c253cSXin Li "WARNING: No ChromeOS checkout detected, and no viable CrOS tree\n" 660*760c253cSXin Li "found; falling back to linting only python and go. If you have a\n" 661*760c253cSXin Li "ChromeOS checkout, please either develop from inside of the source\n" 662*760c253cSXin Li "tree, or set $CHROMEOS_ROOT_DIRECTORY to the root of it." 663*760c253cSXin Li ) 664*760c253cSXin Li 665*760c253cSXin Li results = [(name, get_check_result_or_catch(task)) for name, task in tasks] 666*760c253cSXin Li if not results: 667*760c253cSXin Li return CheckResult( 668*760c253cSXin Li ok=True, 669*760c253cSXin Li output=complaint, 670*760c253cSXin Li autofix_commands=[], 671*760c253cSXin Li ) 672*760c253cSXin Li 673*760c253cSXin Li # We need to complain _somewhere_. 674*760c253cSXin Li name, angry_result = results[0] 675*760c253cSXin Li angry_complaint = (complaint + "\n\n" + angry_result.output).strip() 676*760c253cSXin Li results[0] = (name, angry_result._replace(output=angry_complaint)) 677*760c253cSXin Li return results 678*760c253cSXin Li 679*760c253cSXin Li 680*760c253cSXin Lidef check_go_format(toolchain_utils_root, _thread_pool, files): 681*760c253cSXin Li """Runs gofmt on files to check for style bugs.""" 682*760c253cSXin Li gofmt = "gofmt" 683*760c253cSXin Li if not has_executable_on_path(gofmt): 684*760c253cSXin Li return CheckResult( 685*760c253cSXin Li ok=False, 686*760c253cSXin Li output="gofmt isn't available on your $PATH. Please either " 687*760c253cSXin Li "enter a chroot, or place your go bin/ directory on your $PATH.", 688*760c253cSXin Li autofix_commands=[], 689*760c253cSXin Li ) 690*760c253cSXin Li 691*760c253cSXin Li go_files = [f for f in remove_deleted_files(files) if f.endswith(".go")] 692*760c253cSXin Li if not go_files: 693*760c253cSXin Li return CheckResult( 694*760c253cSXin Li ok=True, 695*760c253cSXin Li output="no go files to check", 696*760c253cSXin Li autofix_commands=[], 697*760c253cSXin Li ) 698*760c253cSXin Li 699*760c253cSXin Li command = [gofmt, "-l"] + go_files 700*760c253cSXin Li exit_code, output = run_command_unchecked(command, cwd=toolchain_utils_root) 701*760c253cSXin Li 702*760c253cSXin Li if exit_code: 703*760c253cSXin Li return CheckResult( 704*760c253cSXin Li ok=False, 705*760c253cSXin Li output=f"{shlex.join(command)} failed; stdout/stderr:\n{output}", 706*760c253cSXin Li autofix_commands=[], 707*760c253cSXin Li ) 708*760c253cSXin Li 709*760c253cSXin Li output = output.strip() 710*760c253cSXin Li if not output: 711*760c253cSXin Li return CheckResult( 712*760c253cSXin Li ok=True, 713*760c253cSXin Li output="", 714*760c253cSXin Li autofix_commands=[], 715*760c253cSXin Li ) 716*760c253cSXin Li 717*760c253cSXin Li broken_files = [x.strip() for x in output.splitlines()] 718*760c253cSXin Li autofix = [gofmt, "-w"] + broken_files 719*760c253cSXin Li return CheckResult( 720*760c253cSXin Li ok=False, 721*760c253cSXin Li output="The following Go files have incorrect " 722*760c253cSXin Li "formatting: %s" % broken_files, 723*760c253cSXin Li autofix_commands=[autofix], 724*760c253cSXin Li ) 725*760c253cSXin Li 726*760c253cSXin Li 727*760c253cSXin Lidef check_no_compiler_wrapper_changes( 728*760c253cSXin Li toolchain_utils_root: str, 729*760c253cSXin Li _thread_pool: multiprocessing.pool.ThreadPool, 730*760c253cSXin Li files: List[str], 731*760c253cSXin Li) -> CheckResult: 732*760c253cSXin Li compiler_wrapper_prefix = ( 733*760c253cSXin Li os.path.join(toolchain_utils_root, "compiler_wrapper") + "/" 734*760c253cSXin Li ) 735*760c253cSXin Li if not any(x.startswith(compiler_wrapper_prefix) for x in files): 736*760c253cSXin Li return CheckResult( 737*760c253cSXin Li ok=True, 738*760c253cSXin Li output="no compiler_wrapper changes detected", 739*760c253cSXin Li autofix_commands=[], 740*760c253cSXin Li ) 741*760c253cSXin Li 742*760c253cSXin Li return CheckResult( 743*760c253cSXin Li ok=False, 744*760c253cSXin Li autofix_commands=[], 745*760c253cSXin Li output=textwrap.dedent( 746*760c253cSXin Li """\ 747*760c253cSXin Li Compiler wrapper changes should be made in chromiumos-overlay. 748*760c253cSXin Li If you're a CrOS toolchain maintainer, please make the change 749*760c253cSXin Li directly there now. If you're contributing as part of a downstream 750*760c253cSXin Li (e.g., the Android toolchain team), feel free to bypass this check 751*760c253cSXin Li and note to your reviewer that you received this message. They can 752*760c253cSXin Li review your CL and commit to the right plate for you. Thanks! 753*760c253cSXin Li """ 754*760c253cSXin Li ).strip(), 755*760c253cSXin Li ) 756*760c253cSXin Li 757*760c253cSXin Li 758*760c253cSXin Lidef check_tests( 759*760c253cSXin Li toolchain_utils_root: str, 760*760c253cSXin Li _thread_pool: multiprocessing.pool.ThreadPool, 761*760c253cSXin Li files: List[str], 762*760c253cSXin Li) -> CheckResult: 763*760c253cSXin Li """Runs tests.""" 764*760c253cSXin Li exit_code, stdout_and_stderr = run_command_unchecked( 765*760c253cSXin Li [os.path.join(toolchain_utils_root, "run_tests_for.py"), "--"] + files, 766*760c253cSXin Li toolchain_utils_root, 767*760c253cSXin Li ) 768*760c253cSXin Li return CheckResult( 769*760c253cSXin Li ok=exit_code == 0, 770*760c253cSXin Li output=stdout_and_stderr, 771*760c253cSXin Li autofix_commands=[], 772*760c253cSXin Li ) 773*760c253cSXin Li 774*760c253cSXin Li 775*760c253cSXin Lidef detect_toolchain_utils_root() -> str: 776*760c253cSXin Li return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 777*760c253cSXin Li 778*760c253cSXin Li 779*760c253cSXin Lidef process_check_result( 780*760c253cSXin Li check_name: str, 781*760c253cSXin Li check_results: CheckResults, 782*760c253cSXin Li start_time: datetime.datetime, 783*760c253cSXin Li) -> Tuple[bool, List[List[str]]]: 784*760c253cSXin Li """Prints human-readable output for the given check_results.""" 785*760c253cSXin Li indent = " " 786*760c253cSXin Li 787*760c253cSXin Li def indent_block(text: str) -> str: 788*760c253cSXin Li return indent + text.replace("\n", "\n" + indent) 789*760c253cSXin Li 790*760c253cSXin Li if isinstance(check_results, CheckResult): 791*760c253cSXin Li ok, output, autofix_commands = check_results 792*760c253cSXin Li if not ok and autofix_commands: 793*760c253cSXin Li recommendation = ( 794*760c253cSXin Li "Recommended command(s) to fix this: " 795*760c253cSXin Li f"{[shlex.join(x) for x in autofix_commands]}" 796*760c253cSXin Li ) 797*760c253cSXin Li if output: 798*760c253cSXin Li output += "\n" + recommendation 799*760c253cSXin Li else: 800*760c253cSXin Li output = recommendation 801*760c253cSXin Li else: 802*760c253cSXin Li output_pieces = [] 803*760c253cSXin Li autofix_commands = [] 804*760c253cSXin Li for subname, (ok, output, autofix) in check_results: 805*760c253cSXin Li status = "succeeded" if ok else "failed" 806*760c253cSXin Li message = ["*** %s.%s %s" % (check_name, subname, status)] 807*760c253cSXin Li if output: 808*760c253cSXin Li message.append(indent_block(output)) 809*760c253cSXin Li if not ok and autofix: 810*760c253cSXin Li message.append( 811*760c253cSXin Li indent_block( 812*760c253cSXin Li "Recommended command(s) to fix this: " 813*760c253cSXin Li f"{[shlex.join(x) for x in autofix]}" 814*760c253cSXin Li ) 815*760c253cSXin Li ) 816*760c253cSXin Li 817*760c253cSXin Li output_pieces.append("\n".join(message)) 818*760c253cSXin Li autofix_commands += autofix 819*760c253cSXin Li 820*760c253cSXin Li ok = all(x.ok for _, x in check_results) 821*760c253cSXin Li output = "\n\n".join(output_pieces) 822*760c253cSXin Li 823*760c253cSXin Li time_taken = datetime.datetime.now() - start_time 824*760c253cSXin Li if ok: 825*760c253cSXin Li print("*** %s succeeded after %s" % (check_name, time_taken)) 826*760c253cSXin Li else: 827*760c253cSXin Li print("*** %s failed after %s" % (check_name, time_taken)) 828*760c253cSXin Li 829*760c253cSXin Li if output: 830*760c253cSXin Li print(indent_block(output)) 831*760c253cSXin Li 832*760c253cSXin Li print() 833*760c253cSXin Li return ok, autofix_commands 834*760c253cSXin Li 835*760c253cSXin Li 836*760c253cSXin Lidef try_autofix( 837*760c253cSXin Li all_autofix_commands: List[List[str]], toolchain_utils_root: str 838*760c253cSXin Li) -> None: 839*760c253cSXin Li """Tries to run all given autofix commands, if appropriate.""" 840*760c253cSXin Li if not all_autofix_commands: 841*760c253cSXin Li return 842*760c253cSXin Li 843*760c253cSXin Li exit_code, output = run_command_unchecked( 844*760c253cSXin Li ["git", "status", "--porcelain"], cwd=toolchain_utils_root 845*760c253cSXin Li ) 846*760c253cSXin Li if exit_code != 0: 847*760c253cSXin Li print("Autofix aborted: couldn't get toolchain-utils git status.") 848*760c253cSXin Li return 849*760c253cSXin Li 850*760c253cSXin Li if output.strip(): 851*760c253cSXin Li # A clean repo makes checking/undoing autofix commands trivial. A dirty 852*760c253cSXin Li # one... less so. :) 853*760c253cSXin Li print("Git repo seems dirty; skipping autofix.") 854*760c253cSXin Li return 855*760c253cSXin Li 856*760c253cSXin Li anything_succeeded = False 857*760c253cSXin Li for command in all_autofix_commands: 858*760c253cSXin Li exit_code, output = run_command_unchecked( 859*760c253cSXin Li command, cwd=toolchain_utils_root 860*760c253cSXin Li ) 861*760c253cSXin Li 862*760c253cSXin Li if exit_code: 863*760c253cSXin Li print( 864*760c253cSXin Li f"*** Autofix command `{shlex.join(command)}` exited with " 865*760c253cSXin Li f"code {exit_code}; stdout/stderr:" 866*760c253cSXin Li ) 867*760c253cSXin Li print(output) 868*760c253cSXin Li else: 869*760c253cSXin Li print(f"*** Autofix `{shlex.join(command)}` succeeded") 870*760c253cSXin Li anything_succeeded = True 871*760c253cSXin Li 872*760c253cSXin Li if anything_succeeded: 873*760c253cSXin Li print( 874*760c253cSXin Li "NOTE: Autofixes have been applied. Please check your tree, since " 875*760c253cSXin Li "some lints may now be fixed" 876*760c253cSXin Li ) 877*760c253cSXin Li 878*760c253cSXin Li 879*760c253cSXin Lidef find_repo_root(base_dir: str) -> Optional[str]: 880*760c253cSXin Li current = base_dir 881*760c253cSXin Li while current != "/": 882*760c253cSXin Li if os.path.isdir(os.path.join(current, ".repo")): 883*760c253cSXin Li return current 884*760c253cSXin Li current = os.path.dirname(current) 885*760c253cSXin Li return None 886*760c253cSXin Li 887*760c253cSXin Li 888*760c253cSXin Lidef is_in_chroot() -> bool: 889*760c253cSXin Li return os.path.exists("/etc/cros_chroot_version") 890*760c253cSXin Li 891*760c253cSXin Li 892*760c253cSXin Lidef maybe_reexec_inside_chroot( 893*760c253cSXin Li autofix: bool, install_deps_only: bool, files: List[str] 894*760c253cSXin Li) -> None: 895*760c253cSXin Li if is_in_chroot(): 896*760c253cSXin Li return 897*760c253cSXin Li 898*760c253cSXin Li enter_chroot = True 899*760c253cSXin Li chdir_to = None 900*760c253cSXin Li toolchain_utils = detect_toolchain_utils_root() 901*760c253cSXin Li if find_repo_root(toolchain_utils) is None: 902*760c253cSXin Li chromeos_root_dir = find_chromeos_root_directory() 903*760c253cSXin Li if chromeos_root_dir is None: 904*760c253cSXin Li print( 905*760c253cSXin Li "Standalone toolchain-utils checkout detected; cannot enter " 906*760c253cSXin Li "chroot." 907*760c253cSXin Li ) 908*760c253cSXin Li enter_chroot = False 909*760c253cSXin Li else: 910*760c253cSXin Li chdir_to = chromeos_root_dir 911*760c253cSXin Li 912*760c253cSXin Li if not has_executable_on_path("cros_sdk"): 913*760c253cSXin Li print("No `cros_sdk` detected on $PATH; cannot enter chroot.") 914*760c253cSXin Li enter_chroot = False 915*760c253cSXin Li 916*760c253cSXin Li if not enter_chroot: 917*760c253cSXin Li print( 918*760c253cSXin Li "Giving up on entering the chroot; be warned that some presubmits " 919*760c253cSXin Li "may be broken." 920*760c253cSXin Li ) 921*760c253cSXin Li return 922*760c253cSXin Li 923*760c253cSXin Li # We'll be changing ${PWD}, so make everything relative to toolchain-utils, 924*760c253cSXin Li # which resides at a well-known place inside of the chroot. 925*760c253cSXin Li chroot_toolchain_utils = "/mnt/host/source/src/third_party/toolchain-utils" 926*760c253cSXin Li 927*760c253cSXin Li def rebase_path(path: str) -> str: 928*760c253cSXin Li return os.path.join( 929*760c253cSXin Li chroot_toolchain_utils, os.path.relpath(path, toolchain_utils) 930*760c253cSXin Li ) 931*760c253cSXin Li 932*760c253cSXin Li args = [ 933*760c253cSXin Li "cros_sdk", 934*760c253cSXin Li "--enter", 935*760c253cSXin Li "--", 936*760c253cSXin Li rebase_path(__file__), 937*760c253cSXin Li ] 938*760c253cSXin Li 939*760c253cSXin Li if not autofix: 940*760c253cSXin Li args.append("--no_autofix") 941*760c253cSXin Li if install_deps_only: 942*760c253cSXin Li args.append("--install_deps_only") 943*760c253cSXin Li args.extend(rebase_path(x) for x in files) 944*760c253cSXin Li 945*760c253cSXin Li if chdir_to is None: 946*760c253cSXin Li print("Attempting to enter the chroot...") 947*760c253cSXin Li else: 948*760c253cSXin Li print(f"Attempting to enter the chroot for tree at {chdir_to}...") 949*760c253cSXin Li os.chdir(chdir_to) 950*760c253cSXin Li os.execvp(args[0], args) 951*760c253cSXin Li 952*760c253cSXin Li 953*760c253cSXin Lidef can_import_py_module(module: str) -> bool: 954*760c253cSXin Li """Returns true if `import {module}` works.""" 955*760c253cSXin Li exit_code = subprocess.call( 956*760c253cSXin Li ["python3", "-c", f"import {module}"], 957*760c253cSXin Li stdout=subprocess.DEVNULL, 958*760c253cSXin Li stderr=subprocess.DEVNULL, 959*760c253cSXin Li ) 960*760c253cSXin Li return exit_code == 0 961*760c253cSXin Li 962*760c253cSXin Li 963*760c253cSXin Lidef ensure_pip_deps_installed() -> None: 964*760c253cSXin Li if not PIP_DEPENDENCIES: 965*760c253cSXin Li # No need to install pip if we don't have any deps. 966*760c253cSXin Li return 967*760c253cSXin Li 968*760c253cSXin Li pip = get_pip() 969*760c253cSXin Li assert pip, "pip not found and could not be installed" 970*760c253cSXin Li 971*760c253cSXin Li for package in PIP_DEPENDENCIES: 972*760c253cSXin Li subprocess.check_call(pip + ["install", "--user", package]) 973*760c253cSXin Li 974*760c253cSXin Li 975*760c253cSXin Lidef main(argv: List[str]) -> int: 976*760c253cSXin Li parser = argparse.ArgumentParser(description=__doc__) 977*760c253cSXin Li parser.add_argument( 978*760c253cSXin Li "--no_autofix", 979*760c253cSXin Li dest="autofix", 980*760c253cSXin Li action="store_false", 981*760c253cSXin Li help="Don't run any autofix commands.", 982*760c253cSXin Li ) 983*760c253cSXin Li parser.add_argument( 984*760c253cSXin Li "--no_enter_chroot", 985*760c253cSXin Li dest="enter_chroot", 986*760c253cSXin Li action="store_false", 987*760c253cSXin Li help="Prevent auto-entering the chroot if we're not already in it.", 988*760c253cSXin Li ) 989*760c253cSXin Li parser.add_argument( 990*760c253cSXin Li "--install_deps_only", 991*760c253cSXin Li action="store_true", 992*760c253cSXin Li help=""" 993*760c253cSXin Li Only install dependencies that would be required if presubmits were 994*760c253cSXin Li being run, and quit. This skips all actual checking. 995*760c253cSXin Li """, 996*760c253cSXin Li ) 997*760c253cSXin Li parser.add_argument("files", nargs="*") 998*760c253cSXin Li opts = parser.parse_args(argv) 999*760c253cSXin Li 1000*760c253cSXin Li files = opts.files 1001*760c253cSXin Li install_deps_only = opts.install_deps_only 1002*760c253cSXin Li if not files and not install_deps_only: 1003*760c253cSXin Li return 0 1004*760c253cSXin Li 1005*760c253cSXin Li if opts.enter_chroot: 1006*760c253cSXin Li maybe_reexec_inside_chroot(opts.autofix, install_deps_only, files) 1007*760c253cSXin Li 1008*760c253cSXin Li # If you ask for --no_enter_chroot, you're on your own for installing these 1009*760c253cSXin Li # things. 1010*760c253cSXin Li if is_in_chroot(): 1011*760c253cSXin Li ensure_pip_deps_installed() 1012*760c253cSXin Li if install_deps_only: 1013*760c253cSXin Li print( 1014*760c253cSXin Li "Dependency installation complete & --install_deps_only " 1015*760c253cSXin Li "passed. Quit." 1016*760c253cSXin Li ) 1017*760c253cSXin Li return 0 1018*760c253cSXin Li elif install_deps_only: 1019*760c253cSXin Li parser.error( 1020*760c253cSXin Li "--install_deps_only is meaningless if the chroot isn't entered" 1021*760c253cSXin Li ) 1022*760c253cSXin Li 1023*760c253cSXin Li files = [os.path.abspath(f) for f in files] 1024*760c253cSXin Li 1025*760c253cSXin Li # Note that we extract .__name__s from these, so please name them in a 1026*760c253cSXin Li # user-friendly way. 1027*760c253cSXin Li checks = ( 1028*760c253cSXin Li check_cros_lint, 1029*760c253cSXin Li check_py_format, 1030*760c253cSXin Li check_py_types, 1031*760c253cSXin Li check_go_format, 1032*760c253cSXin Li check_tests, 1033*760c253cSXin Li check_no_compiler_wrapper_changes, 1034*760c253cSXin Li ) 1035*760c253cSXin Li 1036*760c253cSXin Li toolchain_utils_root = detect_toolchain_utils_root() 1037*760c253cSXin Li 1038*760c253cSXin Li # NOTE: As mentioned above, checks can block on threads they spawn in this 1039*760c253cSXin Li # pool, so we need at least len(checks)+1 threads to avoid deadlock. Use *2 1040*760c253cSXin Li # so all checks can make progress at a decent rate. 1041*760c253cSXin Li num_threads = max(multiprocessing.cpu_count(), len(checks) * 2) 1042*760c253cSXin Li start_time = datetime.datetime.now() 1043*760c253cSXin Li 1044*760c253cSXin Li # For our single print statement... 1045*760c253cSXin Li spawn_print_lock = threading.RLock() 1046*760c253cSXin Li 1047*760c253cSXin Li def run_check(check_fn): 1048*760c253cSXin Li name = check_fn.__name__ 1049*760c253cSXin Li with spawn_print_lock: 1050*760c253cSXin Li print("*** Spawning %s" % name) 1051*760c253cSXin Li return name, check_fn(toolchain_utils_root, pool, files) 1052*760c253cSXin Li 1053*760c253cSXin Li with multiprocessing.pool.ThreadPool(num_threads) as pool: 1054*760c253cSXin Li all_checks_ok = True 1055*760c253cSXin Li all_autofix_commands = [] 1056*760c253cSXin Li for check_name, result in pool.imap_unordered(run_check, checks): 1057*760c253cSXin Li ok, autofix_commands = process_check_result( 1058*760c253cSXin Li check_name, result, start_time 1059*760c253cSXin Li ) 1060*760c253cSXin Li all_checks_ok = ok and all_checks_ok 1061*760c253cSXin Li all_autofix_commands += autofix_commands 1062*760c253cSXin Li 1063*760c253cSXin Li # Run these after everything settles, so: 1064*760c253cSXin Li # - we don't collide with checkers that are running concurrently 1065*760c253cSXin Li # - we clearly print out everything that went wrong ahead of time, in case 1066*760c253cSXin Li # any of these fail 1067*760c253cSXin Li if opts.autofix: 1068*760c253cSXin Li try_autofix(all_autofix_commands, toolchain_utils_root) 1069*760c253cSXin Li 1070*760c253cSXin Li if not all_checks_ok: 1071*760c253cSXin Li return 1 1072*760c253cSXin Li return 0 1073*760c253cSXin Li 1074*760c253cSXin Li 1075*760c253cSXin Liif __name__ == "__main__": 1076*760c253cSXin Li sys.exit(main(sys.argv[1:])) 1077