blob: 43b306c4a532a7843442f2dedb74e316d9f27dca [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 Kleinf4dc4f52018-12-05 13:55:12 -070012import os
Alex Kleinb382e4b2022-05-23 16:29:19 -060013from pathlib import Path
Sean McAllister6a5eaa02021-05-26 10:47:14 -060014import tempfile
Alex Kleindfad94c2022-05-23 16:59:47 -060015from typing import Iterable, Optional, Union
Alex Kleinf4dc4f52018-12-05 13:55:12 -070016
Alex Kleinb382e4b2022-05-23 16:29:19 -060017from chromite.lib import cipd
Alex Kleinf4dc4f52018-12-05 13:55:12 -070018from chromite.lib import commandline
Alex Kleinc33c1912019-02-15 10:29:13 -070019from chromite.lib import constants
Alex Kleinf4dc4f52018-12-05 13:55:12 -070020from chromite.lib import cros_build_lib
Sean McAllister6a5eaa02021-05-26 10:47:14 -060021from chromite.lib import git
Alex Kleinf9859972019-03-14 17:11:42 -060022from chromite.lib import osutils
23
Mike Frysinger1cc8f1f2022-04-28 22:40:40 -040024
Alex Klein098f7982021-03-01 13:15:29 -070025# Chromite's protobuf library version (third_party/google/protobuf).
Mike Nicholse366e7a2021-02-22 18:14:57 -070026PROTOC_VERSION = '3.13.0'
Alex Kleinf9859972019-03-14 17:11:42 -060027
Alex Kleinb382e4b2022-05-23 16:29:19 -060028_CIPD_PACKAGE = 'infra/tools/protoc/linux-amd64'
29_CIPD_PACKAGE_VERSION = f'protobuf_version:v{PROTOC_VERSION}'
Alex Kleinb382e4b2022-05-23 16:29:19 -060030
Alex Kleinf9859972019-03-14 17:11:42 -060031
Alex Klein5534f992019-09-16 16:31:23 -060032class Error(Exception):
33 """Base error class for the module."""
34
35
36class GenerationError(Error):
37 """A failure we can't recover from."""
38
39
Alex Klein098f7982021-03-01 13:15:29 -070040@enum.unique
41class ProtocVersion(enum.Enum):
42 """Enum for possible protoc versions."""
43 # 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()
51
Alex Kleindfad94c2022-05-23 16:59:47 -060052 def get_protoc_command(self, cipd_root: Optional[Path] = None) -> Path:
53 """Get protoc command path."""
54 assert self is ProtocVersion.SDK or cipd_root
55 if self is ProtocVersion.SDK:
56 return Path('protoc')
57 elif cipd_root:
58 return cipd_root / 'protoc'
59
Alex Klein098f7982021-03-01 13:15:29 -070060
Alex Klein851f4ee2022-03-29 16:03:45 -060061@enum.unique
62class SubdirectorySet(enum.Enum):
63 """Enum for the subsets of the proto to compile."""
64 ALL = enum.auto()
65 DEFAULT = enum.auto()
66
67 def get_source_dirs(self, source: Union[str, os.PathLike],
68 chromeos_config_path: Union[str, os.PathLike]
Alex Klein5b1bca32022-04-05 10:56:35 -060069 ) -> Union[Iterable[str], Iterable[os.PathLike]]:
Alex Klein851f4ee2022-03-29 16:03:45 -060070 """Get the directories for the given subdirectory set."""
71 _join = lambda x, y: os.path.join(x, y) if isinstance(x, str) else x / y
72 if self is self.ALL:
73 return [
74 source,
75 _join(chromeos_config_path, 'proto/chromiumos'),
76 ]
77
78 subdirs = [
Alex Klein08c70e82022-05-19 10:10:27 -060079 _join(source, 'analysis_service'),
Alex Klein851f4ee2022-03-29 16:03:45 -060080 _join(source, 'chromite'),
81 _join(source, 'chromiumos'),
82 _join(source, 'config'),
83 _join(source, 'test_platform'),
84 _join(source, 'device'),
85 _join(chromeos_config_path, 'proto/chromiumos'),
86 ]
87 return subdirs
88
89
Alex Klein098f7982021-03-01 13:15:29 -070090def _get_gen_dir(protoc_version: ProtocVersion):
91 """Get the chromite/api directory path."""
92 if protoc_version is ProtocVersion.SDK:
93 return os.path.join(constants.CHROMITE_DIR, 'api', 'gen_sdk')
94 else:
95 return os.path.join(constants.CHROMITE_DIR, 'api', 'gen')
96
97
Alex Klein098f7982021-03-01 13:15:29 -070098def _get_proto_dir(_protoc_version):
99 """Get the proto directory for the target protoc."""
100 return os.path.join(constants.CHROMITE_DIR, 'infra', 'proto')
101
102
Alex Kleindfad94c2022-05-23 16:59:47 -0600103def InstallProtoc(protoc_version: ProtocVersion) -> Path:
Alex Kleinf9859972019-03-14 17:11:42 -0600104 """Install protoc from CIPD."""
Alex Klein098f7982021-03-01 13:15:29 -0700105 if protoc_version is not ProtocVersion.CHROMITE:
Alex Kleinb382e4b2022-05-23 16:29:19 -0600106 cipd_root = None
107 else:
108 cipd_root = Path(
109 cipd.InstallPackage(cipd.GetCIPDFromCache(), _CIPD_PACKAGE,
Alex Kleindfad94c2022-05-23 16:59:47 -0600110 _CIPD_PACKAGE_VERSION))
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
Miriam Polzer713e6542021-08-17 10:58:14 +0200114def _CleanTargetDirectory(directory: str):
Alex Kleinf9859972019-03-14 17:11:42 -0600115 """Remove any existing generated files in the directory.
116
117 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.
122
123 Args:
Miriam Polzer713e6542021-08-17 10:58:14 +0200124 directory: Path to be cleaned up.
Alex Kleinf9859972019-03-14 17:11:42 -0600125 """
Alex Klein098f7982021-03-01 13:15:29 -0700126 logging.info('Cleaning old files from %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600127 for dirpath, _dirnames, filenames in os.walk(directory):
128 old = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
Alex Klein5534f992019-09-16 16:31:23 -0600129 # Remove empty init files to clean up otherwise empty directories.
130 if '__init__.py' in filenames:
131 init = os.path.join(dirpath, '__init__.py')
132 if not osutils.ReadFile(init):
133 old.append(init)
134
Alex Kleinf9859972019-03-14 17:11:42 -0600135 for current in old:
136 osutils.SafeUnlink(current)
137
Alex Klein5534f992019-09-16 16:31:23 -0600138
Alex Klein851f4ee2022-03-29 16:03:45 -0600139def _GenerateFiles(source: str, output: str, protoc_version: ProtocVersion,
Alex Kleindfad94c2022-05-23 16:59:47 -0600140 dir_subset: SubdirectorySet, protoc_bin_path: Path):
Alex Kleinf9859972019-03-14 17:11:42 -0600141 """Generate the proto files from the |source| tree into |output|.
142
143 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700144 source: Path to the proto source root directory.
145 output: Path to the output root directory.
146 protoc_version: Which protoc to use.
Alex Klein851f4ee2022-03-29 16:03:45 -0600147 dir_subset: The subset of the proto to compile.
Alex Kleinb382e4b2022-05-23 16:29:19 -0600148 protoc_bin_path: The protoc command to use.
Alex Kleinf9859972019-03-14 17:11:42 -0600149 """
Alex Klein098f7982021-03-01 13:15:29 -0700150 logging.info('Generating files to %s.', output)
151 osutils.SafeMakedirs(output)
152
Alex Kleinf9859972019-03-14 17:11:42 -0600153 targets = []
154
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600155 chromeos_config_path = os.path.realpath(
156 os.path.join(constants.SOURCE_ROOT, 'src/config'))
Alex Klein098f7982021-03-01 13:15:29 -0700157
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600158 with tempfile.TemporaryDirectory() as tempdir:
159 if not os.path.exists(chromeos_config_path):
160 chromeos_config_path = os.path.join(tempdir, 'config')
Alex Klein5534f992019-09-16 16:31:23 -0600161
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600162 logging.info('Creating shallow clone of chromiumos/config')
Alex Klein4fd378d2022-03-29 16:00:49 -0600163 git.Clone(
164 chromeos_config_path,
165 '%s/chromiumos/config' % constants.EXTERNAL_GOB_URL,
166 depth=1)
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600167
Alex Klein851f4ee2022-03-29 16:03:45 -0600168 for basedir in dir_subset.get_source_dirs(source, chromeos_config_path):
Andrew Lamb59ed32e2021-07-26 15:14:37 -0600169 for dirpath, _dirnames, filenames in os.walk(basedir):
170 for filename in filenames:
171 if filename.endswith('.proto'):
172 # We have a match, add the file.
173 targets.append(os.path.join(dirpath, filename))
174
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600175 cmd = [
Alex Kleinb382e4b2022-05-23 16:29:19 -0600176 protoc_bin_path,
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600177 '-I',
178 os.path.join(chromeos_config_path, 'proto'),
179 '--python_out',
180 output,
181 '--proto_path',
182 source,
183 ]
184 cmd.extend(targets)
185
186 result = cros_build_lib.run(
187 cmd,
188 cwd=source,
189 print_cmd=False,
190 check=False,
191 enter_chroot=protoc_version is ProtocVersion.SDK)
192
193 if result.returncode:
194 raise GenerationError('Error compiling the proto. See the output for a '
195 'message.')
Alex Kleinf9859972019-03-14 17:11:42 -0600196
197
198def _InstallMissingInits(directory):
199 """Add any __init__.py files not present in the generated protobuf folders."""
Alex Klein098f7982021-03-01 13:15:29 -0700200 logging.info('Adding missing __init__.py files in %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600201 for dirpath, _dirnames, filenames in os.walk(directory):
202 if '__init__.py' not in filenames:
203 osutils.Touch(os.path.join(dirpath, '__init__.py'))
204
205
Alex Klein098f7982021-03-01 13:15:29 -0700206def _PostprocessFiles(directory: str, protoc_version: ProtocVersion):
Alex Kleinf9859972019-03-14 17:11:42 -0600207 """Do postprocessing on the generated files.
208
209 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700210 directory: The root directory containing the generated files that are
Alex Kleinf9859972019-03-14 17:11:42 -0600211 to be processed.
Alex Klein098f7982021-03-01 13:15:29 -0700212 protoc_version: Which protoc is being used to generate the files.
Alex Kleinf9859972019-03-14 17:11:42 -0600213 """
Alex Klein098f7982021-03-01 13:15:29 -0700214 logging.info('Postprocessing: Fix imports in %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600215 # We are using a negative address here (the /address/! portion of the sed
216 # command) to make sure we don't change any imports from protobuf itself.
Alex Kleind20d8162021-06-21 12:40:44 -0600217 address = '^from google.protobuf'
Alex Kleinf9859972019-03-14 17:11:42 -0600218 # Find: 'from x import y_pb2 as x_dot_y_pb2'.
219 # "\(^google.protobuf[^ ]*\)" matches the module we're importing from.
220 # - \( and \) are for groups in sed.
221 # - ^google.protobuf prevents changing the import for protobuf's files.
222 # - [^ ] = Not a space. The [:space:] character set is too broad, but would
223 # technically work too.
224 find = r'^from \([^ ]*\) import \([^ ]*\)_pb2 as \([^ ]*\)$'
Alex Klein098f7982021-03-01 13:15:29 -0700225 # Substitute: 'from chromite.api.gen[_sdk].x import y_pb2 as x_dot_y_pb2'.
226 if protoc_version is ProtocVersion.SDK:
227 sub = 'from chromite.api.gen_sdk.\\1 import \\2_pb2 as \\3'
228 else:
229 sub = 'from chromite.api.gen.\\1 import \\2_pb2 as \\3'
230
Alex Klein5534f992019-09-16 16:31:23 -0600231 from_sed = [
232 'sed', '-i',
233 '/%(address)s/!s/%(find)s/%(sub)s/g' % {
234 'address': address,
235 'find': find,
236 'sub': sub
237 }
238 ]
Alex Kleinf9859972019-03-14 17:11:42 -0600239
Alex Kleind20d8162021-06-21 12:40:44 -0600240 seds = [from_sed]
241 if protoc_version is ProtocVersion.CHROMITE:
242 # We also need to change the google.protobuf imports to point directly
243 # at the chromite.third_party version of the library.
244 # The SDK version of the proto is meant to be used with the protobuf
245 # libraries installed in the SDK, so leave those as google.protobuf.
246 g_p_address = '^from google.protobuf'
247 g_p_find = r'from \([^ ]*\) import \(.*\)$'
248 g_p_sub = 'from chromite.third_party.\\1 import \\2'
249 google_protobuf_sed = [
250 'sed', '-i',
251 '/%(address)s/s/%(find)s/%(sub)s/g' % {
252 'address': g_p_address,
253 'find': g_p_find,
254 'sub': g_p_sub
255 }
256 ]
257 seds.append(google_protobuf_sed)
258
Alex Kleinf9859972019-03-14 17:11:42 -0600259 for dirpath, _dirnames, filenames in os.walk(directory):
Alex Klein098f7982021-03-01 13:15:29 -0700260 # Update the imports in the generated files.
Alex Kleinf9859972019-03-14 17:11:42 -0600261 pb2 = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
262 if pb2:
Alex Kleind20d8162021-06-21 12:40:44 -0600263 for sed in seds:
264 cmd = sed + pb2
265 cros_build_lib.run(cmd, print_cmd=False)
Alex Kleinf9859972019-03-14 17:11:42 -0600266
267
Alex Klein851f4ee2022-03-29 16:03:45 -0600268def CompileProto(output: str,
269 protoc_version: ProtocVersion,
Alex Klein5b1bca32022-04-05 10:56:35 -0600270 dir_subset: SubdirectorySet = SubdirectorySet.DEFAULT,
271 postprocess: bool = True):
Alex Kleinf9859972019-03-14 17:11:42 -0600272 """Compile the Build API protobuf files.
273
274 By default this will compile from infra/proto/src to api/gen. The output
275 directory may be changed, but the imports will always be treated as if it is
276 in the default location.
277
278 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700279 output: The output directory.
280 protoc_version: Which protoc to use for the compile.
Alex Klein851f4ee2022-03-29 16:03:45 -0600281 dir_subset: What proto to compile.
Alex Klein5b1bca32022-04-05 10:56:35 -0600282 postprocess: Whether to run the postprocess step.
Alex Kleinf9859972019-03-14 17:11:42 -0600283 """
Alex Klein098f7982021-03-01 13:15:29 -0700284 source = os.path.join(_get_proto_dir(protoc_version), 'src')
285 protoc_version = protoc_version or ProtocVersion.CHROMITE
Alex Kleinf9859972019-03-14 17:11:42 -0600286
Alex Kleinb382e4b2022-05-23 16:29:19 -0600287 protoc_bin_path = InstallProtoc(protoc_version)
Alex Kleinf9859972019-03-14 17:11:42 -0600288 _CleanTargetDirectory(output)
Alex Kleinb382e4b2022-05-23 16:29:19 -0600289 _GenerateFiles(source, output, protoc_version, dir_subset, protoc_bin_path)
Alex Kleinf9859972019-03-14 17:11:42 -0600290 _InstallMissingInits(output)
Alex Klein5b1bca32022-04-05 10:56:35 -0600291 if postprocess:
292 _PostprocessFiles(output, protoc_version)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700293
294
295def GetParser():
Alex Kleinf9859972019-03-14 17:11:42 -0600296 """Build the argument parser."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700297 parser = commandline.ArgumentParser(description=__doc__)
Alex Klein098f7982021-03-01 13:15:29 -0700298 standard_group = parser.add_argument_group(
299 'Committed Bindings',
300 description='Options for generating the bindings in chromite/api/.')
301 standard_group.add_argument(
302 '--chromite',
303 dest='protoc_version',
304 action='append_const',
305 const=ProtocVersion.CHROMITE,
306 help='Generate only the chromite bindings. Generates all by default. The '
Alex Klein5b1bca32022-04-05 10:56:35 -0600307 'chromite bindings are compatible with the version of protobuf in '
308 'chromite/third_party.')
Alex Klein098f7982021-03-01 13:15:29 -0700309 standard_group.add_argument(
310 '--sdk',
311 dest='protoc_version',
312 action='append_const',
313 const=ProtocVersion.SDK,
314 help='Generate only the SDK bindings. Generates all by default. The SDK '
Alex Klein5b1bca32022-04-05 10:56:35 -0600315 'bindings are compiled by protoc in the SDK, and is compatible '
316 'with the version of protobuf in the SDK (i.e. the one installed by '
317 'the ebuild).')
Alex Klein098f7982021-03-01 13:15:29 -0700318
319 dest_group = parser.add_argument_group(
320 'Out of Tree Bindings',
321 description='Options for generating bindings in a custom location.')
322 dest_group.add_argument(
Alex Klein5534f992019-09-16 16:31:23 -0600323 '--destination',
324 type='path',
Alex Klein098f7982021-03-01 13:15:29 -0700325 help='A directory where a single version of the proto should be '
Alex Klein5b1bca32022-04-05 10:56:35 -0600326 'generated. When not given, the proto generates in all default '
327 'locations instead.')
Alex Klein098f7982021-03-01 13:15:29 -0700328 dest_group.add_argument(
329 '--dest-sdk',
330 action='store_const',
331 dest='dest_protoc',
332 default=ProtocVersion.CHROMITE,
333 const=ProtocVersion.SDK,
334 help='Generate the SDK version of the protos in --destination instead of '
Alex Klein5b1bca32022-04-05 10:56:35 -0600335 'the chromite version.')
Alex Klein851f4ee2022-03-29 16:03:45 -0600336 dest_group.add_argument(
337 '--all-proto',
338 action='store_const',
339 dest='dir_subset',
340 default=SubdirectorySet.DEFAULT,
341 const=SubdirectorySet.ALL,
342 help='Compile ALL proto instead of just the subset needed for the API. '
Alex Klein5b1bca32022-04-05 10:56:35 -0600343 'Only considered when generating out of tree bindings.')
344 dest_group.add_argument(
345 '--skip-postprocessing',
346 action='store_false',
347 dest='postprocess',
348 default=True,
349 help='Skip postprocessing files.'
350 )
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700351 return parser
352
353
354def _ParseArguments(argv):
355 """Parse and validate arguments."""
356 parser = GetParser()
357 opts = parser.parse_args(argv)
358
Alex Klein098f7982021-03-01 13:15:29 -0700359 if not opts.protoc_version:
360 opts.protoc_version = [ProtocVersion.CHROMITE, ProtocVersion.SDK]
361
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700362 opts.Freeze()
363 return opts
364
365
366def main(argv):
Alex Kleinf9859972019-03-14 17:11:42 -0600367 opts = _ParseArguments(argv)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700368
Alex Klein098f7982021-03-01 13:15:29 -0700369 if opts.destination:
370 # Destination set, only compile a single version in the destination.
371 try:
Alex Klein851f4ee2022-03-29 16:03:45 -0600372 CompileProto(
373 output=opts.destination,
374 protoc_version=opts.dest_protoc,
Alex Klein5b1bca32022-04-05 10:56:35 -0600375 dir_subset=opts.dir_subset,
376 postprocess=opts.postprocess
377 )
Alex Klein098f7982021-03-01 13:15:29 -0700378 except Error as e:
379 cros_build_lib.Die('Error compiling bindings to destination: %s', str(e))
380 else:
381 return 0
382
383 if ProtocVersion.CHROMITE in opts.protoc_version:
384 # Compile the chromite bindings.
385 try:
386 CompileProto(
387 output=_get_gen_dir(ProtocVersion.CHROMITE),
388 protoc_version=ProtocVersion.CHROMITE)
389 except Error as e:
390 cros_build_lib.Die('Error compiling chromite bindings: %s', str(e))
391
392 if ProtocVersion.SDK in opts.protoc_version:
393 # Compile the SDK bindings.
394 if not cros_build_lib.IsInsideChroot():
395 # Rerun inside of the SDK instead of trying to map all of the paths.
396 cmd = [
397 os.path.join(constants.CHROOT_SOURCE_ROOT, 'chromite', 'api',
398 'compile_build_api_proto'),
399 '--sdk',
400 ]
401 result = cros_build_lib.run(
402 cmd, print_cmd=False, enter_chroot=True, check=False)
403 return result.returncode
404 else:
405 try:
406 CompileProto(
407 output=_get_gen_dir(ProtocVersion.SDK),
408 protoc_version=ProtocVersion.SDK)
409 except Error as e:
410 cros_build_lib.Die('Error compiling SDK bindings: %s', str(e))