blob: 67614b39cec26e2f036e53490c31d0d2b805b7f4 [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2018 The ChromiumOS Authors
Alex Kleinf4dc4f52018-12-05 13:55:12 -07002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
Alex Kleinf9859972019-03-14 17:11:42 -06005"""Compile the Build API's proto.
6
7Install proto using CIPD to ensure a consistent protoc version.
8"""
Alex Kleinf4dc4f52018-12-05 13:55:12 -07009
Alex Klein098f7982021-03-01 13:15:29 -070010import enum
Chris McDonald1672ddb2021-07-21 11:48:23 -060011import logging
Alex Kleinb382e4b2022-05-23 16:29:19 -060012from pathlib import Path
Sean McAllister6a5eaa02021-05-26 10:47:14 -060013import tempfile
Alex Klein177bb942022-05-24 13:32:27 -060014from typing import Iterable, Optional
Alex Kleinf4dc4f52018-12-05 13:55:12 -070015
Alex Kleinb382e4b2022-05-23 16:29:19 -060016from chromite.lib import cipd
Alex Kleinf4dc4f52018-12-05 13:55:12 -070017from chromite.lib import commandline
Alex Kleinc33c1912019-02-15 10:29:13 -070018from chromite.lib import constants
Alex Kleinf4dc4f52018-12-05 13:55:12 -070019from chromite.lib import cros_build_lib
Sean McAllister6a5eaa02021-05-26 10:47:14 -060020from chromite.lib import git
Alex Kleinf9859972019-03-14 17:11:42 -060021from chromite.lib import osutils
22
Mike Frysinger1cc8f1f2022-04-28 22:40:40 -040023
Alex Klein098f7982021-03-01 13:15:29 -070024# Chromite's protobuf library version (third_party/google/protobuf).
Trent Apted5a2038f2023-07-26 15:23:46 +100025PROTOC_VERSION = "21.9"
Alex Kleinf9859972019-03-14 17:11:42 -060026
Trent Apted5a2038f2023-07-26 15:23:46 +100027# Protobuf dropped the major version number after 3.20, jumping to 21.0. But
28# some places (e.g., in protobuf/__init__.py) refer to this as 4.21.0.
29PROTOC_MAJOR_VERSION = "4"
30
31_CIPD_PACKAGE = "infra/3pp/tools/protoc/linux-amd64"
32_CIPD_PACKAGE_VERSION = f"version:2@{PROTOC_VERSION}"
Alex Kleinb382e4b2022-05-23 16:29:19 -060033
Alex Kleinf9859972019-03-14 17:11:42 -060034
Alex Klein5534f992019-09-16 16:31:23 -060035class Error(Exception):
Alex Klein1699fab2022-09-08 08:46:06 -060036 """Base error class for the module."""
Alex Klein5534f992019-09-16 16:31:23 -060037
38
39class GenerationError(Error):
Alex Klein1699fab2022-09-08 08:46:06 -060040 """A failure we can't recover from."""
Alex Klein5534f992019-09-16 16:31:23 -060041
42
Alex Klein098f7982021-03-01 13:15:29 -070043@enum.unique
44class ProtocVersion(enum.Enum):
Alex Klein1699fab2022-09-08 08:46:06 -060045 """Enum for possible protoc versions."""
Alex Klein098f7982021-03-01 13:15:29 -070046
Alex Klein1699fab2022-09-08 08:46:06 -060047 # The SDK version of the bindings use the protoc in the SDK, and so is
48 # compatible with the protobuf library in the SDK, i.e. the one installed
49 # via the ebuild.
50 SDK = enum.auto()
51 # The Chromite version of the bindings uses a protoc binary downloaded from
52 # CIPD that matches the version of the protobuf library in
53 # chromite/third_party/google/protobuf.
54 CHROMITE = enum.auto()
Alex Klein177bb942022-05-24 13:32:27 -060055
Alex Klein1699fab2022-09-08 08:46:06 -060056 def get_gen_dir(self) -> Path:
57 """Get the chromite/api directory path."""
58 if self is ProtocVersion.SDK:
Mike Frysingera69df982023-03-21 16:52:27 -040059 return constants.CHROMITE_DIR / "api" / "gen_sdk"
Alex Klein1699fab2022-09-08 08:46:06 -060060 else:
Mike Frysingera69df982023-03-21 16:52:27 -040061 return constants.CHROMITE_DIR / "api" / "gen"
Alex Klein177bb942022-05-24 13:32:27 -060062
Alex Klein1699fab2022-09-08 08:46:06 -060063 def get_proto_dir(self) -> Path:
64 """Get the proto directory for the target protoc."""
Mike Frysingera69df982023-03-21 16:52:27 -040065 return constants.CHROMITE_DIR / "infra" / "proto"
Alex Klein1699fab2022-09-08 08:46:06 -060066
67 def get_protoc_command(self, cipd_root: Optional[Path] = None) -> Path:
68 """Get protoc command path."""
69 assert self is ProtocVersion.SDK or cipd_root
70 if self is ProtocVersion.SDK:
71 return Path("protoc")
72 elif cipd_root:
Trent Apted5a2038f2023-07-26 15:23:46 +100073 return cipd_root / "bin" / "protoc"
Alex Kleindfad94c2022-05-23 16:59:47 -060074
Alex Klein098f7982021-03-01 13:15:29 -070075
Alex Klein851f4ee2022-03-29 16:03:45 -060076@enum.unique
77class SubdirectorySet(enum.Enum):
Alex Klein1699fab2022-09-08 08:46:06 -060078 """Enum for the subsets of the proto to compile."""
Alex Klein851f4ee2022-03-29 16:03:45 -060079
Alex Klein1699fab2022-09-08 08:46:06 -060080 ALL = enum.auto()
81 DEFAULT = enum.auto()
Alex Klein851f4ee2022-03-29 16:03:45 -060082
Alex Klein1699fab2022-09-08 08:46:06 -060083 def get_source_dirs(
84 self, source: Path, chromeos_config_path: Path
85 ) -> Iterable[Path]:
86 """Get the directories for the given subdirectory set."""
87 if self is self.ALL:
88 return [
89 source,
90 chromeos_config_path / "proto" / "chromiumos",
91 ]
92
93 subdirs = [
94 source / "analysis_service",
95 source / "chromite",
96 source / "chromiumos",
97 source / "config",
98 source / "test_platform",
99 source / "device",
100 chromeos_config_path / "proto" / "chromiumos",
101 ]
102 return subdirs
Alex Klein851f4ee2022-03-29 16:03:45 -0600103
104
Alex Kleindfad94c2022-05-23 16:59:47 -0600105def InstallProtoc(protoc_version: ProtocVersion) -> Path:
Alex Klein1699fab2022-09-08 08:46:06 -0600106 """Install protoc from CIPD."""
107 if protoc_version is not ProtocVersion.CHROMITE:
108 cipd_root = None
109 else:
110 cipd_root = Path(
111 cipd.InstallPackage(
112 cipd.GetCIPDFromCache(), _CIPD_PACKAGE, _CIPD_PACKAGE_VERSION
113 )
114 )
115 return protoc_version.get_protoc_command(cipd_root)
Alex Klein5534f992019-09-16 16:31:23 -0600116
Alex Kleinf9859972019-03-14 17:11:42 -0600117
Alex Klein177bb942022-05-24 13:32:27 -0600118def _CleanTargetDirectory(directory: Path):
Alex Klein1699fab2022-09-08 08:46:06 -0600119 """Remove any existing generated files in the directory.
Alex Kleinf9859972019-03-14 17:11:42 -0600120
Alex Klein1699fab2022-09-08 08:46:06 -0600121 This clean only removes the generated files to avoid accidentally destroying
122 __init__.py customizations down the line. That will leave otherwise empty
123 directories in place if things get moved. Neither case is relevant at the
124 time of writing, but lingering empty directories seemed better than
125 diagnosing accidental __init__.py changes.
Alex Kleinf9859972019-03-14 17:11:42 -0600126
Alex Klein1699fab2022-09-08 08:46:06 -0600127 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600128 directory: Path to be cleaned up.
Alex Klein1699fab2022-09-08 08:46:06 -0600129 """
130 logging.info("Cleaning old files from %s.", directory)
131 for current in directory.rglob("*_pb2.py"):
132 # Remove old generated files.
133 current.unlink()
134 for current in directory.rglob("__init__.py"):
135 # Remove empty init files to clean up otherwise empty directories.
136 if not current.stat().st_size:
137 current.unlink()
Alex Kleinf9859972019-03-14 17:11:42 -0600138
Alex Klein5534f992019-09-16 16:31:23 -0600139
Alex Klein1699fab2022-09-08 08:46:06 -0600140def _GenerateFiles(
141 source: Path,
142 output: Path,
143 protoc_version: ProtocVersion,
144 dir_subset: SubdirectorySet,
145 protoc_bin_path: Path,
146):
147 """Generate the proto files from the |source| tree into |output|.
Alex Kleinf9859972019-03-14 17:11:42 -0600148
Alex Klein1699fab2022-09-08 08:46:06 -0600149 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600150 source: Path to the proto source root directory.
151 output: Path to the output root directory.
152 protoc_version: Which protoc to use.
153 dir_subset: The subset of the proto to compile.
154 protoc_bin_path: The protoc command to use.
Alex Klein1699fab2022-09-08 08:46:06 -0600155 """
156 logging.info("Generating files to %s.", output)
157 osutils.SafeMakedirs(output)
Alex Klein098f7982021-03-01 13:15:29 -0700158
Alex Klein1699fab2022-09-08 08:46:06 -0600159 targets = []
Alex Kleinf9859972019-03-14 17:11:42 -0600160
Alex Klein1699fab2022-09-08 08:46:06 -0600161 chromeos_config_path = Path(constants.SOURCE_ROOT) / "src" / "config"
Alex Klein098f7982021-03-01 13:15:29 -0700162
Alex Klein1699fab2022-09-08 08:46:06 -0600163 with tempfile.TemporaryDirectory() as tempdir:
164 if not chromeos_config_path.exists():
165 chromeos_config_path = Path(tempdir) / "config"
Alex Klein5534f992019-09-16 16:31:23 -0600166
Alex Klein1699fab2022-09-08 08:46:06 -0600167 logging.info("Creating shallow clone of chromiumos/config")
168 git.Clone(
169 chromeos_config_path,
170 "%s/chromiumos/config" % constants.EXTERNAL_GOB_URL,
171 depth=1,
172 )
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600173
Alex Klein1699fab2022-09-08 08:46:06 -0600174 for src_dir in dir_subset.get_source_dirs(source, chromeos_config_path):
175 targets.extend(list(src_dir.rglob("*.proto")))
Andrew Lamb59ed32e2021-07-26 15:14:37 -0600176
Alex Klein1699fab2022-09-08 08:46:06 -0600177 cmd = [
178 protoc_bin_path,
179 "-I",
180 chromeos_config_path / "proto",
181 "--python_out",
182 output,
183 "--proto_path",
184 source,
185 ]
186 cmd.extend(targets)
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600187
Alex Klein1699fab2022-09-08 08:46:06 -0600188 result = cros_build_lib.dbg_run(
189 cmd,
190 cwd=source,
191 check=False,
192 enter_chroot=protoc_version is ProtocVersion.SDK,
193 )
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600194
Alex Klein1699fab2022-09-08 08:46:06 -0600195 if result.returncode:
196 raise GenerationError(
197 "Error compiling the proto. See the output for a " "message."
198 )
Alex Kleinf9859972019-03-14 17:11:42 -0600199
200
Alex Klein177bb942022-05-24 13:32:27 -0600201def _InstallMissingInits(directory: Path):
Alex Klein54c891a2023-01-24 10:45:41 -0700202 """Add missing __init__.py files in the generated protobuf folders."""
Alex Klein1699fab2022-09-08 08:46:06 -0600203 logging.info("Adding missing __init__.py files in %s.", directory)
204 # glob ** returns only directories.
205 for current in directory.rglob("**"):
206 (current / "__init__.py").touch()
Alex Kleinf9859972019-03-14 17:11:42 -0600207
208
Alex Klein177bb942022-05-24 13:32:27 -0600209def _PostprocessFiles(directory: Path, protoc_version: ProtocVersion):
Alex Klein1699fab2022-09-08 08:46:06 -0600210 """Do postprocessing on the generated files.
Alex Kleinf9859972019-03-14 17:11:42 -0600211
Alex Klein1699fab2022-09-08 08:46:06 -0600212 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600213 directory: The root directory containing the generated files that are
214 to be processed.
215 protoc_version: Which protoc is being used to generate the files.
Alex Klein1699fab2022-09-08 08:46:06 -0600216 """
217 logging.info("Postprocessing: Fix imports in %s.", directory)
218 # We are using a negative address here (the /address/! portion of the sed
219 # command) to make sure we don't change any imports from protobuf itself.
220 address = "^from google.protobuf"
221 # Find: 'from x import y_pb2 as x_dot_y_pb2'.
222 # "\(^google.protobuf[^ ]*\)" matches the module we're importing from.
223 # - \( and \) are for groups in sed.
224 # - ^google.protobuf prevents changing the import for protobuf's files.
Alex Klein54c891a2023-01-24 10:45:41 -0700225 # - [^ ] = Not a space. The [:space:] character set is too broad, but
226 # would technically work too.
Alex Klein1699fab2022-09-08 08:46:06 -0600227 find = r"^from \([^ ]*\) import \([^ ]*\)_pb2 as \([^ ]*\)$"
228 # Substitute: 'from chromite.api.gen[_sdk].x import y_pb2 as x_dot_y_pb2'.
229 if protoc_version is ProtocVersion.SDK:
230 sub = "from chromite.api.gen_sdk.\\1 import \\2_pb2 as \\3"
231 else:
232 sub = "from chromite.api.gen.\\1 import \\2_pb2 as \\3"
Alex Klein098f7982021-03-01 13:15:29 -0700233
Alex Klein1699fab2022-09-08 08:46:06 -0600234 from_sed = [
235 "sed",
236 "-i",
237 "/%(address)s/!s/%(find)s/%(sub)s/g"
238 % {"address": address, "find": find, "sub": sub},
Alex Kleind20d8162021-06-21 12:40:44 -0600239 ]
Alex Kleind20d8162021-06-21 12:40:44 -0600240
Alex Klein1699fab2022-09-08 08:46:06 -0600241 seds = [from_sed]
242 if protoc_version is ProtocVersion.CHROMITE:
243 # We also need to change the google.protobuf imports to point directly
244 # at the chromite.third_party version of the library.
245 # The SDK version of the proto is meant to be used with the protobuf
246 # libraries installed in the SDK, so leave those as google.protobuf.
247 g_p_address = "^from google.protobuf"
248 g_p_find = r"from \([^ ]*\) import \(.*\)$"
249 g_p_sub = "from chromite.third_party.\\1 import \\2"
250 google_protobuf_sed = [
251 "sed",
252 "-i",
253 "/%(address)s/s/%(find)s/%(sub)s/g"
254 % {"address": g_p_address, "find": g_p_find, "sub": g_p_sub},
255 ]
256 seds.append(google_protobuf_sed)
257
258 pb2 = list(directory.rglob("*_pb2.py"))
259 if pb2:
260 for sed in seds:
261 cros_build_lib.dbg_run(sed + pb2)
Alex Kleinf9859972019-03-14 17:11:42 -0600262
263
Alex Klein1699fab2022-09-08 08:46:06 -0600264def CompileProto(
265 protoc_version: ProtocVersion,
266 output: Optional[Path] = None,
267 dir_subset: SubdirectorySet = SubdirectorySet.DEFAULT,
268 postprocess: bool = True,
269):
270 """Compile the Build API protobuf files.
Alex Kleinf9859972019-03-14 17:11:42 -0600271
Alex Kleinb6d52022022-10-18 08:55:06 -0600272 By default, this will compile from infra/proto/src to api/gen. The output
Alex Klein1699fab2022-09-08 08:46:06 -0600273 directory may be changed, but the imports will always be treated as if it is
274 in the default location.
Alex Kleinf9859972019-03-14 17:11:42 -0600275
Alex Klein1699fab2022-09-08 08:46:06 -0600276 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600277 output: The output directory.
Alex Kleinb6d52022022-10-18 08:55:06 -0600278 protoc_version: Which protoc to use for the compilation.
Alex Kleina0442682022-10-10 13:47:38 -0600279 dir_subset: What proto to compile.
280 postprocess: Whether to run the postprocess step.
Alex Klein1699fab2022-09-08 08:46:06 -0600281 """
282 protoc_version = protoc_version or ProtocVersion.CHROMITE
283 source = protoc_version.get_proto_dir() / "src"
284 if not output:
285 output = protoc_version.get_gen_dir()
Alex Kleinf9859972019-03-14 17:11:42 -0600286
Alex Klein1699fab2022-09-08 08:46:06 -0600287 protoc_bin_path = InstallProtoc(protoc_version)
288 _CleanTargetDirectory(output)
289 _GenerateFiles(source, output, protoc_version, dir_subset, protoc_bin_path)
290 _InstallMissingInits(output)
291 if postprocess:
292 _PostprocessFiles(output, protoc_version)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700293
294
295def GetParser():
Alex Klein1699fab2022-09-08 08:46:06 -0600296 """Build the argument parser."""
297 parser = commandline.ArgumentParser(description=__doc__)
298 standard_group = parser.add_argument_group(
299 "Committed Bindings",
300 description="Options for generating the bindings in chromite/api/.",
301 )
302 standard_group.add_argument(
303 "--chromite",
304 dest="protoc_version",
305 action="append_const",
306 const=ProtocVersion.CHROMITE,
Alex Klein54c891a2023-01-24 10:45:41 -0700307 help="Generate only the chromite bindings. Generates all by default. "
308 "The chromite bindings are compatible with the version of protobuf "
309 "in chromite/third_party.",
Alex Klein1699fab2022-09-08 08:46:06 -0600310 )
311 standard_group.add_argument(
312 "--sdk",
313 dest="protoc_version",
314 action="append_const",
315 const=ProtocVersion.SDK,
Alex Klein54c891a2023-01-24 10:45:41 -0700316 help="Generate only the SDK bindings. Generates all by default. The "
317 "SDK bindings are compiled by protoc in the SDK, and is compatible "
Alex Klein1699fab2022-09-08 08:46:06 -0600318 "with the version of protobuf in the SDK (i.e. the one installed by "
319 "the ebuild).",
320 )
Alex Klein098f7982021-03-01 13:15:29 -0700321
Alex Klein1699fab2022-09-08 08:46:06 -0600322 dest_group = parser.add_argument_group(
323 "Out of Tree Bindings",
324 description="Options for generating bindings in a custom location.",
325 )
326 dest_group.add_argument(
327 "--destination",
328 type="path",
329 help="A directory where a single version of the proto should be "
330 "generated. When not given, the proto generates in all default "
331 "locations instead.",
332 )
333 dest_group.add_argument(
334 "--dest-sdk",
335 action="store_const",
336 dest="dest_protoc",
337 default=ProtocVersion.CHROMITE,
338 const=ProtocVersion.SDK,
Alex Klein54c891a2023-01-24 10:45:41 -0700339 help="Generate the SDK version of the protos in --destination instead "
340 "of the chromite version.",
Alex Klein1699fab2022-09-08 08:46:06 -0600341 )
342 dest_group.add_argument(
343 "--all-proto",
344 action="store_const",
345 dest="dir_subset",
346 default=SubdirectorySet.DEFAULT,
347 const=SubdirectorySet.ALL,
348 help="Compile ALL proto instead of just the subset needed for the API. "
349 "Only considered when generating out of tree bindings.",
350 )
351 dest_group.add_argument(
352 "--skip-postprocessing",
353 action="store_false",
354 dest="postprocess",
355 default=True,
356 help="Skip postprocessing files.",
357 )
358 return parser
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700359
360
361def _ParseArguments(argv):
Alex Klein1699fab2022-09-08 08:46:06 -0600362 """Parse and validate arguments."""
363 parser = GetParser()
364 opts = parser.parse_args(argv)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700365
Alex Klein1699fab2022-09-08 08:46:06 -0600366 if not opts.protoc_version:
367 opts.protoc_version = [ProtocVersion.CHROMITE, ProtocVersion.SDK]
Alex Klein098f7982021-03-01 13:15:29 -0700368
Alex Klein1699fab2022-09-08 08:46:06 -0600369 if opts.destination:
370 opts.destination = Path(opts.destination)
Alex Klein177bb942022-05-24 13:32:27 -0600371
Alex Klein1699fab2022-09-08 08:46:06 -0600372 opts.Freeze()
373 return opts
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700374
375
376def main(argv):
Alex Klein1699fab2022-09-08 08:46:06 -0600377 opts = _ParseArguments(argv)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700378
Alex Klein1699fab2022-09-08 08:46:06 -0600379 if opts.destination:
380 # Destination set, only compile a single version in the destination.
381 try:
382 CompileProto(
383 protoc_version=opts.dest_protoc,
384 output=opts.destination,
385 dir_subset=opts.dir_subset,
386 postprocess=opts.postprocess,
387 )
388 except Error as e:
389 cros_build_lib.Die(
390 "Error compiling bindings to destination: %s", str(e)
391 )
392 else:
393 return 0
Alex Klein098f7982021-03-01 13:15:29 -0700394
Alex Klein1699fab2022-09-08 08:46:06 -0600395 if ProtocVersion.CHROMITE in opts.protoc_version:
396 # Compile the chromite bindings.
397 try:
398 CompileProto(protoc_version=ProtocVersion.CHROMITE)
399 except Error as e:
400 cros_build_lib.Die("Error compiling chromite bindings: %s", str(e))
Alex Klein098f7982021-03-01 13:15:29 -0700401
Alex Klein1699fab2022-09-08 08:46:06 -0600402 if ProtocVersion.SDK in opts.protoc_version:
403 # Compile the SDK bindings.
404 if not cros_build_lib.IsInsideChroot():
Alex Kleinb6d52022022-10-18 08:55:06 -0600405 # Rerun inside the SDK instead of trying to map all the paths.
Alex Klein1699fab2022-09-08 08:46:06 -0600406 cmd = [
407 (
408 Path(constants.CHROOT_SOURCE_ROOT)
409 / "chromite"
410 / "api"
411 / "compile_build_api_proto"
412 ),
413 "--sdk",
414 ]
415 result = cros_build_lib.dbg_run(cmd, enter_chroot=True, check=False)
416 return result.returncode
417 else:
418 try:
419 CompileProto(protoc_version=ProtocVersion.SDK)
420 except Error as e:
421 cros_build_lib.Die("Error compiling SDK bindings: %s", str(e))