blob: baa1333afc0557f62af2aba7b80b50baf2d36598 [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
Ryan Beltran4706f342023-08-29 01:16:22 +000019from chromite.lib import constants
Ryan Beltran1f2dd082022-04-25 18:42:32 +000020from chromite.lib import cros_build_lib
Ryan Beltranb2175862022-04-28 19:55:57 +000021from chromite.lib import portage_util
Ryan Beltrance85d0f2022-08-09 21:36:39 +000022from chromite.lib import terminal
Ryan Beltran5514eab2022-04-28 21:40:24 +000023from chromite.lib import workon_helper
Ryan Beltran1f2dd082022-04-25 18:42:32 +000024from chromite.lib.parser import package_info
25from chromite.service import toolchain
26from chromite.utils import file_util
27
28
Alex Klein1699fab2022-09-08 08:46:06 -060029def parse_packages(
30 build_target: build_target_lib.BuildTarget, packages: List[str]
31) -> List[package_info.PackageInfo]:
32 """Parse packages and insert the category if none is given.
Ryan Beltranb2175862022-04-28 19:55:57 +000033
Alex Klein1699fab2022-09-08 08:46:06 -060034 Args:
Alex Klein68b270c2023-04-14 14:42:50 -060035 build_target: build_target to find ebuild for
36 packages: user input package names to parse
Ryan Beltranb2175862022-04-28 19:55:57 +000037
Alex Klein1699fab2022-09-08 08:46:06 -060038 Returns:
Alex Klein68b270c2023-04-14 14:42:50 -060039 A list of parsed PackageInfo objects
Alex Klein1699fab2022-09-08 08:46:06 -060040 """
41 package_infos: List[package_info.PackageInfo] = []
42 for package in packages:
43 parsed = package_info.parse(package)
44 if not parsed.category:
Alex Klein68b270c2023-04-14 14:42:50 -060045 # If a category is not specified, get it from the ebuild path.
Alex Klein1699fab2022-09-08 08:46:06 -060046 if build_target.is_host():
47 ebuild_path = portage_util.FindEbuildForPackage(
48 package, build_target.root
49 )
50 else:
51 ebuild_path = portage_util.FindEbuildForBoardPackage(
52 package, build_target.name, build_target.root
53 )
54 ebuild_data = portage_util.EBuild(ebuild_path)
55 parsed = package_info.parse(ebuild_data.package)
56 package_infos.append(parsed)
57 return package_infos
Ryan Beltranb2175862022-04-28 19:55:57 +000058
59
Ryan Beltran4706f342023-08-29 01:16:22 +000060def make_relative_to_cros(file_path: str) -> Path:
61 """removes /mnt/host/source from file_paths if present."""
62 path = Path(file_path)
63 try:
64 return path.relative_to(constants.CHROOT_SOURCE_ROOT)
65 except ValueError:
66 return path
67
68
Ryan Beltrance85d0f2022-08-09 21:36:39 +000069def format_lint(lint: toolchain.LinterFinding) -> Text:
Alex Klein68b270c2023-04-14 14:42:50 -060070 """Formats a lint for human-readable printing.
Ryan Beltrance85d0f2022-08-09 21:36:39 +000071
Alex Klein1699fab2022-09-08 08:46:06 -060072 Example output:
73 [ClangTidy] In 'path/to/file.c' on line 36:
74 Also in 'path/to/file.c' on line 40:
75 Also in 'path/to/file.c' on lines 50-53:
76 You did something bad, don't do it.
Ryan Beltrance85d0f2022-08-09 21:36:39 +000077
Alex Klein1699fab2022-09-08 08:46:06 -060078 Args:
Alex Klein68b270c2023-04-14 14:42:50 -060079 lint: A linter finding from the toolchain service.
Ryan Beltrance85d0f2022-08-09 21:36:39 +000080
Alex Klein1699fab2022-09-08 08:46:06 -060081 Returns:
Alex Klein68b270c2023-04-14 14:42:50 -060082 A correctly formatted string ready to be displayed to the user.
Alex Klein1699fab2022-09-08 08:46:06 -060083 """
Ryan Beltrance85d0f2022-08-09 21:36:39 +000084
Alex Klein1699fab2022-09-08 08:46:06 -060085 color = terminal.Color(True)
86 lines = []
87 linter_prefix = color.Color(
88 terminal.Color.YELLOW,
89 f"[{lint.linter}]",
90 background_color=terminal.Color.BLACK,
91 )
92 for loc in lint.locations:
Ryan Beltran4706f342023-08-29 01:16:22 +000093 filepath = make_relative_to_cros(loc.filepath)
Alex Klein1699fab2022-09-08 08:46:06 -060094 if not lines:
95 location_prefix = f"\n{linter_prefix} In"
96 else:
97 location_prefix = " and in"
98 if loc.line_start != loc.line_end:
99 lines.append(
Ryan Beltran4706f342023-08-29 01:16:22 +0000100 f"{location_prefix} '{filepath}' "
Alex Klein1699fab2022-09-08 08:46:06 -0600101 f"lines {loc.line_start}-{loc.line_end}:"
102 )
103 else:
104 lines.append(
Ryan Beltran4706f342023-08-29 01:16:22 +0000105 f"{location_prefix} '{filepath}' line {loc.line_start}:"
Alex Klein1699fab2022-09-08 08:46:06 -0600106 )
107 message_lines = lint.message.split("\n")
108 for line in message_lines:
109 lines.append(f" {line}")
110 lines.append("")
111 return "\n".join(lines)
Ryan Beltrance85d0f2022-08-09 21:36:39 +0000112
113
Ryan Beltrana32a1a12022-09-28 06:03:45 +0000114def json_format_lint(lint: toolchain.LinterFinding) -> Text:
115 """Formats a lint in json for machine parsing.
116
117 Args:
118 lint: A linter finding from the toolchain service.
119
120 Returns:
121 A correctly formatted json string ready to be displayed to the user.
122 """
123
124 def _dictify(original):
125 """Turns namedtuple's to dictionaries recursively."""
126 # Handle namedtuples
127 if isinstance(original, tuple) and hasattr(original, "_asdict"):
128 return _dictify(original._asdict())
129 # Handle collection types
130 elif hasattr(original, "__iter__"):
131 # Handle strings
132 if isinstance(original, (str, bytes)):
133 return original
134 # Handle dictionaries
135 elif isinstance(original, dict):
136 return {k: _dictify(v) for k, v in original.items()}
137 # Handle lists, sets, etc.
138 else:
139 return [_dictify(x) for x in original]
Ryan Beltranc37fb392023-05-11 18:24:40 +0000140 # Handle PackageInfo objects
141 elif isinstance(original, package_info.PackageInfo):
142 return original.atom
Ryan Beltrana32a1a12022-09-28 06:03:45 +0000143 # Handle everything else
144 return original
145
146 return json.dumps(_dictify(lint))
147
148
Ryan Beltran378934c2022-11-23 00:44:26 +0000149def get_all_sysroots() -> List[Text]:
150 """Gets all available sysroots for both host and boards."""
151 host_root = Path(build_target_lib.BuildTarget(None).root)
152 roots = [str(host_root)]
153 build_dir = host_root / "build"
154 for board in os.listdir(build_dir):
155 if board != "bin":
156 board_root = build_dir / board
157 if board_root.is_dir():
158 roots.append(str(board_root))
159 return roots
160
161
Ryan Beltran1f2dd082022-04-25 18:42:32 +0000162def get_arg_parser() -> commandline.ArgumentParser:
Alex Klein1699fab2022-09-08 08:46:06 -0600163 """Creates an argument parser for this script."""
164 default_board = cros_build_lib.GetDefaultBoard()
165 parser = commandline.ArgumentParser(description=__doc__)
Ryan Beltrandbd7b812022-06-08 23:36:16 +0000166
Ryan Beltran378934c2022-11-23 00:44:26 +0000167 board_group = parser.add_mutually_exclusive_group()
Alex Klein1699fab2022-09-08 08:46:06 -0600168 board_group.add_argument(
169 "-b",
170 "--board",
171 "--build-target",
172 dest="board",
173 default=default_board,
174 help="The board to emerge packages for",
175 )
176 board_group.add_argument(
177 "--host", action="store_true", help="emerge for host instead of board."
178 )
Ryan Beltran378934c2022-11-23 00:44:26 +0000179 parser.add_argument(
Alex Klein1699fab2022-09-08 08:46:06 -0600180 "--fetch-only",
181 action="store_true",
Alex Klein68b270c2023-04-14 14:42:50 -0600182 help="Fetch lints from previous run without resetting or calling "
183 "emerge.",
Alex Klein1699fab2022-09-08 08:46:06 -0600184 )
Alex Klein1699fab2022-09-08 08:46:06 -0600185 parser.add_argument(
186 "--differential",
187 action="store_true",
188 help="only lint lines touched by the last commit",
189 )
190 parser.add_argument(
191 "-o",
192 "--output",
193 default=sys.stdout,
194 help="File to use instead of stdout.",
195 )
196 parser.add_argument(
197 "--json", action="store_true", help="Output lints in JSON format."
198 )
199 parser.add_argument(
200 "--no-clippy",
201 dest="clippy",
202 action="store_false",
203 help="Disable cargo clippy linter.",
204 )
205 parser.add_argument(
206 "--no-tidy",
207 dest="tidy",
208 action="store_false",
209 help="Disable clang tidy linter.",
210 )
211 parser.add_argument(
212 "--no-golint",
213 dest="golint",
214 action="store_false",
215 help="Disable golint linter.",
216 )
217 parser.add_argument(
Ryan Beltran378934c2022-11-23 00:44:26 +0000218 "--iwyu",
219 action="store_true",
220 help="Enable include-what-you-use linter.",
221 )
222 parser.add_argument(
Alex Klein1699fab2022-09-08 08:46:06 -0600223 "packages",
224 nargs="*",
225 help="package(s) to emerge and retrieve lints for",
226 )
227 return parser
Ryan Beltran1f2dd082022-04-25 18:42:32 +0000228
229
230def parse_args(argv: List[str]):
Alex Klein1699fab2022-09-08 08:46:06 -0600231 """Parses arguments in argv and returns the options."""
232 parser = get_arg_parser()
233 opts = parser.parse_args(argv)
234 opts.Freeze()
Ryan Beltrandbd7b812022-06-08 23:36:16 +0000235
Alex Klein1699fab2022-09-08 08:46:06 -0600236 # A package must be specified unless we are in fetch-only mode
237 if not (opts.fetch_only or opts.packages):
Ryan Beltran378934c2022-11-23 00:44:26 +0000238 parser.error("Emerge mode requires specified package(s).")
Alex Klein1699fab2022-09-08 08:46:06 -0600239 if opts.fetch_only and opts.packages:
240 parser.error("Cannot specify packages for fetch-only mode.")
Ryan Beltrandbd7b812022-06-08 23:36:16 +0000241
Ryan Beltran378934c2022-11-23 00:44:26 +0000242 # A board must be specified unless we are in fetch-only mode
243 if not (opts.fetch_only or opts.board or opts.host):
244 parser.error("Emerge mode requires either --board or --host.")
245
Alex Klein1699fab2022-09-08 08:46:06 -0600246 return opts
Ryan Beltran1f2dd082022-04-25 18:42:32 +0000247
248
249def main(argv: List[str]) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -0600250 cros_build_lib.AssertInsideChroot()
251 opts = parse_args(argv)
Ryan Beltran1f2dd082022-04-25 18:42:32 +0000252
Alex Klein1699fab2022-09-08 08:46:06 -0600253 if opts.host:
254 # BuildTarget interprets None as host target
255 build_target = build_target_lib.BuildTarget(None)
Ryan Beltrandbd7b812022-06-08 23:36:16 +0000256 else:
Alex Klein1699fab2022-09-08 08:46:06 -0600257 build_target = build_target_lib.BuildTarget(opts.board)
258 packages = parse_packages(build_target, opts.packages)
259 package_atoms = [x.atom for x in packages]
Ryan Beltran1f2dd082022-04-25 18:42:32 +0000260
Alex Klein1699fab2022-09-08 08:46:06 -0600261 with workon_helper.WorkonScope(build_target, package_atoms):
262 build_linter = toolchain.BuildLinter(
263 packages, build_target.root, opts.differential
264 )
265 if opts.fetch_only:
Ryan Beltran378934c2022-11-23 00:44:26 +0000266 if opts.host or opts.board:
267 roots = [build_target.root]
268 else:
269 roots = get_all_sysroots()
270 lints = []
271 for root in roots:
272 build_linter.sysroot = root
273 lints.extend(
274 build_linter.fetch_findings(
275 use_clippy=opts.clippy,
276 use_tidy=opts.tidy,
277 use_golint=opts.golint,
278 use_iwyu=opts.iwyu,
279 )
280 )
Alex Klein1699fab2022-09-08 08:46:06 -0600281 else:
282 lints = build_linter.emerge_with_linting(
283 use_clippy=opts.clippy,
284 use_tidy=opts.tidy,
285 use_golint=opts.golint,
Ryan Beltran378934c2022-11-23 00:44:26 +0000286 use_iwyu=opts.iwyu,
Alex Klein1699fab2022-09-08 08:46:06 -0600287 )
Ryan Beltrance85d0f2022-08-09 21:36:39 +0000288
Alex Klein1699fab2022-09-08 08:46:06 -0600289 if opts.json:
Ryan Beltrana32a1a12022-09-28 06:03:45 +0000290 formatted_output_inner = ",\n".join(json_format_lint(l) for l in lints)
291 formatted_output = f"[{formatted_output_inner}]"
Alex Klein1699fab2022-09-08 08:46:06 -0600292 else:
Ryan Beltrana32a1a12022-09-28 06:03:45 +0000293 formatted_output = "\n".join(format_lint(l) for l in lints)
Alex Klein1699fab2022-09-08 08:46:06 -0600294
295 with file_util.Open(opts.output, "w") as output_file:
296 output_file.write(formatted_output)
297 if not opts.json:
298 output_file.write(f"\nFound {len(lints)} lints.")
299 output_file.write("\n")