blob: 106bb07d7d51bce36f25420657ca5059e707bd91 [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2022 The ChromiumOS Authors
Ryan Beltran1f2dd082022-04-25 18:42:32 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""This script emerges packages and retrieves their lints.
6
7Currently support is provided for both general and differential linting of C++
8with Clang Tidy and Rust with Cargo Clippy for all packages within platform2.
9"""
10
11import json
Ryan Beltran378934c2022-11-23 00:44:26 +000012import os
13from pathlib import Path
Ryan Beltran1f2dd082022-04-25 18:42:32 +000014import sys
Ryan Beltrance85d0f2022-08-09 21:36:39 +000015from typing import List, Text
Ryan Beltran1f2dd082022-04-25 18:42:32 +000016
17from chromite.lib import build_target_lib
18from chromite.lib import commandline
19from chromite.lib import cros_build_lib
Ryan Beltranb2175862022-04-28 19:55:57 +000020from chromite.lib import portage_util
Ryan Beltrance85d0f2022-08-09 21:36:39 +000021from chromite.lib import terminal
Ryan Beltran5514eab2022-04-28 21:40:24 +000022from chromite.lib import workon_helper
Ryan Beltran1f2dd082022-04-25 18:42:32 +000023from chromite.lib.parser import package_info
24from chromite.service import toolchain
25from chromite.utils import file_util
26
27
Alex Klein1699fab2022-09-08 08:46:06 -060028def parse_packages(
29 build_target: build_target_lib.BuildTarget, packages: List[str]
30) -> List[package_info.PackageInfo]:
31 """Parse packages and insert the category if none is given.
Ryan Beltranb2175862022-04-28 19:55:57 +000032
Alex Klein1699fab2022-09-08 08:46:06 -060033 Args:
Alex Klein68b270c2023-04-14 14:42:50 -060034 build_target: build_target to find ebuild for
35 packages: user input package names to parse
Ryan Beltranb2175862022-04-28 19:55:57 +000036
Alex Klein1699fab2022-09-08 08:46:06 -060037 Returns:
Alex Klein68b270c2023-04-14 14:42:50 -060038 A list of parsed PackageInfo objects
Alex Klein1699fab2022-09-08 08:46:06 -060039 """
40 package_infos: List[package_info.PackageInfo] = []
41 for package in packages:
42 parsed = package_info.parse(package)
43 if not parsed.category:
Alex Klein68b270c2023-04-14 14:42:50 -060044 # If a category is not specified, get it from the ebuild path.
Alex Klein1699fab2022-09-08 08:46:06 -060045 if build_target.is_host():
46 ebuild_path = portage_util.FindEbuildForPackage(
47 package, build_target.root
48 )
49 else:
50 ebuild_path = portage_util.FindEbuildForBoardPackage(
51 package, build_target.name, build_target.root
52 )
53 ebuild_data = portage_util.EBuild(ebuild_path)
54 parsed = package_info.parse(ebuild_data.package)
55 package_infos.append(parsed)
56 return package_infos
Ryan Beltranb2175862022-04-28 19:55:57 +000057
58
Ryan Beltrance85d0f2022-08-09 21:36:39 +000059def format_lint(lint: toolchain.LinterFinding) -> Text:
Alex Klein68b270c2023-04-14 14:42:50 -060060 """Formats a lint for human-readable printing.
Ryan Beltrance85d0f2022-08-09 21:36:39 +000061
Alex Klein1699fab2022-09-08 08:46:06 -060062 Example output:
63 [ClangTidy] In 'path/to/file.c' on line 36:
64 Also in 'path/to/file.c' on line 40:
65 Also in 'path/to/file.c' on lines 50-53:
66 You did something bad, don't do it.
Ryan Beltrance85d0f2022-08-09 21:36:39 +000067
Alex Klein1699fab2022-09-08 08:46:06 -060068 Args:
Alex Klein68b270c2023-04-14 14:42:50 -060069 lint: A linter finding from the toolchain service.
Ryan Beltrance85d0f2022-08-09 21:36:39 +000070
Alex Klein1699fab2022-09-08 08:46:06 -060071 Returns:
Alex Klein68b270c2023-04-14 14:42:50 -060072 A correctly formatted string ready to be displayed to the user.
Alex Klein1699fab2022-09-08 08:46:06 -060073 """
Ryan Beltrance85d0f2022-08-09 21:36:39 +000074
Alex Klein1699fab2022-09-08 08:46:06 -060075 color = terminal.Color(True)
76 lines = []
77 linter_prefix = color.Color(
78 terminal.Color.YELLOW,
79 f"[{lint.linter}]",
80 background_color=terminal.Color.BLACK,
81 )
82 for loc in lint.locations:
83 if not lines:
84 location_prefix = f"\n{linter_prefix} In"
85 else:
86 location_prefix = " and in"
87 if loc.line_start != loc.line_end:
88 lines.append(
89 f"{location_prefix} '{loc.filepath}' "
90 f"lines {loc.line_start}-{loc.line_end}:"
91 )
92 else:
93 lines.append(
94 f"{location_prefix} '{loc.filepath}' line {loc.line_start}:"
95 )
96 message_lines = lint.message.split("\n")
97 for line in message_lines:
98 lines.append(f" {line}")
99 lines.append("")
100 return "\n".join(lines)
Ryan Beltrance85d0f2022-08-09 21:36:39 +0000101
102
Ryan Beltrana32a1a12022-09-28 06:03:45 +0000103def json_format_lint(lint: toolchain.LinterFinding) -> Text:
104 """Formats a lint in json for machine parsing.
105
106 Args:
107 lint: A linter finding from the toolchain service.
108
109 Returns:
110 A correctly formatted json string ready to be displayed to the user.
111 """
112
113 def _dictify(original):
114 """Turns namedtuple's to dictionaries recursively."""
115 # Handle namedtuples
116 if isinstance(original, tuple) and hasattr(original, "_asdict"):
117 return _dictify(original._asdict())
118 # Handle collection types
119 elif hasattr(original, "__iter__"):
120 # Handle strings
121 if isinstance(original, (str, bytes)):
122 return original
123 # Handle dictionaries
124 elif isinstance(original, dict):
125 return {k: _dictify(v) for k, v in original.items()}
126 # Handle lists, sets, etc.
127 else:
128 return [_dictify(x) for x in original]
Ryan Beltranc37fb392023-05-11 18:24:40 +0000129 # Handle PackageInfo objects
130 elif isinstance(original, package_info.PackageInfo):
131 return original.atom
Ryan Beltrana32a1a12022-09-28 06:03:45 +0000132 # Handle everything else
133 return original
134
135 return json.dumps(_dictify(lint))
136
137
Ryan Beltran378934c2022-11-23 00:44:26 +0000138def get_all_sysroots() -> List[Text]:
139 """Gets all available sysroots for both host and boards."""
140 host_root = Path(build_target_lib.BuildTarget(None).root)
141 roots = [str(host_root)]
142 build_dir = host_root / "build"
143 for board in os.listdir(build_dir):
144 if board != "bin":
145 board_root = build_dir / board
146 if board_root.is_dir():
147 roots.append(str(board_root))
148 return roots
149
150
Ryan Beltran1f2dd082022-04-25 18:42:32 +0000151def get_arg_parser() -> commandline.ArgumentParser:
Alex Klein1699fab2022-09-08 08:46:06 -0600152 """Creates an argument parser for this script."""
153 default_board = cros_build_lib.GetDefaultBoard()
154 parser = commandline.ArgumentParser(description=__doc__)
Ryan Beltrandbd7b812022-06-08 23:36:16 +0000155
Ryan Beltran378934c2022-11-23 00:44:26 +0000156 board_group = parser.add_mutually_exclusive_group()
Alex Klein1699fab2022-09-08 08:46:06 -0600157 board_group.add_argument(
158 "-b",
159 "--board",
160 "--build-target",
161 dest="board",
162 default=default_board,
163 help="The board to emerge packages for",
164 )
165 board_group.add_argument(
166 "--host", action="store_true", help="emerge for host instead of board."
167 )
Ryan Beltran378934c2022-11-23 00:44:26 +0000168 parser.add_argument(
Alex Klein1699fab2022-09-08 08:46:06 -0600169 "--fetch-only",
170 action="store_true",
Alex Klein68b270c2023-04-14 14:42:50 -0600171 help="Fetch lints from previous run without resetting or calling "
172 "emerge.",
Alex Klein1699fab2022-09-08 08:46:06 -0600173 )
Alex Klein1699fab2022-09-08 08:46:06 -0600174 parser.add_argument(
175 "--differential",
176 action="store_true",
177 help="only lint lines touched by the last commit",
178 )
179 parser.add_argument(
180 "-o",
181 "--output",
182 default=sys.stdout,
183 help="File to use instead of stdout.",
184 )
185 parser.add_argument(
186 "--json", action="store_true", help="Output lints in JSON format."
187 )
188 parser.add_argument(
189 "--no-clippy",
190 dest="clippy",
191 action="store_false",
192 help="Disable cargo clippy linter.",
193 )
194 parser.add_argument(
195 "--no-tidy",
196 dest="tidy",
197 action="store_false",
198 help="Disable clang tidy linter.",
199 )
200 parser.add_argument(
201 "--no-golint",
202 dest="golint",
203 action="store_false",
204 help="Disable golint linter.",
205 )
206 parser.add_argument(
Ryan Beltran378934c2022-11-23 00:44:26 +0000207 "--iwyu",
208 action="store_true",
209 help="Enable include-what-you-use linter.",
210 )
211 parser.add_argument(
Alex Klein1699fab2022-09-08 08:46:06 -0600212 "packages",
213 nargs="*",
214 help="package(s) to emerge and retrieve lints for",
215 )
216 return parser
Ryan Beltran1f2dd082022-04-25 18:42:32 +0000217
218
219def parse_args(argv: List[str]):
Alex Klein1699fab2022-09-08 08:46:06 -0600220 """Parses arguments in argv and returns the options."""
221 parser = get_arg_parser()
222 opts = parser.parse_args(argv)
223 opts.Freeze()
Ryan Beltrandbd7b812022-06-08 23:36:16 +0000224
Alex Klein1699fab2022-09-08 08:46:06 -0600225 # A package must be specified unless we are in fetch-only mode
226 if not (opts.fetch_only or opts.packages):
Ryan Beltran378934c2022-11-23 00:44:26 +0000227 parser.error("Emerge mode requires specified package(s).")
Alex Klein1699fab2022-09-08 08:46:06 -0600228 if opts.fetch_only and opts.packages:
229 parser.error("Cannot specify packages for fetch-only mode.")
Ryan Beltrandbd7b812022-06-08 23:36:16 +0000230
Ryan Beltran378934c2022-11-23 00:44:26 +0000231 # A board must be specified unless we are in fetch-only mode
232 if not (opts.fetch_only or opts.board or opts.host):
233 parser.error("Emerge mode requires either --board or --host.")
234
Alex Klein1699fab2022-09-08 08:46:06 -0600235 return opts
Ryan Beltran1f2dd082022-04-25 18:42:32 +0000236
237
238def main(argv: List[str]) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -0600239 cros_build_lib.AssertInsideChroot()
240 opts = parse_args(argv)
Ryan Beltran1f2dd082022-04-25 18:42:32 +0000241
Alex Klein1699fab2022-09-08 08:46:06 -0600242 if opts.host:
243 # BuildTarget interprets None as host target
244 build_target = build_target_lib.BuildTarget(None)
Ryan Beltrandbd7b812022-06-08 23:36:16 +0000245 else:
Alex Klein1699fab2022-09-08 08:46:06 -0600246 build_target = build_target_lib.BuildTarget(opts.board)
247 packages = parse_packages(build_target, opts.packages)
248 package_atoms = [x.atom for x in packages]
Ryan Beltran1f2dd082022-04-25 18:42:32 +0000249
Alex Klein1699fab2022-09-08 08:46:06 -0600250 with workon_helper.WorkonScope(build_target, package_atoms):
251 build_linter = toolchain.BuildLinter(
252 packages, build_target.root, opts.differential
253 )
254 if opts.fetch_only:
Ryan Beltran378934c2022-11-23 00:44:26 +0000255 if opts.host or opts.board:
256 roots = [build_target.root]
257 else:
258 roots = get_all_sysroots()
259 lints = []
260 for root in roots:
261 build_linter.sysroot = root
262 lints.extend(
263 build_linter.fetch_findings(
264 use_clippy=opts.clippy,
265 use_tidy=opts.tidy,
266 use_golint=opts.golint,
267 use_iwyu=opts.iwyu,
268 )
269 )
Alex Klein1699fab2022-09-08 08:46:06 -0600270 else:
271 lints = build_linter.emerge_with_linting(
272 use_clippy=opts.clippy,
273 use_tidy=opts.tidy,
274 use_golint=opts.golint,
Ryan Beltran378934c2022-11-23 00:44:26 +0000275 use_iwyu=opts.iwyu,
Alex Klein1699fab2022-09-08 08:46:06 -0600276 )
Ryan Beltrance85d0f2022-08-09 21:36:39 +0000277
Alex Klein1699fab2022-09-08 08:46:06 -0600278 if opts.json:
Ryan Beltrana32a1a12022-09-28 06:03:45 +0000279 formatted_output_inner = ",\n".join(json_format_lint(l) for l in lints)
280 formatted_output = f"[{formatted_output_inner}]"
Alex Klein1699fab2022-09-08 08:46:06 -0600281 else:
Ryan Beltrana32a1a12022-09-28 06:03:45 +0000282 formatted_output = "\n".join(format_lint(l) for l in lints)
Alex Klein1699fab2022-09-08 08:46:06 -0600283
284 with file_util.Open(opts.output, "w") as output_file:
285 output_file.write(formatted_output)
286 if not opts.json:
287 output_file.write(f"\nFound {len(lints)} lints.")
288 output_file.write("\n")