blob: 44002ab56cfa7cf2ae0b4feec5e2cae7fc49f4e9 [file] [log] [blame]
Alex Kleinf4dc4f52018-12-05 13:55:12 -07001# Copyright 2018 The Chromium OS Authors. All rights reserved.
2# 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).
Mike Nicholse366e7a2021-02-22 18:14:57 -070025PROTOC_VERSION = '3.13.0'
Alex Kleinf9859972019-03-14 17:11:42 -060026
Alex Kleinb382e4b2022-05-23 16:29:19 -060027_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):
32 """Base error class for the module."""
33
34
35class GenerationError(Error):
36 """A failure we can't recover from."""
37
38
Alex Klein098f7982021-03-01 13:15:29 -070039@enum.unique
40class ProtocVersion(enum.Enum):
41 """Enum for possible protoc versions."""
42 # The SDK version of the bindings use the protoc in the SDK, and so is
43 # compatible with the protobuf library in the SDK, i.e. the one installed
44 # via the ebuild.
45 SDK = enum.auto()
46 # The Chromite version of the bindings uses a protoc binary downloaded from
47 # CIPD that matches the version of the protobuf library in
48 # chromite/third_party/google/protobuf.
49 CHROMITE = enum.auto()
50
Alex Klein177bb942022-05-24 13:32:27 -060051 def get_gen_dir(self) -> Path:
52 """Get the chromite/api directory path."""
53 if self is ProtocVersion.SDK:
54 return Path(constants.CHROMITE_DIR) / 'api' / 'gen_sdk'
55 else:
56 return Path(constants.CHROMITE_DIR) / 'api' / 'gen'
57
58 def get_proto_dir(self) -> Path:
59 """Get the proto directory for the target protoc."""
60 return Path(constants.CHROMITE_DIR) / 'infra' / 'proto'
61
Alex Kleindfad94c2022-05-23 16:59:47 -060062 def get_protoc_command(self, cipd_root: Optional[Path] = None) -> Path:
63 """Get protoc command path."""
64 assert self is ProtocVersion.SDK or cipd_root
65 if self is ProtocVersion.SDK:
66 return Path('protoc')
67 elif cipd_root:
68 return cipd_root / 'protoc'
69
Alex Klein098f7982021-03-01 13:15:29 -070070
Alex Klein851f4ee2022-03-29 16:03:45 -060071@enum.unique
72class SubdirectorySet(enum.Enum):
73 """Enum for the subsets of the proto to compile."""
74 ALL = enum.auto()
75 DEFAULT = enum.auto()
76
Alex Klein177bb942022-05-24 13:32:27 -060077 def get_source_dirs(self, source: Path,
78 chromeos_config_path: Path) -> Iterable[Path]:
Alex Klein851f4ee2022-03-29 16:03:45 -060079 """Get the directories for the given subdirectory set."""
Alex Klein851f4ee2022-03-29 16:03:45 -060080 if self is self.ALL:
81 return [
82 source,
Alex Klein177bb942022-05-24 13:32:27 -060083 chromeos_config_path / 'proto' / 'chromiumos',
Alex Klein851f4ee2022-03-29 16:03:45 -060084 ]
85
86 subdirs = [
Alex Klein177bb942022-05-24 13:32:27 -060087 source / 'analysis_service',
88 source / 'chromite',
89 source / 'chromiumos',
90 source / 'config',
91 source / 'test_platform',
92 source / 'device',
93 chromeos_config_path / 'proto' / 'chromiumos',
Alex Klein851f4ee2022-03-29 16:03:45 -060094 ]
95 return subdirs
96
97
Alex Kleindfad94c2022-05-23 16:59:47 -060098def InstallProtoc(protoc_version: ProtocVersion) -> Path:
Alex Kleinf9859972019-03-14 17:11:42 -060099 """Install protoc from CIPD."""
Alex Klein098f7982021-03-01 13:15:29 -0700100 if protoc_version is not ProtocVersion.CHROMITE:
Alex Kleinb382e4b2022-05-23 16:29:19 -0600101 cipd_root = None
102 else:
103 cipd_root = Path(
104 cipd.InstallPackage(cipd.GetCIPDFromCache(), _CIPD_PACKAGE,
Alex Kleindfad94c2022-05-23 16:59:47 -0600105 _CIPD_PACKAGE_VERSION))
106 return protoc_version.get_protoc_command(cipd_root)
Alex Klein5534f992019-09-16 16:31:23 -0600107
Alex Kleinf9859972019-03-14 17:11:42 -0600108
Alex Klein177bb942022-05-24 13:32:27 -0600109def _CleanTargetDirectory(directory: Path):
Alex Kleinf9859972019-03-14 17:11:42 -0600110 """Remove any existing generated files in the directory.
111
112 This clean only removes the generated files to avoid accidentally destroying
113 __init__.py customizations down the line. That will leave otherwise empty
114 directories in place if things get moved. Neither case is relevant at the
115 time of writing, but lingering empty directories seemed better than
116 diagnosing accidental __init__.py changes.
117
118 Args:
Miriam Polzer713e6542021-08-17 10:58:14 +0200119 directory: Path to be cleaned up.
Alex Kleinf9859972019-03-14 17:11:42 -0600120 """
Alex Klein098f7982021-03-01 13:15:29 -0700121 logging.info('Cleaning old files from %s.', directory)
Alex Klein177bb942022-05-24 13:32:27 -0600122 for current in directory.rglob('*_pb2.py'):
123 # Remove old generated files.
124 current.unlink()
125 for current in directory.rglob('__init__.py'):
Alex Klein5534f992019-09-16 16:31:23 -0600126 # Remove empty init files to clean up otherwise empty directories.
Alex Klein177bb942022-05-24 13:32:27 -0600127 if not current.stat().st_size:
128 current.unlink()
Alex Kleinf9859972019-03-14 17:11:42 -0600129
Alex Klein5534f992019-09-16 16:31:23 -0600130
Alex Klein177bb942022-05-24 13:32:27 -0600131def _GenerateFiles(source: Path, output: Path, protoc_version: ProtocVersion,
Alex Kleindfad94c2022-05-23 16:59:47 -0600132 dir_subset: SubdirectorySet, protoc_bin_path: Path):
Alex Kleinf9859972019-03-14 17:11:42 -0600133 """Generate the proto files from the |source| tree into |output|.
134
135 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700136 source: Path to the proto source root directory.
137 output: Path to the output root directory.
138 protoc_version: Which protoc to use.
Alex Klein851f4ee2022-03-29 16:03:45 -0600139 dir_subset: The subset of the proto to compile.
Alex Kleinb382e4b2022-05-23 16:29:19 -0600140 protoc_bin_path: The protoc command to use.
Alex Kleinf9859972019-03-14 17:11:42 -0600141 """
Alex Klein098f7982021-03-01 13:15:29 -0700142 logging.info('Generating files to %s.', output)
143 osutils.SafeMakedirs(output)
144
Alex Kleinf9859972019-03-14 17:11:42 -0600145 targets = []
146
Alex Klein177bb942022-05-24 13:32:27 -0600147 chromeos_config_path = (
148 Path(constants.SOURCE_ROOT) / 'src' / 'config')
Alex Klein098f7982021-03-01 13:15:29 -0700149
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600150 with tempfile.TemporaryDirectory() as tempdir:
Alex Klein177bb942022-05-24 13:32:27 -0600151 if not chromeos_config_path.exists():
152 chromeos_config_path = Path(tempdir) / 'config'
Alex Klein5534f992019-09-16 16:31:23 -0600153
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600154 logging.info('Creating shallow clone of chromiumos/config')
Alex Klein4fd378d2022-03-29 16:00:49 -0600155 git.Clone(
156 chromeos_config_path,
157 '%s/chromiumos/config' % constants.EXTERNAL_GOB_URL,
158 depth=1)
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600159
Alex Klein177bb942022-05-24 13:32:27 -0600160 for src_dir in dir_subset.get_source_dirs(source, chromeos_config_path):
161 targets.extend(list(src_dir.rglob('*.proto')))
Andrew Lamb59ed32e2021-07-26 15:14:37 -0600162
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600163 cmd = [
Alex Kleinb382e4b2022-05-23 16:29:19 -0600164 protoc_bin_path,
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600165 '-I',
Alex Klein177bb942022-05-24 13:32:27 -0600166 chromeos_config_path / 'proto',
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600167 '--python_out',
168 output,
169 '--proto_path',
170 source,
171 ]
172 cmd.extend(targets)
173
Alex Klein177bb942022-05-24 13:32:27 -0600174 result = cros_build_lib.dbg_run(
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600175 cmd,
176 cwd=source,
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600177 check=False,
178 enter_chroot=protoc_version is ProtocVersion.SDK)
179
180 if result.returncode:
181 raise GenerationError('Error compiling the proto. See the output for a '
182 'message.')
Alex Kleinf9859972019-03-14 17:11:42 -0600183
184
Alex Klein177bb942022-05-24 13:32:27 -0600185def _InstallMissingInits(directory: Path):
Alex Kleinf9859972019-03-14 17:11:42 -0600186 """Add any __init__.py files not present in the generated protobuf folders."""
Alex Klein098f7982021-03-01 13:15:29 -0700187 logging.info('Adding missing __init__.py files in %s.', directory)
Alex Klein177bb942022-05-24 13:32:27 -0600188 # glob ** returns only directories.
189 for current in directory.rglob('**'):
190 (current / '__init__.py').touch()
Alex Kleinf9859972019-03-14 17:11:42 -0600191
192
Alex Klein177bb942022-05-24 13:32:27 -0600193def _PostprocessFiles(directory: Path, protoc_version: ProtocVersion):
Alex Kleinf9859972019-03-14 17:11:42 -0600194 """Do postprocessing on the generated files.
195
196 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700197 directory: The root directory containing the generated files that are
Alex Kleinf9859972019-03-14 17:11:42 -0600198 to be processed.
Alex Klein098f7982021-03-01 13:15:29 -0700199 protoc_version: Which protoc is being used to generate the files.
Alex Kleinf9859972019-03-14 17:11:42 -0600200 """
Alex Klein098f7982021-03-01 13:15:29 -0700201 logging.info('Postprocessing: Fix imports in %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600202 # We are using a negative address here (the /address/! portion of the sed
203 # command) to make sure we don't change any imports from protobuf itself.
Alex Kleind20d8162021-06-21 12:40:44 -0600204 address = '^from google.protobuf'
Alex Kleinf9859972019-03-14 17:11:42 -0600205 # Find: 'from x import y_pb2 as x_dot_y_pb2'.
206 # "\(^google.protobuf[^ ]*\)" matches the module we're importing from.
207 # - \( and \) are for groups in sed.
208 # - ^google.protobuf prevents changing the import for protobuf's files.
209 # - [^ ] = Not a space. The [:space:] character set is too broad, but would
210 # technically work too.
211 find = r'^from \([^ ]*\) import \([^ ]*\)_pb2 as \([^ ]*\)$'
Alex Klein098f7982021-03-01 13:15:29 -0700212 # Substitute: 'from chromite.api.gen[_sdk].x import y_pb2 as x_dot_y_pb2'.
213 if protoc_version is ProtocVersion.SDK:
214 sub = 'from chromite.api.gen_sdk.\\1 import \\2_pb2 as \\3'
215 else:
216 sub = 'from chromite.api.gen.\\1 import \\2_pb2 as \\3'
217
Alex Klein5534f992019-09-16 16:31:23 -0600218 from_sed = [
219 'sed', '-i',
220 '/%(address)s/!s/%(find)s/%(sub)s/g' % {
221 'address': address,
222 'find': find,
223 'sub': sub
224 }
225 ]
Alex Kleinf9859972019-03-14 17:11:42 -0600226
Alex Kleind20d8162021-06-21 12:40:44 -0600227 seds = [from_sed]
228 if protoc_version is ProtocVersion.CHROMITE:
229 # We also need to change the google.protobuf imports to point directly
230 # at the chromite.third_party version of the library.
231 # The SDK version of the proto is meant to be used with the protobuf
232 # libraries installed in the SDK, so leave those as google.protobuf.
233 g_p_address = '^from google.protobuf'
234 g_p_find = r'from \([^ ]*\) import \(.*\)$'
235 g_p_sub = 'from chromite.third_party.\\1 import \\2'
236 google_protobuf_sed = [
237 'sed', '-i',
238 '/%(address)s/s/%(find)s/%(sub)s/g' % {
239 'address': g_p_address,
240 'find': g_p_find,
241 'sub': g_p_sub
242 }
243 ]
244 seds.append(google_protobuf_sed)
245
Alex Klein177bb942022-05-24 13:32:27 -0600246 pb2 = list(directory.rglob('*_pb2.py'))
247 if pb2:
248 for sed in seds:
249 cros_build_lib.dbg_run(sed + pb2)
Alex Kleinf9859972019-03-14 17:11:42 -0600250
251
Alex Klein177bb942022-05-24 13:32:27 -0600252def CompileProto(protoc_version: ProtocVersion,
253 output: Optional[Path] = None,
Alex Klein5b1bca32022-04-05 10:56:35 -0600254 dir_subset: SubdirectorySet = SubdirectorySet.DEFAULT,
255 postprocess: bool = True):
Alex Kleinf9859972019-03-14 17:11:42 -0600256 """Compile the Build API protobuf files.
257
258 By default this will compile from infra/proto/src to api/gen. The output
259 directory may be changed, but the imports will always be treated as if it is
260 in the default location.
261
262 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700263 output: The output directory.
264 protoc_version: Which protoc to use for the compile.
Alex Klein851f4ee2022-03-29 16:03:45 -0600265 dir_subset: What proto to compile.
Alex Klein5b1bca32022-04-05 10:56:35 -0600266 postprocess: Whether to run the postprocess step.
Alex Kleinf9859972019-03-14 17:11:42 -0600267 """
Alex Klein098f7982021-03-01 13:15:29 -0700268 protoc_version = protoc_version or ProtocVersion.CHROMITE
Alex Klein177bb942022-05-24 13:32:27 -0600269 source = protoc_version.get_proto_dir() / 'src'
270 if not output:
271 output = protoc_version.get_gen_dir()
Alex Kleinf9859972019-03-14 17:11:42 -0600272
Alex Kleinb382e4b2022-05-23 16:29:19 -0600273 protoc_bin_path = InstallProtoc(protoc_version)
Alex Kleinf9859972019-03-14 17:11:42 -0600274 _CleanTargetDirectory(output)
Alex Kleinb382e4b2022-05-23 16:29:19 -0600275 _GenerateFiles(source, output, protoc_version, dir_subset, protoc_bin_path)
Alex Kleinf9859972019-03-14 17:11:42 -0600276 _InstallMissingInits(output)
Alex Klein5b1bca32022-04-05 10:56:35 -0600277 if postprocess:
278 _PostprocessFiles(output, protoc_version)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700279
280
281def GetParser():
Alex Kleinf9859972019-03-14 17:11:42 -0600282 """Build the argument parser."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700283 parser = commandline.ArgumentParser(description=__doc__)
Alex Klein098f7982021-03-01 13:15:29 -0700284 standard_group = parser.add_argument_group(
285 'Committed Bindings',
286 description='Options for generating the bindings in chromite/api/.')
287 standard_group.add_argument(
288 '--chromite',
289 dest='protoc_version',
290 action='append_const',
291 const=ProtocVersion.CHROMITE,
292 help='Generate only the chromite bindings. Generates all by default. The '
Alex Klein5b1bca32022-04-05 10:56:35 -0600293 'chromite bindings are compatible with the version of protobuf in '
294 'chromite/third_party.')
Alex Klein098f7982021-03-01 13:15:29 -0700295 standard_group.add_argument(
296 '--sdk',
297 dest='protoc_version',
298 action='append_const',
299 const=ProtocVersion.SDK,
300 help='Generate only the SDK bindings. Generates all by default. The SDK '
Alex Klein5b1bca32022-04-05 10:56:35 -0600301 'bindings are compiled by protoc in the SDK, and is compatible '
302 'with the version of protobuf in the SDK (i.e. the one installed by '
303 'the ebuild).')
Alex Klein098f7982021-03-01 13:15:29 -0700304
305 dest_group = parser.add_argument_group(
306 'Out of Tree Bindings',
307 description='Options for generating bindings in a custom location.')
308 dest_group.add_argument(
Alex Klein5534f992019-09-16 16:31:23 -0600309 '--destination',
310 type='path',
Alex Klein098f7982021-03-01 13:15:29 -0700311 help='A directory where a single version of the proto should be '
Alex Klein5b1bca32022-04-05 10:56:35 -0600312 'generated. When not given, the proto generates in all default '
313 'locations instead.')
Alex Klein098f7982021-03-01 13:15:29 -0700314 dest_group.add_argument(
315 '--dest-sdk',
316 action='store_const',
317 dest='dest_protoc',
318 default=ProtocVersion.CHROMITE,
319 const=ProtocVersion.SDK,
320 help='Generate the SDK version of the protos in --destination instead of '
Alex Klein5b1bca32022-04-05 10:56:35 -0600321 'the chromite version.')
Alex Klein851f4ee2022-03-29 16:03:45 -0600322 dest_group.add_argument(
323 '--all-proto',
324 action='store_const',
325 dest='dir_subset',
326 default=SubdirectorySet.DEFAULT,
327 const=SubdirectorySet.ALL,
328 help='Compile ALL proto instead of just the subset needed for the API. '
Alex Klein5b1bca32022-04-05 10:56:35 -0600329 'Only considered when generating out of tree bindings.')
330 dest_group.add_argument(
331 '--skip-postprocessing',
332 action='store_false',
333 dest='postprocess',
334 default=True,
335 help='Skip postprocessing files.'
336 )
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700337 return parser
338
339
340def _ParseArguments(argv):
341 """Parse and validate arguments."""
342 parser = GetParser()
343 opts = parser.parse_args(argv)
344
Alex Klein098f7982021-03-01 13:15:29 -0700345 if not opts.protoc_version:
346 opts.protoc_version = [ProtocVersion.CHROMITE, ProtocVersion.SDK]
347
Alex Klein177bb942022-05-24 13:32:27 -0600348 if opts.destination:
349 opts.destination = Path(opts.destination)
350
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700351 opts.Freeze()
352 return opts
353
354
355def main(argv):
Alex Kleinf9859972019-03-14 17:11:42 -0600356 opts = _ParseArguments(argv)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700357
Alex Klein098f7982021-03-01 13:15:29 -0700358 if opts.destination:
359 # Destination set, only compile a single version in the destination.
360 try:
Alex Klein851f4ee2022-03-29 16:03:45 -0600361 CompileProto(
Alex Klein851f4ee2022-03-29 16:03:45 -0600362 protoc_version=opts.dest_protoc,
Alex Klein177bb942022-05-24 13:32:27 -0600363 output=opts.destination,
Alex Klein5b1bca32022-04-05 10:56:35 -0600364 dir_subset=opts.dir_subset,
365 postprocess=opts.postprocess
366 )
Alex Klein098f7982021-03-01 13:15:29 -0700367 except Error as e:
368 cros_build_lib.Die('Error compiling bindings to destination: %s', str(e))
369 else:
370 return 0
371
372 if ProtocVersion.CHROMITE in opts.protoc_version:
373 # Compile the chromite bindings.
374 try:
Alex Klein177bb942022-05-24 13:32:27 -0600375 CompileProto(protoc_version=ProtocVersion.CHROMITE)
Alex Klein098f7982021-03-01 13:15:29 -0700376 except Error as e:
377 cros_build_lib.Die('Error compiling chromite bindings: %s', str(e))
378
379 if ProtocVersion.SDK in opts.protoc_version:
380 # Compile the SDK bindings.
381 if not cros_build_lib.IsInsideChroot():
382 # Rerun inside of the SDK instead of trying to map all of the paths.
383 cmd = [
Alex Klein177bb942022-05-24 13:32:27 -0600384 (Path(constants.CHROOT_SOURCE_ROOT) / 'chromite' / 'api' /
385 'compile_build_api_proto'),
Alex Klein098f7982021-03-01 13:15:29 -0700386 '--sdk',
387 ]
Alex Klein177bb942022-05-24 13:32:27 -0600388 result = cros_build_lib.dbg_run(cmd, enter_chroot=True, check=False)
Alex Klein098f7982021-03-01 13:15:29 -0700389 return result.returncode
390 else:
391 try:
Alex Klein177bb942022-05-24 13:32:27 -0600392 CompileProto(protoc_version=ProtocVersion.SDK)
Alex Klein098f7982021-03-01 13:15:29 -0700393 except Error as e:
394 cros_build_lib.Die('Error compiling SDK bindings: %s', str(e))