blob: 41fd2164748bb9b2501ecee55bd6afdc536e608e [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 = [
Alex Klein08c70e82022-05-19 10:10:27 -060068 _join(source, 'analysis_service'),
Alex Klein851f4ee2022-03-29 16:03:45 -060069 _join(source, 'chromite'),
70 _join(source, 'chromiumos'),
71 _join(source, 'config'),
72 _join(source, 'test_platform'),
73 _join(source, 'device'),
74 _join(chromeos_config_path, 'proto/chromiumos'),
75 ]
76 return subdirs
77
78
Alex Klein098f7982021-03-01 13:15:29 -070079def _get_gen_dir(protoc_version: ProtocVersion):
80 """Get the chromite/api directory path."""
81 if protoc_version is ProtocVersion.SDK:
82 return os.path.join(constants.CHROMITE_DIR, 'api', 'gen_sdk')
83 else:
84 return os.path.join(constants.CHROMITE_DIR, 'api', 'gen')
85
86
87def _get_protoc_command(protoc_version: ProtocVersion):
88 """Get the protoc command for the target protoc."""
89 if protoc_version is ProtocVersion.SDK:
90 return 'protoc'
91 else:
92 return os.path.join(_CIPD_ROOT, 'protoc')
93
94
95def _get_proto_dir(_protoc_version):
96 """Get the proto directory for the target protoc."""
97 return os.path.join(constants.CHROMITE_DIR, 'infra', 'proto')
98
99
100def _InstallProtoc(protoc_version: ProtocVersion):
Alex Kleinf9859972019-03-14 17:11:42 -0600101 """Install protoc from CIPD."""
Alex Klein098f7982021-03-01 13:15:29 -0700102 if protoc_version is not ProtocVersion.CHROMITE:
103 return
104
Alex Klein5534f992019-09-16 16:31:23 -0600105 logging.info('Installing protoc.')
Alex Kleinf9859972019-03-14 17:11:42 -0600106 cmd = ['cipd', 'ensure']
107 # Clean up the output.
108 cmd.extend(['-log-level', 'warning'])
109 # Set the install location.
110 cmd.extend(['-root', _CIPD_ROOT])
111
112 ensure_content = ('infra/tools/protoc/${platform} '
113 'protobuf_version:v%s' % PROTOC_VERSION)
114 with osutils.TempDir() as tempdir:
115 ensure_file = os.path.join(tempdir, 'cipd_ensure_file')
116 osutils.WriteFile(ensure_file, ensure_content)
117
118 cmd.extend(['-ensure-file', ensure_file])
119
Mike Frysinger45602c72019-09-22 02:15:11 -0400120 cros_build_lib.run(cmd, cwd=constants.CHROMITE_DIR, print_cmd=False)
Alex Klein5534f992019-09-16 16:31:23 -0600121
Alex Kleinf9859972019-03-14 17:11:42 -0600122
Miriam Polzer713e6542021-08-17 10:58:14 +0200123def _CleanTargetDirectory(directory: str):
Alex Kleinf9859972019-03-14 17:11:42 -0600124 """Remove any existing generated files in the directory.
125
126 This clean only removes the generated files to avoid accidentally destroying
127 __init__.py customizations down the line. That will leave otherwise empty
128 directories in place if things get moved. Neither case is relevant at the
129 time of writing, but lingering empty directories seemed better than
130 diagnosing accidental __init__.py changes.
131
132 Args:
Miriam Polzer713e6542021-08-17 10:58:14 +0200133 directory: Path to be cleaned up.
Alex Kleinf9859972019-03-14 17:11:42 -0600134 """
Alex Klein098f7982021-03-01 13:15:29 -0700135 logging.info('Cleaning old files from %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600136 for dirpath, _dirnames, filenames in os.walk(directory):
137 old = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
Alex Klein5534f992019-09-16 16:31:23 -0600138 # Remove empty init files to clean up otherwise empty directories.
139 if '__init__.py' in filenames:
140 init = os.path.join(dirpath, '__init__.py')
141 if not osutils.ReadFile(init):
142 old.append(init)
143
Alex Kleinf9859972019-03-14 17:11:42 -0600144 for current in old:
145 osutils.SafeUnlink(current)
146
Alex Klein5534f992019-09-16 16:31:23 -0600147
Alex Klein851f4ee2022-03-29 16:03:45 -0600148def _GenerateFiles(source: str, output: str, protoc_version: ProtocVersion,
149 dir_subset: SubdirectorySet):
Alex Kleinf9859972019-03-14 17:11:42 -0600150 """Generate the proto files from the |source| tree into |output|.
151
152 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700153 source: Path to the proto source root directory.
154 output: Path to the output root directory.
155 protoc_version: Which protoc to use.
Alex Klein851f4ee2022-03-29 16:03:45 -0600156 dir_subset: The subset of the proto to compile.
Alex Kleinf9859972019-03-14 17:11:42 -0600157 """
Alex Klein098f7982021-03-01 13:15:29 -0700158 logging.info('Generating files to %s.', output)
159 osutils.SafeMakedirs(output)
160
Alex Kleinf9859972019-03-14 17:11:42 -0600161 targets = []
162
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600163 chromeos_config_path = os.path.realpath(
164 os.path.join(constants.SOURCE_ROOT, 'src/config'))
Alex Klein098f7982021-03-01 13:15:29 -0700165
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600166 with tempfile.TemporaryDirectory() as tempdir:
167 if not os.path.exists(chromeos_config_path):
168 chromeos_config_path = os.path.join(tempdir, 'config')
Alex Klein5534f992019-09-16 16:31:23 -0600169
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600170 logging.info('Creating shallow clone of chromiumos/config')
Alex Klein4fd378d2022-03-29 16:00:49 -0600171 git.Clone(
172 chromeos_config_path,
173 '%s/chromiumos/config' % constants.EXTERNAL_GOB_URL,
174 depth=1)
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600175
Alex Klein851f4ee2022-03-29 16:03:45 -0600176 for basedir in dir_subset.get_source_dirs(source, chromeos_config_path):
Andrew Lamb59ed32e2021-07-26 15:14:37 -0600177 for dirpath, _dirnames, filenames in os.walk(basedir):
178 for filename in filenames:
179 if filename.endswith('.proto'):
180 # We have a match, add the file.
181 targets.append(os.path.join(dirpath, filename))
182
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600183 cmd = [
184 _get_protoc_command(protoc_version),
185 '-I',
186 os.path.join(chromeos_config_path, 'proto'),
187 '--python_out',
188 output,
189 '--proto_path',
190 source,
191 ]
192 cmd.extend(targets)
193
194 result = cros_build_lib.run(
195 cmd,
196 cwd=source,
197 print_cmd=False,
198 check=False,
199 enter_chroot=protoc_version is ProtocVersion.SDK)
200
201 if result.returncode:
202 raise GenerationError('Error compiling the proto. See the output for a '
203 'message.')
Alex Kleinf9859972019-03-14 17:11:42 -0600204
205
206def _InstallMissingInits(directory):
207 """Add any __init__.py files not present in the generated protobuf folders."""
Alex Klein098f7982021-03-01 13:15:29 -0700208 logging.info('Adding missing __init__.py files in %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600209 for dirpath, _dirnames, filenames in os.walk(directory):
210 if '__init__.py' not in filenames:
211 osutils.Touch(os.path.join(dirpath, '__init__.py'))
212
213
Alex Klein098f7982021-03-01 13:15:29 -0700214def _PostprocessFiles(directory: str, protoc_version: ProtocVersion):
Alex Kleinf9859972019-03-14 17:11:42 -0600215 """Do postprocessing on the generated files.
216
217 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700218 directory: The root directory containing the generated files that are
Alex Kleinf9859972019-03-14 17:11:42 -0600219 to be processed.
Alex Klein098f7982021-03-01 13:15:29 -0700220 protoc_version: Which protoc is being used to generate the files.
Alex Kleinf9859972019-03-14 17:11:42 -0600221 """
Alex Klein098f7982021-03-01 13:15:29 -0700222 logging.info('Postprocessing: Fix imports in %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600223 # We are using a negative address here (the /address/! portion of the sed
224 # command) to make sure we don't change any imports from protobuf itself.
Alex Kleind20d8162021-06-21 12:40:44 -0600225 address = '^from google.protobuf'
Alex Kleinf9859972019-03-14 17:11:42 -0600226 # Find: 'from x import y_pb2 as x_dot_y_pb2'.
227 # "\(^google.protobuf[^ ]*\)" matches the module we're importing from.
228 # - \( and \) are for groups in sed.
229 # - ^google.protobuf prevents changing the import for protobuf's files.
230 # - [^ ] = Not a space. The [:space:] character set is too broad, but would
231 # technically work too.
232 find = r'^from \([^ ]*\) import \([^ ]*\)_pb2 as \([^ ]*\)$'
Alex Klein098f7982021-03-01 13:15:29 -0700233 # Substitute: 'from chromite.api.gen[_sdk].x import y_pb2 as x_dot_y_pb2'.
234 if protoc_version is ProtocVersion.SDK:
235 sub = 'from chromite.api.gen_sdk.\\1 import \\2_pb2 as \\3'
236 else:
237 sub = 'from chromite.api.gen.\\1 import \\2_pb2 as \\3'
238
Alex Klein5534f992019-09-16 16:31:23 -0600239 from_sed = [
240 'sed', '-i',
241 '/%(address)s/!s/%(find)s/%(sub)s/g' % {
242 'address': address,
243 'find': find,
244 'sub': sub
245 }
246 ]
Alex Kleinf9859972019-03-14 17:11:42 -0600247
Alex Kleind20d8162021-06-21 12:40:44 -0600248 seds = [from_sed]
249 if protoc_version is ProtocVersion.CHROMITE:
250 # We also need to change the google.protobuf imports to point directly
251 # at the chromite.third_party version of the library.
252 # The SDK version of the proto is meant to be used with the protobuf
253 # libraries installed in the SDK, so leave those as google.protobuf.
254 g_p_address = '^from google.protobuf'
255 g_p_find = r'from \([^ ]*\) import \(.*\)$'
256 g_p_sub = 'from chromite.third_party.\\1 import \\2'
257 google_protobuf_sed = [
258 'sed', '-i',
259 '/%(address)s/s/%(find)s/%(sub)s/g' % {
260 'address': g_p_address,
261 'find': g_p_find,
262 'sub': g_p_sub
263 }
264 ]
265 seds.append(google_protobuf_sed)
266
Alex Kleinf9859972019-03-14 17:11:42 -0600267 for dirpath, _dirnames, filenames in os.walk(directory):
Alex Klein098f7982021-03-01 13:15:29 -0700268 # Update the imports in the generated files.
Alex Kleinf9859972019-03-14 17:11:42 -0600269 pb2 = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
270 if pb2:
Alex Kleind20d8162021-06-21 12:40:44 -0600271 for sed in seds:
272 cmd = sed + pb2
273 cros_build_lib.run(cmd, print_cmd=False)
Alex Kleinf9859972019-03-14 17:11:42 -0600274
275
Alex Klein851f4ee2022-03-29 16:03:45 -0600276def CompileProto(output: str,
277 protoc_version: ProtocVersion,
Alex Klein5b1bca32022-04-05 10:56:35 -0600278 dir_subset: SubdirectorySet = SubdirectorySet.DEFAULT,
279 postprocess: bool = True):
Alex Kleinf9859972019-03-14 17:11:42 -0600280 """Compile the Build API protobuf files.
281
282 By default this will compile from infra/proto/src to api/gen. The output
283 directory may be changed, but the imports will always be treated as if it is
284 in the default location.
285
286 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700287 output: The output directory.
288 protoc_version: Which protoc to use for the compile.
Alex Klein851f4ee2022-03-29 16:03:45 -0600289 dir_subset: What proto to compile.
Alex Klein5b1bca32022-04-05 10:56:35 -0600290 postprocess: Whether to run the postprocess step.
Alex Kleinf9859972019-03-14 17:11:42 -0600291 """
Alex Klein098f7982021-03-01 13:15:29 -0700292 source = os.path.join(_get_proto_dir(protoc_version), 'src')
293 protoc_version = protoc_version or ProtocVersion.CHROMITE
Alex Kleinf9859972019-03-14 17:11:42 -0600294
Alex Klein098f7982021-03-01 13:15:29 -0700295 _InstallProtoc(protoc_version)
Alex Kleinf9859972019-03-14 17:11:42 -0600296 _CleanTargetDirectory(output)
Alex Klein851f4ee2022-03-29 16:03:45 -0600297 _GenerateFiles(source, output, protoc_version, dir_subset)
Alex Kleinf9859972019-03-14 17:11:42 -0600298 _InstallMissingInits(output)
Alex Klein5b1bca32022-04-05 10:56:35 -0600299 if postprocess:
300 _PostprocessFiles(output, protoc_version)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700301
302
303def GetParser():
Alex Kleinf9859972019-03-14 17:11:42 -0600304 """Build the argument parser."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700305 parser = commandline.ArgumentParser(description=__doc__)
Alex Klein098f7982021-03-01 13:15:29 -0700306 standard_group = parser.add_argument_group(
307 'Committed Bindings',
308 description='Options for generating the bindings in chromite/api/.')
309 standard_group.add_argument(
310 '--chromite',
311 dest='protoc_version',
312 action='append_const',
313 const=ProtocVersion.CHROMITE,
314 help='Generate only the chromite bindings. Generates all by default. The '
Alex Klein5b1bca32022-04-05 10:56:35 -0600315 'chromite bindings are compatible with the version of protobuf in '
316 'chromite/third_party.')
Alex Klein098f7982021-03-01 13:15:29 -0700317 standard_group.add_argument(
318 '--sdk',
319 dest='protoc_version',
320 action='append_const',
321 const=ProtocVersion.SDK,
322 help='Generate only the SDK bindings. Generates all by default. The SDK '
Alex Klein5b1bca32022-04-05 10:56:35 -0600323 'bindings are compiled by protoc in the SDK, and is compatible '
324 'with the version of protobuf in the SDK (i.e. the one installed by '
325 'the ebuild).')
Alex Klein098f7982021-03-01 13:15:29 -0700326
327 dest_group = parser.add_argument_group(
328 'Out of Tree Bindings',
329 description='Options for generating bindings in a custom location.')
330 dest_group.add_argument(
Alex Klein5534f992019-09-16 16:31:23 -0600331 '--destination',
332 type='path',
Alex Klein098f7982021-03-01 13:15:29 -0700333 help='A directory where a single version of the proto should be '
Alex Klein5b1bca32022-04-05 10:56:35 -0600334 'generated. When not given, the proto generates in all default '
335 'locations instead.')
Alex Klein098f7982021-03-01 13:15:29 -0700336 dest_group.add_argument(
337 '--dest-sdk',
338 action='store_const',
339 dest='dest_protoc',
340 default=ProtocVersion.CHROMITE,
341 const=ProtocVersion.SDK,
342 help='Generate the SDK version of the protos in --destination instead of '
Alex Klein5b1bca32022-04-05 10:56:35 -0600343 'the chromite version.')
Alex Klein851f4ee2022-03-29 16:03:45 -0600344 dest_group.add_argument(
345 '--all-proto',
346 action='store_const',
347 dest='dir_subset',
348 default=SubdirectorySet.DEFAULT,
349 const=SubdirectorySet.ALL,
350 help='Compile ALL proto instead of just the subset needed for the API. '
Alex Klein5b1bca32022-04-05 10:56:35 -0600351 'Only considered when generating out of tree bindings.')
352 dest_group.add_argument(
353 '--skip-postprocessing',
354 action='store_false',
355 dest='postprocess',
356 default=True,
357 help='Skip postprocessing files.'
358 )
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700359 return parser
360
361
362def _ParseArguments(argv):
363 """Parse and validate arguments."""
364 parser = GetParser()
365 opts = parser.parse_args(argv)
366
Alex Klein098f7982021-03-01 13:15:29 -0700367 if not opts.protoc_version:
368 opts.protoc_version = [ProtocVersion.CHROMITE, ProtocVersion.SDK]
369
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700370 opts.Freeze()
371 return opts
372
373
374def main(argv):
Alex Kleinf9859972019-03-14 17:11:42 -0600375 opts = _ParseArguments(argv)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700376
Alex Klein098f7982021-03-01 13:15:29 -0700377 if opts.destination:
378 # Destination set, only compile a single version in the destination.
379 try:
Alex Klein851f4ee2022-03-29 16:03:45 -0600380 CompileProto(
381 output=opts.destination,
382 protoc_version=opts.dest_protoc,
Alex Klein5b1bca32022-04-05 10:56:35 -0600383 dir_subset=opts.dir_subset,
384 postprocess=opts.postprocess
385 )
Alex Klein098f7982021-03-01 13:15:29 -0700386 except Error as e:
387 cros_build_lib.Die('Error compiling bindings to destination: %s', str(e))
388 else:
389 return 0
390
391 if ProtocVersion.CHROMITE in opts.protoc_version:
392 # Compile the chromite bindings.
393 try:
394 CompileProto(
395 output=_get_gen_dir(ProtocVersion.CHROMITE),
396 protoc_version=ProtocVersion.CHROMITE)
397 except Error as e:
398 cros_build_lib.Die('Error compiling chromite bindings: %s', str(e))
399
400 if ProtocVersion.SDK in opts.protoc_version:
401 # Compile the SDK bindings.
402 if not cros_build_lib.IsInsideChroot():
403 # Rerun inside of the SDK instead of trying to map all of the paths.
404 cmd = [
405 os.path.join(constants.CHROOT_SOURCE_ROOT, 'chromite', 'api',
406 'compile_build_api_proto'),
407 '--sdk',
408 ]
409 result = cros_build_lib.run(
410 cmd, print_cmd=False, enter_chroot=True, check=False)
411 return result.returncode
412 else:
413 try:
414 CompileProto(
415 output=_get_gen_dir(ProtocVersion.SDK),
416 protoc_version=ProtocVersion.SDK)
417 except Error as e:
418 cros_build_lib.Die('Error compiling SDK bindings: %s', str(e))