blob: 485ded913118685dd3337e8886665caf639f9468 [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).
Aaron Masseyee4555c2023-07-27 22:39:57 +000025PROTOC_VERSION = "3.13.0"
Alex Kleinf9859972019-03-14 17:11:42 -060026
Aaron Masseyee4555c2023-07-27 22:39:57 +000027_CIPD_PACKAGE = "infra/tools/protoc/linux-amd64"
28_CIPD_PACKAGE_VERSION = f"protobuf_version:v{PROTOC_VERSION}"
Alex Kleinb382e4b2022-05-23 16:29:19 -060029
Alex Kleinf9859972019-03-14 17:11:42 -060030
Alex Klein5534f992019-09-16 16:31:23 -060031class Error(Exception):
Alex Klein1699fab2022-09-08 08:46:06 -060032 """Base error class for the module."""
Alex Klein5534f992019-09-16 16:31:23 -060033
34
35class GenerationError(Error):
Alex Klein1699fab2022-09-08 08:46:06 -060036 """A failure we can't recover from."""
Alex Klein5534f992019-09-16 16:31:23 -060037
38
Alex Klein098f7982021-03-01 13:15:29 -070039@enum.unique
40class ProtocVersion(enum.Enum):
Alex Klein1699fab2022-09-08 08:46:06 -060041 """Enum for possible protoc versions."""
Alex Klein098f7982021-03-01 13:15:29 -070042
Alex Klein1699fab2022-09-08 08:46:06 -060043 # The SDK version of the bindings use the protoc in the SDK, and so is
44 # compatible with the protobuf library in the SDK, i.e. the one installed
45 # via the ebuild.
46 SDK = enum.auto()
47 # The Chromite version of the bindings uses a protoc binary downloaded from
48 # CIPD that matches the version of the protobuf library in
49 # chromite/third_party/google/protobuf.
50 CHROMITE = enum.auto()
Alex Klein177bb942022-05-24 13:32:27 -060051
Alex Klein1699fab2022-09-08 08:46:06 -060052 def get_gen_dir(self) -> Path:
53 """Get the chromite/api directory path."""
54 if self is ProtocVersion.SDK:
Mike Frysingera69df982023-03-21 16:52:27 -040055 return constants.CHROMITE_DIR / "api" / "gen_sdk"
Alex Klein1699fab2022-09-08 08:46:06 -060056 else:
Mike Frysingera69df982023-03-21 16:52:27 -040057 return constants.CHROMITE_DIR / "api" / "gen"
Alex Klein177bb942022-05-24 13:32:27 -060058
Alex Klein1699fab2022-09-08 08:46:06 -060059 def get_proto_dir(self) -> Path:
60 """Get the proto directory for the target protoc."""
Mike Frysingera69df982023-03-21 16:52:27 -040061 return constants.CHROMITE_DIR / "infra" / "proto"
Alex Klein1699fab2022-09-08 08:46:06 -060062
63 def get_protoc_command(self, cipd_root: Optional[Path] = None) -> Path:
64 """Get protoc command path."""
65 assert self is ProtocVersion.SDK or cipd_root
66 if self is ProtocVersion.SDK:
67 return Path("protoc")
68 elif cipd_root:
Aaron Masseyee4555c2023-07-27 22:39:57 +000069 return cipd_root / "protoc"
Alex Kleindfad94c2022-05-23 16:59:47 -060070
Alex Klein098f7982021-03-01 13:15:29 -070071
Alex Klein851f4ee2022-03-29 16:03:45 -060072@enum.unique
73class SubdirectorySet(enum.Enum):
Alex Klein1699fab2022-09-08 08:46:06 -060074 """Enum for the subsets of the proto to compile."""
Alex Klein851f4ee2022-03-29 16:03:45 -060075
Alex Klein1699fab2022-09-08 08:46:06 -060076 ALL = enum.auto()
77 DEFAULT = enum.auto()
Alex Klein851f4ee2022-03-29 16:03:45 -060078
Alex Klein1699fab2022-09-08 08:46:06 -060079 def get_source_dirs(
80 self, source: Path, chromeos_config_path: Path
81 ) -> Iterable[Path]:
82 """Get the directories for the given subdirectory set."""
83 if self is self.ALL:
84 return [
85 source,
86 chromeos_config_path / "proto" / "chromiumos",
87 ]
88
89 subdirs = [
90 source / "analysis_service",
91 source / "chromite",
92 source / "chromiumos",
93 source / "config",
94 source / "test_platform",
95 source / "device",
96 chromeos_config_path / "proto" / "chromiumos",
97 ]
98 return subdirs
Alex Klein851f4ee2022-03-29 16:03:45 -060099
100
Alex Kleindfad94c2022-05-23 16:59:47 -0600101def InstallProtoc(protoc_version: ProtocVersion) -> Path:
Alex Klein1699fab2022-09-08 08:46:06 -0600102 """Install protoc from CIPD."""
103 if protoc_version is not ProtocVersion.CHROMITE:
104 cipd_root = None
105 else:
106 cipd_root = Path(
107 cipd.InstallPackage(
108 cipd.GetCIPDFromCache(), _CIPD_PACKAGE, _CIPD_PACKAGE_VERSION
109 )
110 )
111 return protoc_version.get_protoc_command(cipd_root)
Alex Klein5534f992019-09-16 16:31:23 -0600112
Alex Kleinf9859972019-03-14 17:11:42 -0600113
Alex Klein177bb942022-05-24 13:32:27 -0600114def _CleanTargetDirectory(directory: Path):
Alex Klein1699fab2022-09-08 08:46:06 -0600115 """Remove any existing generated files in the directory.
Alex Kleinf9859972019-03-14 17:11:42 -0600116
Alex Klein1699fab2022-09-08 08:46:06 -0600117 This clean only removes the generated files to avoid accidentally destroying
118 __init__.py customizations down the line. That will leave otherwise empty
119 directories in place if things get moved. Neither case is relevant at the
120 time of writing, but lingering empty directories seemed better than
121 diagnosing accidental __init__.py changes.
Alex Kleinf9859972019-03-14 17:11:42 -0600122
Alex Klein1699fab2022-09-08 08:46:06 -0600123 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600124 directory: Path to be cleaned up.
Alex Klein1699fab2022-09-08 08:46:06 -0600125 """
126 logging.info("Cleaning old files from %s.", directory)
127 for current in directory.rglob("*_pb2.py"):
128 # Remove old generated files.
129 current.unlink()
130 for current in directory.rglob("__init__.py"):
131 # Remove empty init files to clean up otherwise empty directories.
132 if not current.stat().st_size:
133 current.unlink()
Alex Kleinf9859972019-03-14 17:11:42 -0600134
Alex Klein5534f992019-09-16 16:31:23 -0600135
Alex Klein1699fab2022-09-08 08:46:06 -0600136def _GenerateFiles(
137 source: Path,
138 output: Path,
139 protoc_version: ProtocVersion,
140 dir_subset: SubdirectorySet,
141 protoc_bin_path: Path,
142):
143 """Generate the proto files from the |source| tree into |output|.
Alex Kleinf9859972019-03-14 17:11:42 -0600144
Alex Klein1699fab2022-09-08 08:46:06 -0600145 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600146 source: Path to the proto source root directory.
147 output: Path to the output root directory.
148 protoc_version: Which protoc to use.
149 dir_subset: The subset of the proto to compile.
150 protoc_bin_path: The protoc command to use.
Alex Klein1699fab2022-09-08 08:46:06 -0600151 """
152 logging.info("Generating files to %s.", output)
153 osutils.SafeMakedirs(output)
Alex Klein098f7982021-03-01 13:15:29 -0700154
Alex Klein1699fab2022-09-08 08:46:06 -0600155 targets = []
Alex Kleinf9859972019-03-14 17:11:42 -0600156
Alex Klein1699fab2022-09-08 08:46:06 -0600157 chromeos_config_path = Path(constants.SOURCE_ROOT) / "src" / "config"
Alex Klein098f7982021-03-01 13:15:29 -0700158
Alex Klein1699fab2022-09-08 08:46:06 -0600159 with tempfile.TemporaryDirectory() as tempdir:
160 if not chromeos_config_path.exists():
161 chromeos_config_path = Path(tempdir) / "config"
Alex Klein5534f992019-09-16 16:31:23 -0600162
Alex Klein1699fab2022-09-08 08:46:06 -0600163 logging.info("Creating shallow clone of chromiumos/config")
164 git.Clone(
165 chromeos_config_path,
166 "%s/chromiumos/config" % constants.EXTERNAL_GOB_URL,
167 depth=1,
168 )
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600169
Alex Klein1699fab2022-09-08 08:46:06 -0600170 for src_dir in dir_subset.get_source_dirs(source, chromeos_config_path):
171 targets.extend(list(src_dir.rglob("*.proto")))
Andrew Lamb59ed32e2021-07-26 15:14:37 -0600172
Alex Klein1699fab2022-09-08 08:46:06 -0600173 cmd = [
174 protoc_bin_path,
175 "-I",
176 chromeos_config_path / "proto",
177 "--python_out",
178 output,
179 "--proto_path",
180 source,
181 ]
182 cmd.extend(targets)
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600183
Alex Klein1699fab2022-09-08 08:46:06 -0600184 result = cros_build_lib.dbg_run(
185 cmd,
186 cwd=source,
187 check=False,
188 enter_chroot=protoc_version is ProtocVersion.SDK,
189 )
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600190
Alex Klein1699fab2022-09-08 08:46:06 -0600191 if result.returncode:
192 raise GenerationError(
193 "Error compiling the proto. See the output for a " "message."
194 )
Alex Kleinf9859972019-03-14 17:11:42 -0600195
196
Alex Klein177bb942022-05-24 13:32:27 -0600197def _InstallMissingInits(directory: Path):
Alex Klein54c891a2023-01-24 10:45:41 -0700198 """Add missing __init__.py files in the generated protobuf folders."""
Alex Klein1699fab2022-09-08 08:46:06 -0600199 logging.info("Adding missing __init__.py files in %s.", directory)
200 # glob ** returns only directories.
201 for current in directory.rglob("**"):
202 (current / "__init__.py").touch()
Alex Kleinf9859972019-03-14 17:11:42 -0600203
204
Alex Klein177bb942022-05-24 13:32:27 -0600205def _PostprocessFiles(directory: Path, protoc_version: ProtocVersion):
Alex Klein1699fab2022-09-08 08:46:06 -0600206 """Do postprocessing on the generated files.
Alex Kleinf9859972019-03-14 17:11:42 -0600207
Alex Klein1699fab2022-09-08 08:46:06 -0600208 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600209 directory: The root directory containing the generated files that are
210 to be processed.
211 protoc_version: Which protoc is being used to generate the files.
Alex Klein1699fab2022-09-08 08:46:06 -0600212 """
213 logging.info("Postprocessing: Fix imports in %s.", directory)
214 # We are using a negative address here (the /address/! portion of the sed
215 # command) to make sure we don't change any imports from protobuf itself.
216 address = "^from google.protobuf"
217 # Find: 'from x import y_pb2 as x_dot_y_pb2'.
218 # "\(^google.protobuf[^ ]*\)" matches the module we're importing from.
219 # - \( and \) are for groups in sed.
220 # - ^google.protobuf prevents changing the import for protobuf's files.
Alex Klein54c891a2023-01-24 10:45:41 -0700221 # - [^ ] = Not a space. The [:space:] character set is too broad, but
222 # would technically work too.
Alex Klein1699fab2022-09-08 08:46:06 -0600223 find = r"^from \([^ ]*\) import \([^ ]*\)_pb2 as \([^ ]*\)$"
224 # Substitute: 'from chromite.api.gen[_sdk].x import y_pb2 as x_dot_y_pb2'.
225 if protoc_version is ProtocVersion.SDK:
226 sub = "from chromite.api.gen_sdk.\\1 import \\2_pb2 as \\3"
227 else:
228 sub = "from chromite.api.gen.\\1 import \\2_pb2 as \\3"
Alex Klein098f7982021-03-01 13:15:29 -0700229
Alex Klein1699fab2022-09-08 08:46:06 -0600230 from_sed = [
231 "sed",
232 "-i",
233 "/%(address)s/!s/%(find)s/%(sub)s/g"
234 % {"address": address, "find": find, "sub": sub},
Alex Kleind20d8162021-06-21 12:40:44 -0600235 ]
Alex Kleind20d8162021-06-21 12:40:44 -0600236
Alex Klein1699fab2022-09-08 08:46:06 -0600237 seds = [from_sed]
238 if protoc_version is ProtocVersion.CHROMITE:
239 # We also need to change the google.protobuf imports to point directly
240 # at the chromite.third_party version of the library.
241 # The SDK version of the proto is meant to be used with the protobuf
242 # libraries installed in the SDK, so leave those as google.protobuf.
243 g_p_address = "^from google.protobuf"
244 g_p_find = r"from \([^ ]*\) import \(.*\)$"
245 g_p_sub = "from chromite.third_party.\\1 import \\2"
246 google_protobuf_sed = [
247 "sed",
248 "-i",
249 "/%(address)s/s/%(find)s/%(sub)s/g"
250 % {"address": g_p_address, "find": g_p_find, "sub": g_p_sub},
251 ]
252 seds.append(google_protobuf_sed)
253
254 pb2 = list(directory.rglob("*_pb2.py"))
255 if pb2:
256 for sed in seds:
257 cros_build_lib.dbg_run(sed + pb2)
Alex Kleinf9859972019-03-14 17:11:42 -0600258
259
Alex Klein1699fab2022-09-08 08:46:06 -0600260def CompileProto(
261 protoc_version: ProtocVersion,
262 output: Optional[Path] = None,
263 dir_subset: SubdirectorySet = SubdirectorySet.DEFAULT,
264 postprocess: bool = True,
265):
266 """Compile the Build API protobuf files.
Alex Kleinf9859972019-03-14 17:11:42 -0600267
Alex Kleinb6d52022022-10-18 08:55:06 -0600268 By default, this will compile from infra/proto/src to api/gen. The output
Alex Klein1699fab2022-09-08 08:46:06 -0600269 directory may be changed, but the imports will always be treated as if it is
270 in the default location.
Alex Kleinf9859972019-03-14 17:11:42 -0600271
Alex Klein1699fab2022-09-08 08:46:06 -0600272 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600273 output: The output directory.
Alex Kleinb6d52022022-10-18 08:55:06 -0600274 protoc_version: Which protoc to use for the compilation.
Alex Kleina0442682022-10-10 13:47:38 -0600275 dir_subset: What proto to compile.
276 postprocess: Whether to run the postprocess step.
Alex Klein1699fab2022-09-08 08:46:06 -0600277 """
278 protoc_version = protoc_version or ProtocVersion.CHROMITE
279 source = protoc_version.get_proto_dir() / "src"
280 if not output:
281 output = protoc_version.get_gen_dir()
Alex Kleinf9859972019-03-14 17:11:42 -0600282
Alex Klein1699fab2022-09-08 08:46:06 -0600283 protoc_bin_path = InstallProtoc(protoc_version)
284 _CleanTargetDirectory(output)
285 _GenerateFiles(source, output, protoc_version, dir_subset, protoc_bin_path)
286 _InstallMissingInits(output)
287 if postprocess:
288 _PostprocessFiles(output, protoc_version)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700289
290
291def GetParser():
Alex Klein1699fab2022-09-08 08:46:06 -0600292 """Build the argument parser."""
293 parser = commandline.ArgumentParser(description=__doc__)
294 standard_group = parser.add_argument_group(
295 "Committed Bindings",
296 description="Options for generating the bindings in chromite/api/.",
297 )
298 standard_group.add_argument(
299 "--chromite",
300 dest="protoc_version",
301 action="append_const",
302 const=ProtocVersion.CHROMITE,
Alex Klein54c891a2023-01-24 10:45:41 -0700303 help="Generate only the chromite bindings. Generates all by default. "
304 "The chromite bindings are compatible with the version of protobuf "
305 "in chromite/third_party.",
Alex Klein1699fab2022-09-08 08:46:06 -0600306 )
307 standard_group.add_argument(
308 "--sdk",
309 dest="protoc_version",
310 action="append_const",
311 const=ProtocVersion.SDK,
Alex Klein54c891a2023-01-24 10:45:41 -0700312 help="Generate only the SDK bindings. Generates all by default. The "
313 "SDK bindings are compiled by protoc in the SDK, and is compatible "
Alex Klein1699fab2022-09-08 08:46:06 -0600314 "with the version of protobuf in the SDK (i.e. the one installed by "
315 "the ebuild).",
316 )
Alex Klein098f7982021-03-01 13:15:29 -0700317
Alex Klein1699fab2022-09-08 08:46:06 -0600318 dest_group = parser.add_argument_group(
319 "Out of Tree Bindings",
320 description="Options for generating bindings in a custom location.",
321 )
322 dest_group.add_argument(
323 "--destination",
324 type="path",
325 help="A directory where a single version of the proto should be "
326 "generated. When not given, the proto generates in all default "
327 "locations instead.",
328 )
329 dest_group.add_argument(
330 "--dest-sdk",
331 action="store_const",
332 dest="dest_protoc",
333 default=ProtocVersion.CHROMITE,
334 const=ProtocVersion.SDK,
Alex Klein54c891a2023-01-24 10:45:41 -0700335 help="Generate the SDK version of the protos in --destination instead "
336 "of the chromite version.",
Alex Klein1699fab2022-09-08 08:46:06 -0600337 )
338 dest_group.add_argument(
339 "--all-proto",
340 action="store_const",
341 dest="dir_subset",
342 default=SubdirectorySet.DEFAULT,
343 const=SubdirectorySet.ALL,
344 help="Compile ALL proto instead of just the subset needed for the API. "
345 "Only considered when generating out of tree bindings.",
346 )
347 dest_group.add_argument(
348 "--skip-postprocessing",
349 action="store_false",
350 dest="postprocess",
351 default=True,
352 help="Skip postprocessing files.",
353 )
354 return parser
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700355
356
357def _ParseArguments(argv):
Alex Klein1699fab2022-09-08 08:46:06 -0600358 """Parse and validate arguments."""
359 parser = GetParser()
360 opts = parser.parse_args(argv)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700361
Alex Klein1699fab2022-09-08 08:46:06 -0600362 if not opts.protoc_version:
363 opts.protoc_version = [ProtocVersion.CHROMITE, ProtocVersion.SDK]
Alex Klein098f7982021-03-01 13:15:29 -0700364
Alex Klein1699fab2022-09-08 08:46:06 -0600365 if opts.destination:
366 opts.destination = Path(opts.destination)
Alex Klein177bb942022-05-24 13:32:27 -0600367
Alex Klein1699fab2022-09-08 08:46:06 -0600368 opts.Freeze()
369 return opts
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700370
371
372def main(argv):
Alex Klein1699fab2022-09-08 08:46:06 -0600373 opts = _ParseArguments(argv)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700374
Alex Klein1699fab2022-09-08 08:46:06 -0600375 if opts.destination:
376 # Destination set, only compile a single version in the destination.
377 try:
378 CompileProto(
379 protoc_version=opts.dest_protoc,
380 output=opts.destination,
381 dir_subset=opts.dir_subset,
382 postprocess=opts.postprocess,
383 )
384 except Error as e:
385 cros_build_lib.Die(
386 "Error compiling bindings to destination: %s", str(e)
387 )
388 else:
389 return 0
Alex Klein098f7982021-03-01 13:15:29 -0700390
Alex Klein1699fab2022-09-08 08:46:06 -0600391 if ProtocVersion.CHROMITE in opts.protoc_version:
392 # Compile the chromite bindings.
393 try:
394 CompileProto(protoc_version=ProtocVersion.CHROMITE)
395 except Error as e:
396 cros_build_lib.Die("Error compiling chromite bindings: %s", str(e))
Alex Klein098f7982021-03-01 13:15:29 -0700397
Alex Klein1699fab2022-09-08 08:46:06 -0600398 if ProtocVersion.SDK in opts.protoc_version:
399 # Compile the SDK bindings.
400 if not cros_build_lib.IsInsideChroot():
Alex Kleinb6d52022022-10-18 08:55:06 -0600401 # Rerun inside the SDK instead of trying to map all the paths.
Alex Klein1699fab2022-09-08 08:46:06 -0600402 cmd = [
403 (
404 Path(constants.CHROOT_SOURCE_ROOT)
405 / "chromite"
406 / "api"
407 / "compile_build_api_proto"
408 ),
409 "--sdk",
410 ]
411 result = cros_build_lib.dbg_run(cmd, enter_chroot=True, check=False)
412 return result.returncode
413 else:
414 try:
415 CompileProto(protoc_version=ProtocVersion.SDK)
416 except Error as e:
417 cros_build_lib.Die("Error compiling SDK bindings: %s", str(e))