blob: 9f80a2e7b1109fd024b47f9b1eb0629739e322ca [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]
57 ) -> Union[Iterable[str], Iterable[os.PathLike]]:
58 """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,
276 dir_subset: SubdirectorySet = SubdirectorySet.DEFAULT):
Alex Kleinf9859972019-03-14 17:11:42 -0600277 """Compile the Build API protobuf files.
278
279 By default this will compile from infra/proto/src to api/gen. The output
280 directory may be changed, but the imports will always be treated as if it is
281 in the default location.
282
283 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700284 output: The output directory.
285 protoc_version: Which protoc to use for the compile.
Alex Klein851f4ee2022-03-29 16:03:45 -0600286 dir_subset: What proto to compile.
Alex Kleinf9859972019-03-14 17:11:42 -0600287 """
Alex Klein098f7982021-03-01 13:15:29 -0700288 source = os.path.join(_get_proto_dir(protoc_version), 'src')
289 protoc_version = protoc_version or ProtocVersion.CHROMITE
Alex Kleinf9859972019-03-14 17:11:42 -0600290
Alex Klein098f7982021-03-01 13:15:29 -0700291 _InstallProtoc(protoc_version)
Alex Kleinf9859972019-03-14 17:11:42 -0600292 _CleanTargetDirectory(output)
Alex Klein851f4ee2022-03-29 16:03:45 -0600293 _GenerateFiles(source, output, protoc_version, dir_subset)
Alex Kleinf9859972019-03-14 17:11:42 -0600294 _InstallMissingInits(output)
Alex Klein098f7982021-03-01 13:15:29 -0700295 _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 Klein4fd378d2022-03-29 16:00:49 -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 Klein4fd378d2022-03-29 16:00:49 -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 Klein4fd378d2022-03-29 16:00:49 -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 Klein4fd378d2022-03-29 16:00:49 -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. '
346 'Only considered when generating out of tree bindings.')
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700347 return parser
348
349
350def _ParseArguments(argv):
351 """Parse and validate arguments."""
352 parser = GetParser()
353 opts = parser.parse_args(argv)
354
Alex Klein098f7982021-03-01 13:15:29 -0700355 if not opts.protoc_version:
356 opts.protoc_version = [ProtocVersion.CHROMITE, ProtocVersion.SDK]
357
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700358 opts.Freeze()
359 return opts
360
361
362def main(argv):
Alex Kleinf9859972019-03-14 17:11:42 -0600363 opts = _ParseArguments(argv)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700364
Alex Klein098f7982021-03-01 13:15:29 -0700365 if opts.destination:
366 # Destination set, only compile a single version in the destination.
367 try:
Alex Klein851f4ee2022-03-29 16:03:45 -0600368 CompileProto(
369 output=opts.destination,
370 protoc_version=opts.dest_protoc,
371 dir_subset=opts.dir_subset)
Alex Klein098f7982021-03-01 13:15:29 -0700372 except Error as e:
373 cros_build_lib.Die('Error compiling bindings to destination: %s', str(e))
374 else:
375 return 0
376
377 if ProtocVersion.CHROMITE in opts.protoc_version:
378 # Compile the chromite bindings.
379 try:
380 CompileProto(
381 output=_get_gen_dir(ProtocVersion.CHROMITE),
382 protoc_version=ProtocVersion.CHROMITE)
383 except Error as e:
384 cros_build_lib.Die('Error compiling chromite bindings: %s', str(e))
385
386 if ProtocVersion.SDK in opts.protoc_version:
387 # Compile the SDK bindings.
388 if not cros_build_lib.IsInsideChroot():
389 # Rerun inside of the SDK instead of trying to map all of the paths.
390 cmd = [
391 os.path.join(constants.CHROOT_SOURCE_ROOT, 'chromite', 'api',
392 'compile_build_api_proto'),
393 '--sdk',
394 ]
395 result = cros_build_lib.run(
396 cmd, print_cmd=False, enter_chroot=True, check=False)
397 return result.returncode
398 else:
399 try:
400 CompileProto(
401 output=_get_gen_dir(ProtocVersion.SDK),
402 protoc_version=ProtocVersion.SDK)
403 except Error as e:
404 cros_build_lib.Die('Error compiling SDK bindings: %s', str(e))