blob: d9da0709ef17d5f91d1276d2a3b86a25e9f8ecaf [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 Klein851f4ee2022-03-29 16:03:45 -060015from typing import Iterable, 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
Alex Kleinb382e4b2022-05-23 16:29:19 -060023from chromite.lib import path_util
Alex Kleinf9859972019-03-14 17:11:42 -060024
Mike Frysinger1cc8f1f2022-04-28 22:40:40 -040025
Alex Klein098f7982021-03-01 13:15:29 -070026# Chromite's protobuf library version (third_party/google/protobuf).
Mike Nicholse366e7a2021-02-22 18:14:57 -070027PROTOC_VERSION = '3.13.0'
Alex Kleinf9859972019-03-14 17:11:42 -060028
Alex Kleinb382e4b2022-05-23 16:29:19 -060029_CIPD_PACKAGE = 'infra/tools/protoc/linux-amd64'
30_CIPD_PACKAGE_VERSION = f'protobuf_version:v{PROTOC_VERSION}'
31_CIPD_DESINATION = (
32 Path(path_util.GetCacheDir()).absolute() / 'cipd' / 'packages')
33
Alex Kleinf9859972019-03-14 17:11:42 -060034
Alex Klein5534f992019-09-16 16:31:23 -060035class Error(Exception):
36 """Base error class for the module."""
37
38
39class GenerationError(Error):
40 """A failure we can't recover from."""
41
42
Alex Klein098f7982021-03-01 13:15:29 -070043@enum.unique
44class ProtocVersion(enum.Enum):
45 """Enum for possible protoc versions."""
46 # The SDK version of the bindings use the protoc in the SDK, and so is
47 # compatible with the protobuf library in the SDK, i.e. the one installed
48 # via the ebuild.
49 SDK = enum.auto()
50 # The Chromite version of the bindings uses a protoc binary downloaded from
51 # CIPD that matches the version of the protobuf library in
52 # chromite/third_party/google/protobuf.
53 CHROMITE = enum.auto()
54
55
Alex Klein851f4ee2022-03-29 16:03:45 -060056@enum.unique
57class SubdirectorySet(enum.Enum):
58 """Enum for the subsets of the proto to compile."""
59 ALL = enum.auto()
60 DEFAULT = enum.auto()
61
62 def get_source_dirs(self, source: Union[str, os.PathLike],
63 chromeos_config_path: Union[str, os.PathLike]
Alex Klein5b1bca32022-04-05 10:56:35 -060064 ) -> Union[Iterable[str], Iterable[os.PathLike]]:
Alex Klein851f4ee2022-03-29 16:03:45 -060065 """Get the directories for the given subdirectory set."""
66 _join = lambda x, y: os.path.join(x, y) if isinstance(x, str) else x / y
67 if self is self.ALL:
68 return [
69 source,
70 _join(chromeos_config_path, 'proto/chromiumos'),
71 ]
72
73 subdirs = [
Alex Klein08c70e82022-05-19 10:10:27 -060074 _join(source, 'analysis_service'),
Alex Klein851f4ee2022-03-29 16:03:45 -060075 _join(source, 'chromite'),
76 _join(source, 'chromiumos'),
77 _join(source, 'config'),
78 _join(source, 'test_platform'),
79 _join(source, 'device'),
80 _join(chromeos_config_path, 'proto/chromiumos'),
81 ]
82 return subdirs
83
84
Alex Klein098f7982021-03-01 13:15:29 -070085def _get_gen_dir(protoc_version: ProtocVersion):
86 """Get the chromite/api directory path."""
87 if protoc_version is ProtocVersion.SDK:
88 return os.path.join(constants.CHROMITE_DIR, 'api', 'gen_sdk')
89 else:
90 return os.path.join(constants.CHROMITE_DIR, 'api', 'gen')
91
92
Alex Kleinb382e4b2022-05-23 16:29:19 -060093def _get_protoc_command(protoc_version: ProtocVersion, cipd_root: Path = None):
Alex Klein098f7982021-03-01 13:15:29 -070094 """Get the protoc command for the target protoc."""
95 if protoc_version is ProtocVersion.SDK:
96 return 'protoc'
Alex Kleinb382e4b2022-05-23 16:29:19 -060097 elif cipd_root:
98 return str(cipd_root / 'protoc')
Alex Klein098f7982021-03-01 13:15:29 -070099
100
101def _get_proto_dir(_protoc_version):
102 """Get the proto directory for the target protoc."""
103 return os.path.join(constants.CHROMITE_DIR, 'infra', 'proto')
104
105
Alex Kleinb382e4b2022-05-23 16:29:19 -0600106def InstallProtoc(protoc_version: ProtocVersion) -> str:
Alex Kleinf9859972019-03-14 17:11:42 -0600107 """Install protoc from CIPD."""
Alex Klein098f7982021-03-01 13:15:29 -0700108 if protoc_version is not ProtocVersion.CHROMITE:
Alex Kleinb382e4b2022-05-23 16:29:19 -0600109 cipd_root = None
110 else:
111 cipd_root = Path(
112 cipd.InstallPackage(cipd.GetCIPDFromCache(), _CIPD_PACKAGE,
113 _CIPD_PACKAGE_VERSION, _CIPD_DESINATION))
114 return _get_protoc_command(protoc_version, cipd_root)
Alex Klein5534f992019-09-16 16:31:23 -0600115
Alex Kleinf9859972019-03-14 17:11:42 -0600116
Miriam Polzer713e6542021-08-17 10:58:14 +0200117def _CleanTargetDirectory(directory: str):
Alex Kleinf9859972019-03-14 17:11:42 -0600118 """Remove any existing generated files in the directory.
119
120 This clean only removes the generated files to avoid accidentally destroying
121 __init__.py customizations down the line. That will leave otherwise empty
122 directories in place if things get moved. Neither case is relevant at the
123 time of writing, but lingering empty directories seemed better than
124 diagnosing accidental __init__.py changes.
125
126 Args:
Miriam Polzer713e6542021-08-17 10:58:14 +0200127 directory: Path to be cleaned up.
Alex Kleinf9859972019-03-14 17:11:42 -0600128 """
Alex Klein098f7982021-03-01 13:15:29 -0700129 logging.info('Cleaning old files from %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600130 for dirpath, _dirnames, filenames in os.walk(directory):
131 old = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
Alex Klein5534f992019-09-16 16:31:23 -0600132 # Remove empty init files to clean up otherwise empty directories.
133 if '__init__.py' in filenames:
134 init = os.path.join(dirpath, '__init__.py')
135 if not osutils.ReadFile(init):
136 old.append(init)
137
Alex Kleinf9859972019-03-14 17:11:42 -0600138 for current in old:
139 osutils.SafeUnlink(current)
140
Alex Klein5534f992019-09-16 16:31:23 -0600141
Alex Klein851f4ee2022-03-29 16:03:45 -0600142def _GenerateFiles(source: str, output: str, protoc_version: ProtocVersion,
Alex Kleinb382e4b2022-05-23 16:29:19 -0600143 dir_subset: SubdirectorySet, protoc_bin_path: str):
Alex Kleinf9859972019-03-14 17:11:42 -0600144 """Generate the proto files from the |source| tree into |output|.
145
146 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700147 source: Path to the proto source root directory.
148 output: Path to the output root directory.
149 protoc_version: Which protoc to use.
Alex Klein851f4ee2022-03-29 16:03:45 -0600150 dir_subset: The subset of the proto to compile.
Alex Kleinb382e4b2022-05-23 16:29:19 -0600151 protoc_bin_path: The protoc command to use.
Alex Kleinf9859972019-03-14 17:11:42 -0600152 """
Alex Klein098f7982021-03-01 13:15:29 -0700153 logging.info('Generating files to %s.', output)
154 osutils.SafeMakedirs(output)
155
Alex Kleinf9859972019-03-14 17:11:42 -0600156 targets = []
157
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600158 chromeos_config_path = os.path.realpath(
159 os.path.join(constants.SOURCE_ROOT, 'src/config'))
Alex Klein098f7982021-03-01 13:15:29 -0700160
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600161 with tempfile.TemporaryDirectory() as tempdir:
162 if not os.path.exists(chromeos_config_path):
163 chromeos_config_path = os.path.join(tempdir, 'config')
Alex Klein5534f992019-09-16 16:31:23 -0600164
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600165 logging.info('Creating shallow clone of chromiumos/config')
Alex Klein4fd378d2022-03-29 16:00:49 -0600166 git.Clone(
167 chromeos_config_path,
168 '%s/chromiumos/config' % constants.EXTERNAL_GOB_URL,
169 depth=1)
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600170
Alex Klein851f4ee2022-03-29 16:03:45 -0600171 for basedir in dir_subset.get_source_dirs(source, chromeos_config_path):
Andrew Lamb59ed32e2021-07-26 15:14:37 -0600172 for dirpath, _dirnames, filenames in os.walk(basedir):
173 for filename in filenames:
174 if filename.endswith('.proto'):
175 # We have a match, add the file.
176 targets.append(os.path.join(dirpath, filename))
177
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600178 cmd = [
Alex Kleinb382e4b2022-05-23 16:29:19 -0600179 protoc_bin_path,
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600180 '-I',
181 os.path.join(chromeos_config_path, 'proto'),
182 '--python_out',
183 output,
184 '--proto_path',
185 source,
186 ]
187 cmd.extend(targets)
188
189 result = cros_build_lib.run(
190 cmd,
191 cwd=source,
192 print_cmd=False,
193 check=False,
194 enter_chroot=protoc_version is ProtocVersion.SDK)
195
196 if result.returncode:
197 raise GenerationError('Error compiling the proto. See the output for a '
198 'message.')
Alex Kleinf9859972019-03-14 17:11:42 -0600199
200
201def _InstallMissingInits(directory):
202 """Add any __init__.py files not present in the generated protobuf folders."""
Alex Klein098f7982021-03-01 13:15:29 -0700203 logging.info('Adding missing __init__.py files in %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600204 for dirpath, _dirnames, filenames in os.walk(directory):
205 if '__init__.py' not in filenames:
206 osutils.Touch(os.path.join(dirpath, '__init__.py'))
207
208
Alex Klein098f7982021-03-01 13:15:29 -0700209def _PostprocessFiles(directory: str, protoc_version: ProtocVersion):
Alex Kleinf9859972019-03-14 17:11:42 -0600210 """Do postprocessing on the generated files.
211
212 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700213 directory: The root directory containing the generated files that are
Alex Kleinf9859972019-03-14 17:11:42 -0600214 to be processed.
Alex Klein098f7982021-03-01 13:15:29 -0700215 protoc_version: Which protoc is being used to generate the files.
Alex Kleinf9859972019-03-14 17:11:42 -0600216 """
Alex Klein098f7982021-03-01 13:15:29 -0700217 logging.info('Postprocessing: Fix imports in %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600218 # 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.
Alex Kleind20d8162021-06-21 12:40:44 -0600220 address = '^from google.protobuf'
Alex Kleinf9859972019-03-14 17:11:42 -0600221 # 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.
225 # - [^ ] = Not a space. The [:space:] character set is too broad, but would
226 # technically work too.
227 find = r'^from \([^ ]*\) import \([^ ]*\)_pb2 as \([^ ]*\)$'
Alex Klein098f7982021-03-01 13:15:29 -0700228 # 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'
233
Alex Klein5534f992019-09-16 16:31:23 -0600234 from_sed = [
235 'sed', '-i',
236 '/%(address)s/!s/%(find)s/%(sub)s/g' % {
237 'address': address,
238 'find': find,
239 'sub': sub
240 }
241 ]
Alex Kleinf9859972019-03-14 17:11:42 -0600242
Alex Kleind20d8162021-06-21 12:40:44 -0600243 seds = [from_sed]
244 if protoc_version is ProtocVersion.CHROMITE:
245 # We also need to change the google.protobuf imports to point directly
246 # at the chromite.third_party version of the library.
247 # The SDK version of the proto is meant to be used with the protobuf
248 # libraries installed in the SDK, so leave those as google.protobuf.
249 g_p_address = '^from google.protobuf'
250 g_p_find = r'from \([^ ]*\) import \(.*\)$'
251 g_p_sub = 'from chromite.third_party.\\1 import \\2'
252 google_protobuf_sed = [
253 'sed', '-i',
254 '/%(address)s/s/%(find)s/%(sub)s/g' % {
255 'address': g_p_address,
256 'find': g_p_find,
257 'sub': g_p_sub
258 }
259 ]
260 seds.append(google_protobuf_sed)
261
Alex Kleinf9859972019-03-14 17:11:42 -0600262 for dirpath, _dirnames, filenames in os.walk(directory):
Alex Klein098f7982021-03-01 13:15:29 -0700263 # Update the imports in the generated files.
Alex Kleinf9859972019-03-14 17:11:42 -0600264 pb2 = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
265 if pb2:
Alex Kleind20d8162021-06-21 12:40:44 -0600266 for sed in seds:
267 cmd = sed + pb2
268 cros_build_lib.run(cmd, print_cmd=False)
Alex Kleinf9859972019-03-14 17:11:42 -0600269
270
Alex Klein851f4ee2022-03-29 16:03:45 -0600271def CompileProto(output: str,
272 protoc_version: ProtocVersion,
Alex Klein5b1bca32022-04-05 10:56:35 -0600273 dir_subset: SubdirectorySet = SubdirectorySet.DEFAULT,
274 postprocess: bool = True):
Alex Kleinf9859972019-03-14 17:11:42 -0600275 """Compile the Build API protobuf files.
276
277 By default this will compile from infra/proto/src to api/gen. The output
278 directory may be changed, but the imports will always be treated as if it is
279 in the default location.
280
281 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700282 output: The output directory.
283 protoc_version: Which protoc to use for the compile.
Alex Klein851f4ee2022-03-29 16:03:45 -0600284 dir_subset: What proto to compile.
Alex Klein5b1bca32022-04-05 10:56:35 -0600285 postprocess: Whether to run the postprocess step.
Alex Kleinf9859972019-03-14 17:11:42 -0600286 """
Alex Klein098f7982021-03-01 13:15:29 -0700287 source = os.path.join(_get_proto_dir(protoc_version), 'src')
288 protoc_version = protoc_version or ProtocVersion.CHROMITE
Alex Kleinf9859972019-03-14 17:11:42 -0600289
Alex Kleinb382e4b2022-05-23 16:29:19 -0600290 protoc_bin_path = InstallProtoc(protoc_version)
Alex Kleinf9859972019-03-14 17:11:42 -0600291 _CleanTargetDirectory(output)
Alex Kleinb382e4b2022-05-23 16:29:19 -0600292 _GenerateFiles(source, output, protoc_version, dir_subset, protoc_bin_path)
Alex Kleinf9859972019-03-14 17:11:42 -0600293 _InstallMissingInits(output)
Alex Klein5b1bca32022-04-05 10:56:35 -0600294 if postprocess:
295 _PostprocessFiles(output, protoc_version)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700296
297
298def GetParser():
Alex Kleinf9859972019-03-14 17:11:42 -0600299 """Build the argument parser."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700300 parser = commandline.ArgumentParser(description=__doc__)
Alex Klein098f7982021-03-01 13:15:29 -0700301 standard_group = parser.add_argument_group(
302 'Committed Bindings',
303 description='Options for generating the bindings in chromite/api/.')
304 standard_group.add_argument(
305 '--chromite',
306 dest='protoc_version',
307 action='append_const',
308 const=ProtocVersion.CHROMITE,
309 help='Generate only the chromite bindings. Generates all by default. The '
Alex Klein5b1bca32022-04-05 10:56:35 -0600310 'chromite bindings are compatible with the version of protobuf in '
311 'chromite/third_party.')
Alex Klein098f7982021-03-01 13:15:29 -0700312 standard_group.add_argument(
313 '--sdk',
314 dest='protoc_version',
315 action='append_const',
316 const=ProtocVersion.SDK,
317 help='Generate only the SDK bindings. Generates all by default. The SDK '
Alex Klein5b1bca32022-04-05 10:56:35 -0600318 'bindings are compiled by protoc in the SDK, and is compatible '
319 'with the version of protobuf in the SDK (i.e. the one installed by '
320 'the ebuild).')
Alex Klein098f7982021-03-01 13:15:29 -0700321
322 dest_group = parser.add_argument_group(
323 'Out of Tree Bindings',
324 description='Options for generating bindings in a custom location.')
325 dest_group.add_argument(
Alex Klein5534f992019-09-16 16:31:23 -0600326 '--destination',
327 type='path',
Alex Klein098f7982021-03-01 13:15:29 -0700328 help='A directory where a single version of the proto should be '
Alex Klein5b1bca32022-04-05 10:56:35 -0600329 'generated. When not given, the proto generates in all default '
330 'locations instead.')
Alex Klein098f7982021-03-01 13:15:29 -0700331 dest_group.add_argument(
332 '--dest-sdk',
333 action='store_const',
334 dest='dest_protoc',
335 default=ProtocVersion.CHROMITE,
336 const=ProtocVersion.SDK,
337 help='Generate the SDK version of the protos in --destination instead of '
Alex Klein5b1bca32022-04-05 10:56:35 -0600338 'the chromite version.')
Alex Klein851f4ee2022-03-29 16:03:45 -0600339 dest_group.add_argument(
340 '--all-proto',
341 action='store_const',
342 dest='dir_subset',
343 default=SubdirectorySet.DEFAULT,
344 const=SubdirectorySet.ALL,
345 help='Compile ALL proto instead of just the subset needed for the API. '
Alex Klein5b1bca32022-04-05 10:56:35 -0600346 'Only considered when generating out of tree bindings.')
347 dest_group.add_argument(
348 '--skip-postprocessing',
349 action='store_false',
350 dest='postprocess',
351 default=True,
352 help='Skip postprocessing files.'
353 )
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700354 return parser
355
356
357def _ParseArguments(argv):
358 """Parse and validate arguments."""
359 parser = GetParser()
360 opts = parser.parse_args(argv)
361
Alex Klein098f7982021-03-01 13:15:29 -0700362 if not opts.protoc_version:
363 opts.protoc_version = [ProtocVersion.CHROMITE, ProtocVersion.SDK]
364
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700365 opts.Freeze()
366 return opts
367
368
369def main(argv):
Alex Kleinf9859972019-03-14 17:11:42 -0600370 opts = _ParseArguments(argv)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700371
Alex Klein098f7982021-03-01 13:15:29 -0700372 if opts.destination:
373 # Destination set, only compile a single version in the destination.
374 try:
Alex Klein851f4ee2022-03-29 16:03:45 -0600375 CompileProto(
376 output=opts.destination,
377 protoc_version=opts.dest_protoc,
Alex Klein5b1bca32022-04-05 10:56:35 -0600378 dir_subset=opts.dir_subset,
379 postprocess=opts.postprocess
380 )
Alex Klein098f7982021-03-01 13:15:29 -0700381 except Error as e:
382 cros_build_lib.Die('Error compiling bindings to destination: %s', str(e))
383 else:
384 return 0
385
386 if ProtocVersion.CHROMITE in opts.protoc_version:
387 # Compile the chromite bindings.
388 try:
389 CompileProto(
390 output=_get_gen_dir(ProtocVersion.CHROMITE),
391 protoc_version=ProtocVersion.CHROMITE)
392 except Error as e:
393 cros_build_lib.Die('Error compiling chromite bindings: %s', str(e))
394
395 if ProtocVersion.SDK in opts.protoc_version:
396 # Compile the SDK bindings.
397 if not cros_build_lib.IsInsideChroot():
398 # Rerun inside of the SDK instead of trying to map all of the paths.
399 cmd = [
400 os.path.join(constants.CHROOT_SOURCE_ROOT, 'chromite', 'api',
401 'compile_build_api_proto'),
402 '--sdk',
403 ]
404 result = cros_build_lib.run(
405 cmd, print_cmd=False, enter_chroot=True, check=False)
406 return result.returncode
407 else:
408 try:
409 CompileProto(
410 output=_get_gen_dir(ProtocVersion.SDK),
411 protoc_version=ProtocVersion.SDK)
412 except Error as e:
413 cros_build_lib.Die('Error compiling SDK bindings: %s', str(e))