blob: 4a0f70d0b97c6d2cd4752141d9025ea172cd8eb5 [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
Mike Frysinger1cc8f1f2022-04-28 22:40:40 -040022
Alex Kleinf9859972019-03-14 17:11:42 -060023_CIPD_ROOT = os.path.join(constants.CHROMITE_DIR, '.cipd_bin')
Alex Kleinf9859972019-03-14 17:11:42 -060024
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
28
Alex Klein5534f992019-09-16 16:31:23 -060029class Error(Exception):
30 """Base error class for the module."""
31
32
33class GenerationError(Error):
34 """A failure we can't recover from."""
35
36
Alex Klein098f7982021-03-01 13:15:29 -070037@enum.unique
38class ProtocVersion(enum.Enum):
39 """Enum for possible protoc versions."""
40 # The SDK version of the bindings use the protoc in the SDK, and so is
41 # compatible with the protobuf library in the SDK, i.e. the one installed
42 # via the ebuild.
43 SDK = enum.auto()
44 # The Chromite version of the bindings uses a protoc binary downloaded from
45 # CIPD that matches the version of the protobuf library in
46 # chromite/third_party/google/protobuf.
47 CHROMITE = enum.auto()
48
49
Alex Klein851f4ee2022-03-29 16:03:45 -060050@enum.unique
51class SubdirectorySet(enum.Enum):
52 """Enum for the subsets of the proto to compile."""
53 ALL = enum.auto()
54 DEFAULT = enum.auto()
55
56 def get_source_dirs(self, source: Union[str, os.PathLike],
57 chromeos_config_path: Union[str, os.PathLike]
Alex Klein5b1bca32022-04-05 10:56:35 -060058 ) -> Union[Iterable[str], Iterable[os.PathLike]]:
Alex Klein851f4ee2022-03-29 16:03:45 -060059 """Get the directories for the given subdirectory set."""
60 _join = lambda x, y: os.path.join(x, y) if isinstance(x, str) else x / y
61 if self is self.ALL:
62 return [
63 source,
64 _join(chromeos_config_path, 'proto/chromiumos'),
65 ]
66
67 subdirs = [
68 _join(source, 'chromite'),
69 _join(source, 'chromiumos'),
70 _join(source, 'config'),
71 _join(source, 'test_platform'),
72 _join(source, 'device'),
73 _join(chromeos_config_path, 'proto/chromiumos'),
74 ]
75 return subdirs
76
77
Alex Klein098f7982021-03-01 13:15:29 -070078def _get_gen_dir(protoc_version: ProtocVersion):
79 """Get the chromite/api directory path."""
80 if protoc_version is ProtocVersion.SDK:
81 return os.path.join(constants.CHROMITE_DIR, 'api', 'gen_sdk')
82 else:
83 return os.path.join(constants.CHROMITE_DIR, 'api', 'gen')
84
85
86def _get_protoc_command(protoc_version: ProtocVersion):
87 """Get the protoc command for the target protoc."""
88 if protoc_version is ProtocVersion.SDK:
89 return 'protoc'
90 else:
91 return os.path.join(_CIPD_ROOT, 'protoc')
92
93
94def _get_proto_dir(_protoc_version):
95 """Get the proto directory for the target protoc."""
96 return os.path.join(constants.CHROMITE_DIR, 'infra', 'proto')
97
98
99def _InstallProtoc(protoc_version: ProtocVersion):
Alex Kleinf9859972019-03-14 17:11:42 -0600100 """Install protoc from CIPD."""
Alex Klein098f7982021-03-01 13:15:29 -0700101 if protoc_version is not ProtocVersion.CHROMITE:
102 return
103
Alex Klein5534f992019-09-16 16:31:23 -0600104 logging.info('Installing protoc.')
Alex Kleinf9859972019-03-14 17:11:42 -0600105 cmd = ['cipd', 'ensure']
106 # Clean up the output.
107 cmd.extend(['-log-level', 'warning'])
108 # Set the install location.
109 cmd.extend(['-root', _CIPD_ROOT])
110
111 ensure_content = ('infra/tools/protoc/${platform} '
112 'protobuf_version:v%s' % PROTOC_VERSION)
113 with osutils.TempDir() as tempdir:
114 ensure_file = os.path.join(tempdir, 'cipd_ensure_file')
115 osutils.WriteFile(ensure_file, ensure_content)
116
117 cmd.extend(['-ensure-file', ensure_file])
118
Mike Frysinger45602c72019-09-22 02:15:11 -0400119 cros_build_lib.run(cmd, cwd=constants.CHROMITE_DIR, print_cmd=False)
Alex Klein5534f992019-09-16 16:31:23 -0600120
Alex Kleinf9859972019-03-14 17:11:42 -0600121
Miriam Polzer713e6542021-08-17 10:58:14 +0200122def _CleanTargetDirectory(directory: str):
Alex Kleinf9859972019-03-14 17:11:42 -0600123 """Remove any existing generated files in the directory.
124
125 This clean only removes the generated files to avoid accidentally destroying
126 __init__.py customizations down the line. That will leave otherwise empty
127 directories in place if things get moved. Neither case is relevant at the
128 time of writing, but lingering empty directories seemed better than
129 diagnosing accidental __init__.py changes.
130
131 Args:
Miriam Polzer713e6542021-08-17 10:58:14 +0200132 directory: Path to be cleaned up.
Alex Kleinf9859972019-03-14 17:11:42 -0600133 """
Alex Klein098f7982021-03-01 13:15:29 -0700134 logging.info('Cleaning old files from %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600135 for dirpath, _dirnames, filenames in os.walk(directory):
136 old = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
Alex Klein5534f992019-09-16 16:31:23 -0600137 # Remove empty init files to clean up otherwise empty directories.
138 if '__init__.py' in filenames:
139 init = os.path.join(dirpath, '__init__.py')
140 if not osutils.ReadFile(init):
141 old.append(init)
142
Alex Kleinf9859972019-03-14 17:11:42 -0600143 for current in old:
144 osutils.SafeUnlink(current)
145
Alex Klein5534f992019-09-16 16:31:23 -0600146
Alex Klein851f4ee2022-03-29 16:03:45 -0600147def _GenerateFiles(source: str, output: str, protoc_version: ProtocVersion,
148 dir_subset: SubdirectorySet):
Alex Kleinf9859972019-03-14 17:11:42 -0600149 """Generate the proto files from the |source| tree into |output|.
150
151 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700152 source: Path to the proto source root directory.
153 output: Path to the output root directory.
154 protoc_version: Which protoc to use.
Alex Klein851f4ee2022-03-29 16:03:45 -0600155 dir_subset: The subset of the proto to compile.
Alex Kleinf9859972019-03-14 17:11:42 -0600156 """
Alex Klein098f7982021-03-01 13:15:29 -0700157 logging.info('Generating files to %s.', output)
158 osutils.SafeMakedirs(output)
159
Alex Kleinf9859972019-03-14 17:11:42 -0600160 targets = []
161
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600162 chromeos_config_path = os.path.realpath(
163 os.path.join(constants.SOURCE_ROOT, 'src/config'))
Alex Klein098f7982021-03-01 13:15:29 -0700164
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600165 with tempfile.TemporaryDirectory() as tempdir:
166 if not os.path.exists(chromeos_config_path):
167 chromeos_config_path = os.path.join(tempdir, 'config')
Alex Klein5534f992019-09-16 16:31:23 -0600168
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600169 logging.info('Creating shallow clone of chromiumos/config')
Alex Klein4fd378d2022-03-29 16:00:49 -0600170 git.Clone(
171 chromeos_config_path,
172 '%s/chromiumos/config' % constants.EXTERNAL_GOB_URL,
173 depth=1)
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600174
Alex Klein851f4ee2022-03-29 16:03:45 -0600175 for basedir in dir_subset.get_source_dirs(source, chromeos_config_path):
Andrew Lamb59ed32e2021-07-26 15:14:37 -0600176 for dirpath, _dirnames, filenames in os.walk(basedir):
177 for filename in filenames:
178 if filename.endswith('.proto'):
179 # We have a match, add the file.
180 targets.append(os.path.join(dirpath, filename))
181
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600182 cmd = [
183 _get_protoc_command(protoc_version),
184 '-I',
185 os.path.join(chromeos_config_path, 'proto'),
186 '--python_out',
187 output,
188 '--proto_path',
189 source,
190 ]
191 cmd.extend(targets)
192
193 result = cros_build_lib.run(
194 cmd,
195 cwd=source,
196 print_cmd=False,
197 check=False,
198 enter_chroot=protoc_version is ProtocVersion.SDK)
199
200 if result.returncode:
201 raise GenerationError('Error compiling the proto. See the output for a '
202 'message.')
Alex Kleinf9859972019-03-14 17:11:42 -0600203
204
205def _InstallMissingInits(directory):
206 """Add any __init__.py files not present in the generated protobuf folders."""
Alex Klein098f7982021-03-01 13:15:29 -0700207 logging.info('Adding missing __init__.py files in %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600208 for dirpath, _dirnames, filenames in os.walk(directory):
209 if '__init__.py' not in filenames:
210 osutils.Touch(os.path.join(dirpath, '__init__.py'))
211
212
Alex Klein098f7982021-03-01 13:15:29 -0700213def _PostprocessFiles(directory: str, protoc_version: ProtocVersion):
Alex Kleinf9859972019-03-14 17:11:42 -0600214 """Do postprocessing on the generated files.
215
216 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700217 directory: The root directory containing the generated files that are
Alex Kleinf9859972019-03-14 17:11:42 -0600218 to be processed.
Alex Klein098f7982021-03-01 13:15:29 -0700219 protoc_version: Which protoc is being used to generate the files.
Alex Kleinf9859972019-03-14 17:11:42 -0600220 """
Alex Klein098f7982021-03-01 13:15:29 -0700221 logging.info('Postprocessing: Fix imports in %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600222 # We are using a negative address here (the /address/! portion of the sed
223 # command) to make sure we don't change any imports from protobuf itself.
Alex Kleind20d8162021-06-21 12:40:44 -0600224 address = '^from google.protobuf'
Alex Kleinf9859972019-03-14 17:11:42 -0600225 # Find: 'from x import y_pb2 as x_dot_y_pb2'.
226 # "\(^google.protobuf[^ ]*\)" matches the module we're importing from.
227 # - \( and \) are for groups in sed.
228 # - ^google.protobuf prevents changing the import for protobuf's files.
229 # - [^ ] = Not a space. The [:space:] character set is too broad, but would
230 # technically work too.
231 find = r'^from \([^ ]*\) import \([^ ]*\)_pb2 as \([^ ]*\)$'
Alex Klein098f7982021-03-01 13:15:29 -0700232 # Substitute: 'from chromite.api.gen[_sdk].x import y_pb2 as x_dot_y_pb2'.
233 if protoc_version is ProtocVersion.SDK:
234 sub = 'from chromite.api.gen_sdk.\\1 import \\2_pb2 as \\3'
235 else:
236 sub = 'from chromite.api.gen.\\1 import \\2_pb2 as \\3'
237
Alex Klein5534f992019-09-16 16:31:23 -0600238 from_sed = [
239 'sed', '-i',
240 '/%(address)s/!s/%(find)s/%(sub)s/g' % {
241 'address': address,
242 'find': find,
243 'sub': sub
244 }
245 ]
Alex Kleinf9859972019-03-14 17:11:42 -0600246
Alex Kleind20d8162021-06-21 12:40:44 -0600247 seds = [from_sed]
248 if protoc_version is ProtocVersion.CHROMITE:
249 # We also need to change the google.protobuf imports to point directly
250 # at the chromite.third_party version of the library.
251 # The SDK version of the proto is meant to be used with the protobuf
252 # libraries installed in the SDK, so leave those as google.protobuf.
253 g_p_address = '^from google.protobuf'
254 g_p_find = r'from \([^ ]*\) import \(.*\)$'
255 g_p_sub = 'from chromite.third_party.\\1 import \\2'
256 google_protobuf_sed = [
257 'sed', '-i',
258 '/%(address)s/s/%(find)s/%(sub)s/g' % {
259 'address': g_p_address,
260 'find': g_p_find,
261 'sub': g_p_sub
262 }
263 ]
264 seds.append(google_protobuf_sed)
265
Alex Kleinf9859972019-03-14 17:11:42 -0600266 for dirpath, _dirnames, filenames in os.walk(directory):
Alex Klein098f7982021-03-01 13:15:29 -0700267 # Update the imports in the generated files.
Alex Kleinf9859972019-03-14 17:11:42 -0600268 pb2 = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
269 if pb2:
Alex Kleind20d8162021-06-21 12:40:44 -0600270 for sed in seds:
271 cmd = sed + pb2
272 cros_build_lib.run(cmd, print_cmd=False)
Alex Kleinf9859972019-03-14 17:11:42 -0600273
274
Alex Klein851f4ee2022-03-29 16:03:45 -0600275def CompileProto(output: str,
276 protoc_version: ProtocVersion,
Alex Klein5b1bca32022-04-05 10:56:35 -0600277 dir_subset: SubdirectorySet = SubdirectorySet.DEFAULT,
278 postprocess: bool = True):
Alex Kleinf9859972019-03-14 17:11:42 -0600279 """Compile the Build API protobuf files.
280
281 By default this will compile from infra/proto/src to api/gen. The output
282 directory may be changed, but the imports will always be treated as if it is
283 in the default location.
284
285 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700286 output: The output directory.
287 protoc_version: Which protoc to use for the compile.
Alex Klein851f4ee2022-03-29 16:03:45 -0600288 dir_subset: What proto to compile.
Alex Klein5b1bca32022-04-05 10:56:35 -0600289 postprocess: Whether to run the postprocess step.
Alex Kleinf9859972019-03-14 17:11:42 -0600290 """
Alex Klein098f7982021-03-01 13:15:29 -0700291 source = os.path.join(_get_proto_dir(protoc_version), 'src')
292 protoc_version = protoc_version or ProtocVersion.CHROMITE
Alex Kleinf9859972019-03-14 17:11:42 -0600293
Alex Klein098f7982021-03-01 13:15:29 -0700294 _InstallProtoc(protoc_version)
Alex Kleinf9859972019-03-14 17:11:42 -0600295 _CleanTargetDirectory(output)
Alex Klein851f4ee2022-03-29 16:03:45 -0600296 _GenerateFiles(source, output, protoc_version, dir_subset)
Alex Kleinf9859972019-03-14 17:11:42 -0600297 _InstallMissingInits(output)
Alex Klein5b1bca32022-04-05 10:56:35 -0600298 if postprocess:
299 _PostprocessFiles(output, protoc_version)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700300
301
302def GetParser():
Alex Kleinf9859972019-03-14 17:11:42 -0600303 """Build the argument parser."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700304 parser = commandline.ArgumentParser(description=__doc__)
Alex Klein098f7982021-03-01 13:15:29 -0700305 standard_group = parser.add_argument_group(
306 'Committed Bindings',
307 description='Options for generating the bindings in chromite/api/.')
308 standard_group.add_argument(
309 '--chromite',
310 dest='protoc_version',
311 action='append_const',
312 const=ProtocVersion.CHROMITE,
313 help='Generate only the chromite bindings. Generates all by default. The '
Alex Klein5b1bca32022-04-05 10:56:35 -0600314 'chromite bindings are compatible with the version of protobuf in '
315 'chromite/third_party.')
Alex Klein098f7982021-03-01 13:15:29 -0700316 standard_group.add_argument(
317 '--sdk',
318 dest='protoc_version',
319 action='append_const',
320 const=ProtocVersion.SDK,
321 help='Generate only the SDK bindings. Generates all by default. The SDK '
Alex Klein5b1bca32022-04-05 10:56:35 -0600322 'bindings are compiled by protoc in the SDK, and is compatible '
323 'with the version of protobuf in the SDK (i.e. the one installed by '
324 'the ebuild).')
Alex Klein098f7982021-03-01 13:15:29 -0700325
326 dest_group = parser.add_argument_group(
327 'Out of Tree Bindings',
328 description='Options for generating bindings in a custom location.')
329 dest_group.add_argument(
Alex Klein5534f992019-09-16 16:31:23 -0600330 '--destination',
331 type='path',
Alex Klein098f7982021-03-01 13:15:29 -0700332 help='A directory where a single version of the proto should be '
Alex Klein5b1bca32022-04-05 10:56:35 -0600333 'generated. When not given, the proto generates in all default '
334 'locations instead.')
Alex Klein098f7982021-03-01 13:15:29 -0700335 dest_group.add_argument(
336 '--dest-sdk',
337 action='store_const',
338 dest='dest_protoc',
339 default=ProtocVersion.CHROMITE,
340 const=ProtocVersion.SDK,
341 help='Generate the SDK version of the protos in --destination instead of '
Alex Klein5b1bca32022-04-05 10:56:35 -0600342 'the chromite version.')
Alex Klein851f4ee2022-03-29 16:03:45 -0600343 dest_group.add_argument(
344 '--all-proto',
345 action='store_const',
346 dest='dir_subset',
347 default=SubdirectorySet.DEFAULT,
348 const=SubdirectorySet.ALL,
349 help='Compile ALL proto instead of just the subset needed for the API. '
Alex Klein5b1bca32022-04-05 10:56:35 -0600350 'Only considered when generating out of tree bindings.')
351 dest_group.add_argument(
352 '--skip-postprocessing',
353 action='store_false',
354 dest='postprocess',
355 default=True,
356 help='Skip postprocessing files.'
357 )
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700358 return parser
359
360
361def _ParseArguments(argv):
362 """Parse and validate arguments."""
363 parser = GetParser()
364 opts = parser.parse_args(argv)
365
Alex Klein098f7982021-03-01 13:15:29 -0700366 if not opts.protoc_version:
367 opts.protoc_version = [ProtocVersion.CHROMITE, ProtocVersion.SDK]
368
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700369 opts.Freeze()
370 return opts
371
372
373def main(argv):
Alex Kleinf9859972019-03-14 17:11:42 -0600374 opts = _ParseArguments(argv)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700375
Alex Klein098f7982021-03-01 13:15:29 -0700376 if opts.destination:
377 # Destination set, only compile a single version in the destination.
378 try:
Alex Klein851f4ee2022-03-29 16:03:45 -0600379 CompileProto(
380 output=opts.destination,
381 protoc_version=opts.dest_protoc,
Alex Klein5b1bca32022-04-05 10:56:35 -0600382 dir_subset=opts.dir_subset,
383 postprocess=opts.postprocess
384 )
Alex Klein098f7982021-03-01 13:15:29 -0700385 except Error as e:
386 cros_build_lib.Die('Error compiling bindings to destination: %s', str(e))
387 else:
388 return 0
389
390 if ProtocVersion.CHROMITE in opts.protoc_version:
391 # Compile the chromite bindings.
392 try:
393 CompileProto(
394 output=_get_gen_dir(ProtocVersion.CHROMITE),
395 protoc_version=ProtocVersion.CHROMITE)
396 except Error as e:
397 cros_build_lib.Die('Error compiling chromite bindings: %s', str(e))
398
399 if ProtocVersion.SDK in opts.protoc_version:
400 # Compile the SDK bindings.
401 if not cros_build_lib.IsInsideChroot():
402 # Rerun inside of the SDK instead of trying to map all of the paths.
403 cmd = [
404 os.path.join(constants.CHROOT_SOURCE_ROOT, 'chromite', 'api',
405 'compile_build_api_proto'),
406 '--sdk',
407 ]
408 result = cros_build_lib.run(
409 cmd, print_cmd=False, enter_chroot=True, check=False)
410 return result.returncode
411 else:
412 try:
413 CompileProto(
414 output=_get_gen_dir(ProtocVersion.SDK),
415 protoc_version=ProtocVersion.SDK)
416 except Error as e:
417 cros_build_lib.Die('Error compiling SDK bindings: %s', str(e))