blob: a8ca4c7e0e0bccf63033b54119b4c1f053968c34 [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
Sean McAllister6a5eaa02021-05-26 10:47:14 -060013import tempfile
Alex Klein851f4ee2022-03-29 16:03:45 -060014from typing import Iterable, Union
Alex Kleinf4dc4f52018-12-05 13:55:12 -070015
16from chromite.lib import commandline
Alex Kleinc33c1912019-02-15 10:29:13 -070017from chromite.lib import constants
Alex Kleinf4dc4f52018-12-05 13:55:12 -070018from chromite.lib import cros_build_lib
Sean McAllister6a5eaa02021-05-26 10:47:14 -060019from chromite.lib import git
Alex Kleinf9859972019-03-14 17:11:42 -060020from chromite.lib import osutils
21
Alex Kleinf9859972019-03-14 17:11:42 -060022_CIPD_ROOT = os.path.join(constants.CHROMITE_DIR, '.cipd_bin')
Alex Kleinf9859972019-03-14 17:11:42 -060023
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
27
Alex Klein5534f992019-09-16 16:31:23 -060028class Error(Exception):
29 """Base error class for the module."""
30
31
32class GenerationError(Error):
33 """A failure we can't recover from."""
34
35
Alex Klein098f7982021-03-01 13:15:29 -070036@enum.unique
37class ProtocVersion(enum.Enum):
38 """Enum for possible protoc versions."""
39 # The SDK version of the bindings use the protoc in the SDK, and so is
40 # compatible with the protobuf library in the SDK, i.e. the one installed
41 # via the ebuild.
42 SDK = enum.auto()
43 # The Chromite version of the bindings uses a protoc binary downloaded from
44 # CIPD that matches the version of the protobuf library in
45 # chromite/third_party/google/protobuf.
46 CHROMITE = enum.auto()
47
48
Alex Klein851f4ee2022-03-29 16:03:45 -060049@enum.unique
50class SubdirectorySet(enum.Enum):
51 """Enum for the subsets of the proto to compile."""
52 ALL = enum.auto()
53 DEFAULT = enum.auto()
54
55 def get_source_dirs(self, source: Union[str, os.PathLike],
56 chromeos_config_path: Union[str, os.PathLike]
Alex Klein5b1bca32022-04-05 10:56:35 -060057 ) -> Union[Iterable[str], Iterable[os.PathLike]]:
Alex Klein851f4ee2022-03-29 16:03:45 -060058 """Get the directories for the given subdirectory set."""
59 _join = lambda x, y: os.path.join(x, y) if isinstance(x, str) else x / y
60 if self is self.ALL:
61 return [
62 source,
63 _join(chromeos_config_path, 'proto/chromiumos'),
64 ]
65
66 subdirs = [
67 _join(source, 'chromite'),
68 _join(source, 'chromiumos'),
69 _join(source, 'config'),
70 _join(source, 'test_platform'),
71 _join(source, 'device'),
72 _join(chromeos_config_path, 'proto/chromiumos'),
73 ]
74 return subdirs
75
76
Alex Klein098f7982021-03-01 13:15:29 -070077def _get_gen_dir(protoc_version: ProtocVersion):
78 """Get the chromite/api directory path."""
79 if protoc_version is ProtocVersion.SDK:
80 return os.path.join(constants.CHROMITE_DIR, 'api', 'gen_sdk')
81 else:
82 return os.path.join(constants.CHROMITE_DIR, 'api', 'gen')
83
84
85def _get_protoc_command(protoc_version: ProtocVersion):
86 """Get the protoc command for the target protoc."""
87 if protoc_version is ProtocVersion.SDK:
88 return 'protoc'
89 else:
90 return os.path.join(_CIPD_ROOT, 'protoc')
91
92
93def _get_proto_dir(_protoc_version):
94 """Get the proto directory for the target protoc."""
95 return os.path.join(constants.CHROMITE_DIR, 'infra', 'proto')
96
97
98def _InstallProtoc(protoc_version: ProtocVersion):
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:
101 return
102
Alex Klein5534f992019-09-16 16:31:23 -0600103 logging.info('Installing protoc.')
Alex Kleinf9859972019-03-14 17:11:42 -0600104 cmd = ['cipd', 'ensure']
105 # Clean up the output.
106 cmd.extend(['-log-level', 'warning'])
107 # Set the install location.
108 cmd.extend(['-root', _CIPD_ROOT])
109
110 ensure_content = ('infra/tools/protoc/${platform} '
111 'protobuf_version:v%s' % PROTOC_VERSION)
112 with osutils.TempDir() as tempdir:
113 ensure_file = os.path.join(tempdir, 'cipd_ensure_file')
114 osutils.WriteFile(ensure_file, ensure_content)
115
116 cmd.extend(['-ensure-file', ensure_file])
117
Mike Frysinger45602c72019-09-22 02:15:11 -0400118 cros_build_lib.run(cmd, cwd=constants.CHROMITE_DIR, print_cmd=False)
Alex Klein5534f992019-09-16 16:31:23 -0600119
Alex Kleinf9859972019-03-14 17:11:42 -0600120
Miriam Polzer713e6542021-08-17 10:58:14 +0200121def _CleanTargetDirectory(directory: str):
Alex Kleinf9859972019-03-14 17:11:42 -0600122 """Remove any existing generated files in the directory.
123
124 This clean only removes the generated files to avoid accidentally destroying
125 __init__.py customizations down the line. That will leave otherwise empty
126 directories in place if things get moved. Neither case is relevant at the
127 time of writing, but lingering empty directories seemed better than
128 diagnosing accidental __init__.py changes.
129
130 Args:
Miriam Polzer713e6542021-08-17 10:58:14 +0200131 directory: Path to be cleaned up.
Alex Kleinf9859972019-03-14 17:11:42 -0600132 """
Alex Klein098f7982021-03-01 13:15:29 -0700133 logging.info('Cleaning old files from %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600134 for dirpath, _dirnames, filenames in os.walk(directory):
135 old = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
Alex Klein5534f992019-09-16 16:31:23 -0600136 # Remove empty init files to clean up otherwise empty directories.
137 if '__init__.py' in filenames:
138 init = os.path.join(dirpath, '__init__.py')
139 if not osutils.ReadFile(init):
140 old.append(init)
141
Alex Kleinf9859972019-03-14 17:11:42 -0600142 for current in old:
143 osutils.SafeUnlink(current)
144
Alex Klein5534f992019-09-16 16:31:23 -0600145
Alex Klein851f4ee2022-03-29 16:03:45 -0600146def _GenerateFiles(source: str, output: str, protoc_version: ProtocVersion,
147 dir_subset: SubdirectorySet):
Alex Kleinf9859972019-03-14 17:11:42 -0600148 """Generate the proto files from the |source| tree into |output|.
149
150 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700151 source: Path to the proto source root directory.
152 output: Path to the output root directory.
153 protoc_version: Which protoc to use.
Alex Klein851f4ee2022-03-29 16:03:45 -0600154 dir_subset: The subset of the proto to compile.
Alex Kleinf9859972019-03-14 17:11:42 -0600155 """
Alex Klein098f7982021-03-01 13:15:29 -0700156 logging.info('Generating files to %s.', output)
157 osutils.SafeMakedirs(output)
158
Alex Kleinf9859972019-03-14 17:11:42 -0600159 targets = []
160
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600161 chromeos_config_path = os.path.realpath(
162 os.path.join(constants.SOURCE_ROOT, 'src/config'))
Alex Klein098f7982021-03-01 13:15:29 -0700163
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600164 with tempfile.TemporaryDirectory() as tempdir:
165 if not os.path.exists(chromeos_config_path):
166 chromeos_config_path = os.path.join(tempdir, 'config')
Alex Klein5534f992019-09-16 16:31:23 -0600167
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600168 logging.info('Creating shallow clone of chromiumos/config')
Alex Klein4fd378d2022-03-29 16:00:49 -0600169 git.Clone(
170 chromeos_config_path,
171 '%s/chromiumos/config' % constants.EXTERNAL_GOB_URL,
172 depth=1)
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600173
Alex Klein851f4ee2022-03-29 16:03:45 -0600174 for basedir in dir_subset.get_source_dirs(source, chromeos_config_path):
Andrew Lamb59ed32e2021-07-26 15:14:37 -0600175 for dirpath, _dirnames, filenames in os.walk(basedir):
176 for filename in filenames:
177 if filename.endswith('.proto'):
178 # We have a match, add the file.
179 targets.append(os.path.join(dirpath, filename))
180
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600181 cmd = [
182 _get_protoc_command(protoc_version),
183 '-I',
184 os.path.join(chromeos_config_path, 'proto'),
185 '--python_out',
186 output,
187 '--proto_path',
188 source,
189 ]
190 cmd.extend(targets)
191
192 result = cros_build_lib.run(
193 cmd,
194 cwd=source,
195 print_cmd=False,
196 check=False,
197 enter_chroot=protoc_version is ProtocVersion.SDK)
198
199 if result.returncode:
200 raise GenerationError('Error compiling the proto. See the output for a '
201 'message.')
Alex Kleinf9859972019-03-14 17:11:42 -0600202
203
204def _InstallMissingInits(directory):
205 """Add any __init__.py files not present in the generated protobuf folders."""
Alex Klein098f7982021-03-01 13:15:29 -0700206 logging.info('Adding missing __init__.py files in %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600207 for dirpath, _dirnames, filenames in os.walk(directory):
208 if '__init__.py' not in filenames:
209 osutils.Touch(os.path.join(dirpath, '__init__.py'))
210
211
Alex Klein098f7982021-03-01 13:15:29 -0700212def _PostprocessFiles(directory: str, protoc_version: ProtocVersion):
Alex Kleinf9859972019-03-14 17:11:42 -0600213 """Do postprocessing on the generated files.
214
215 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700216 directory: The root directory containing the generated files that are
Alex Kleinf9859972019-03-14 17:11:42 -0600217 to be processed.
Alex Klein098f7982021-03-01 13:15:29 -0700218 protoc_version: Which protoc is being used to generate the files.
Alex Kleinf9859972019-03-14 17:11:42 -0600219 """
Alex Klein098f7982021-03-01 13:15:29 -0700220 logging.info('Postprocessing: Fix imports in %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600221 # We are using a negative address here (the /address/! portion of the sed
222 # command) to make sure we don't change any imports from protobuf itself.
Alex Kleind20d8162021-06-21 12:40:44 -0600223 address = '^from google.protobuf'
Alex Kleinf9859972019-03-14 17:11:42 -0600224 # Find: 'from x import y_pb2 as x_dot_y_pb2'.
225 # "\(^google.protobuf[^ ]*\)" matches the module we're importing from.
226 # - \( and \) are for groups in sed.
227 # - ^google.protobuf prevents changing the import for protobuf's files.
228 # - [^ ] = Not a space. The [:space:] character set is too broad, but would
229 # technically work too.
230 find = r'^from \([^ ]*\) import \([^ ]*\)_pb2 as \([^ ]*\)$'
Alex Klein098f7982021-03-01 13:15:29 -0700231 # Substitute: 'from chromite.api.gen[_sdk].x import y_pb2 as x_dot_y_pb2'.
232 if protoc_version is ProtocVersion.SDK:
233 sub = 'from chromite.api.gen_sdk.\\1 import \\2_pb2 as \\3'
234 else:
235 sub = 'from chromite.api.gen.\\1 import \\2_pb2 as \\3'
236
Alex Klein5534f992019-09-16 16:31:23 -0600237 from_sed = [
238 'sed', '-i',
239 '/%(address)s/!s/%(find)s/%(sub)s/g' % {
240 'address': address,
241 'find': find,
242 'sub': sub
243 }
244 ]
Alex Kleinf9859972019-03-14 17:11:42 -0600245
Alex Kleind20d8162021-06-21 12:40:44 -0600246 seds = [from_sed]
247 if protoc_version is ProtocVersion.CHROMITE:
248 # We also need to change the google.protobuf imports to point directly
249 # at the chromite.third_party version of the library.
250 # The SDK version of the proto is meant to be used with the protobuf
251 # libraries installed in the SDK, so leave those as google.protobuf.
252 g_p_address = '^from google.protobuf'
253 g_p_find = r'from \([^ ]*\) import \(.*\)$'
254 g_p_sub = 'from chromite.third_party.\\1 import \\2'
255 google_protobuf_sed = [
256 'sed', '-i',
257 '/%(address)s/s/%(find)s/%(sub)s/g' % {
258 'address': g_p_address,
259 'find': g_p_find,
260 'sub': g_p_sub
261 }
262 ]
263 seds.append(google_protobuf_sed)
264
Alex Kleinf9859972019-03-14 17:11:42 -0600265 for dirpath, _dirnames, filenames in os.walk(directory):
Alex Klein098f7982021-03-01 13:15:29 -0700266 # Update the imports in the generated files.
Alex Kleinf9859972019-03-14 17:11:42 -0600267 pb2 = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
268 if pb2:
Alex Kleind20d8162021-06-21 12:40:44 -0600269 for sed in seds:
270 cmd = sed + pb2
271 cros_build_lib.run(cmd, print_cmd=False)
Alex Kleinf9859972019-03-14 17:11:42 -0600272
273
Alex Klein851f4ee2022-03-29 16:03:45 -0600274def CompileProto(output: str,
275 protoc_version: ProtocVersion,
Alex Klein5b1bca32022-04-05 10:56:35 -0600276 dir_subset: SubdirectorySet = SubdirectorySet.DEFAULT,
277 postprocess: bool = True):
Alex Kleinf9859972019-03-14 17:11:42 -0600278 """Compile the Build API protobuf files.
279
280 By default this will compile from infra/proto/src to api/gen. The output
281 directory may be changed, but the imports will always be treated as if it is
282 in the default location.
283
284 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700285 output: The output directory.
286 protoc_version: Which protoc to use for the compile.
Alex Klein851f4ee2022-03-29 16:03:45 -0600287 dir_subset: What proto to compile.
Alex Klein5b1bca32022-04-05 10:56:35 -0600288 postprocess: Whether to run the postprocess step.
Alex Kleinf9859972019-03-14 17:11:42 -0600289 """
Alex Klein098f7982021-03-01 13:15:29 -0700290 source = os.path.join(_get_proto_dir(protoc_version), 'src')
291 protoc_version = protoc_version or ProtocVersion.CHROMITE
Alex Kleinf9859972019-03-14 17:11:42 -0600292
Alex Klein098f7982021-03-01 13:15:29 -0700293 _InstallProtoc(protoc_version)
Alex Kleinf9859972019-03-14 17:11:42 -0600294 _CleanTargetDirectory(output)
Alex Klein851f4ee2022-03-29 16:03:45 -0600295 _GenerateFiles(source, output, protoc_version, dir_subset)
Alex Kleinf9859972019-03-14 17:11:42 -0600296 _InstallMissingInits(output)
Alex Klein5b1bca32022-04-05 10:56:35 -0600297 if postprocess:
298 _PostprocessFiles(output, protoc_version)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700299
300
301def GetParser():
Alex Kleinf9859972019-03-14 17:11:42 -0600302 """Build the argument parser."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700303 parser = commandline.ArgumentParser(description=__doc__)
Alex Klein098f7982021-03-01 13:15:29 -0700304 standard_group = parser.add_argument_group(
305 'Committed Bindings',
306 description='Options for generating the bindings in chromite/api/.')
307 standard_group.add_argument(
308 '--chromite',
309 dest='protoc_version',
310 action='append_const',
311 const=ProtocVersion.CHROMITE,
312 help='Generate only the chromite bindings. Generates all by default. The '
Alex Klein5b1bca32022-04-05 10:56:35 -0600313 'chromite bindings are compatible with the version of protobuf in '
314 'chromite/third_party.')
Alex Klein098f7982021-03-01 13:15:29 -0700315 standard_group.add_argument(
316 '--sdk',
317 dest='protoc_version',
318 action='append_const',
319 const=ProtocVersion.SDK,
320 help='Generate only the SDK bindings. Generates all by default. The SDK '
Alex Klein5b1bca32022-04-05 10:56:35 -0600321 'bindings are compiled by protoc in the SDK, and is compatible '
322 'with the version of protobuf in the SDK (i.e. the one installed by '
323 'the ebuild).')
Alex Klein098f7982021-03-01 13:15:29 -0700324
325 dest_group = parser.add_argument_group(
326 'Out of Tree Bindings',
327 description='Options for generating bindings in a custom location.')
328 dest_group.add_argument(
Alex Klein5534f992019-09-16 16:31:23 -0600329 '--destination',
330 type='path',
Alex Klein098f7982021-03-01 13:15:29 -0700331 help='A directory where a single version of the proto should be '
Alex Klein5b1bca32022-04-05 10:56:35 -0600332 'generated. When not given, the proto generates in all default '
333 'locations instead.')
Alex Klein098f7982021-03-01 13:15:29 -0700334 dest_group.add_argument(
335 '--dest-sdk',
336 action='store_const',
337 dest='dest_protoc',
338 default=ProtocVersion.CHROMITE,
339 const=ProtocVersion.SDK,
340 help='Generate the SDK version of the protos in --destination instead of '
Alex Klein5b1bca32022-04-05 10:56:35 -0600341 'the chromite version.')
Alex Klein851f4ee2022-03-29 16:03:45 -0600342 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. '
Alex Klein5b1bca32022-04-05 10:56:35 -0600349 'Only considered when generating out of tree bindings.')
350 dest_group.add_argument(
351 '--skip-postprocessing',
352 action='store_false',
353 dest='postprocess',
354 default=True,
355 help='Skip postprocessing files.'
356 )
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700357 return parser
358
359
360def _ParseArguments(argv):
361 """Parse and validate arguments."""
362 parser = GetParser()
363 opts = parser.parse_args(argv)
364
Alex Klein098f7982021-03-01 13:15:29 -0700365 if not opts.protoc_version:
366 opts.protoc_version = [ProtocVersion.CHROMITE, ProtocVersion.SDK]
367
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700368 opts.Freeze()
369 return opts
370
371
372def main(argv):
Alex Kleinf9859972019-03-14 17:11:42 -0600373 opts = _ParseArguments(argv)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700374
Alex Klein098f7982021-03-01 13:15:29 -0700375 if opts.destination:
376 # Destination set, only compile a single version in the destination.
377 try:
Alex Klein851f4ee2022-03-29 16:03:45 -0600378 CompileProto(
379 output=opts.destination,
380 protoc_version=opts.dest_protoc,
Alex Klein5b1bca32022-04-05 10:56:35 -0600381 dir_subset=opts.dir_subset,
382 postprocess=opts.postprocess
383 )
Alex Klein098f7982021-03-01 13:15:29 -0700384 except Error as e:
385 cros_build_lib.Die('Error compiling bindings to destination: %s', str(e))
386 else:
387 return 0
388
389 if ProtocVersion.CHROMITE in opts.protoc_version:
390 # Compile the chromite bindings.
391 try:
392 CompileProto(
393 output=_get_gen_dir(ProtocVersion.CHROMITE),
394 protoc_version=ProtocVersion.CHROMITE)
395 except Error as e:
396 cros_build_lib.Die('Error compiling chromite bindings: %s', str(e))
397
398 if ProtocVersion.SDK in opts.protoc_version:
399 # Compile the SDK bindings.
400 if not cros_build_lib.IsInsideChroot():
401 # Rerun inside of the SDK instead of trying to map all of the paths.
402 cmd = [
403 os.path.join(constants.CHROOT_SOURCE_ROOT, 'chromite', 'api',
404 'compile_build_api_proto'),
405 '--sdk',
406 ]
407 result = cros_build_lib.run(
408 cmd, print_cmd=False, enter_chroot=True, check=False)
409 return result.returncode
410 else:
411 try:
412 CompileProto(
413 output=_get_gen_dir(ProtocVersion.SDK),
414 protoc_version=ProtocVersion.SDK)
415 except Error as e:
416 cros_build_lib.Die('Error compiling SDK bindings: %s', str(e))