blob: 86f833f18f462652b17f4ef9b7d66c88a4850d18 [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 Kleinf4dc4f52018-12-05 13:55:12 -070014
15from chromite.lib import commandline
Alex Kleinc33c1912019-02-15 10:29:13 -070016from chromite.lib import constants
Alex Kleinf4dc4f52018-12-05 13:55:12 -070017from chromite.lib import cros_build_lib
Sean McAllister6a5eaa02021-05-26 10:47:14 -060018from chromite.lib import git
Alex Kleinf9859972019-03-14 17:11:42 -060019from chromite.lib import osutils
20
Alex Kleinf9859972019-03-14 17:11:42 -060021_CIPD_ROOT = os.path.join(constants.CHROMITE_DIR, '.cipd_bin')
Alex Kleinf9859972019-03-14 17:11:42 -060022
Alex Klein098f7982021-03-01 13:15:29 -070023# Chromite's protobuf library version (third_party/google/protobuf).
Mike Nicholse366e7a2021-02-22 18:14:57 -070024PROTOC_VERSION = '3.13.0'
Alex Kleinf9859972019-03-14 17:11:42 -060025
26
Alex Klein5534f992019-09-16 16:31:23 -060027class Error(Exception):
28 """Base error class for the module."""
29
30
31class GenerationError(Error):
32 """A failure we can't recover from."""
33
34
Alex Klein098f7982021-03-01 13:15:29 -070035@enum.unique
36class ProtocVersion(enum.Enum):
37 """Enum for possible protoc versions."""
38 # The SDK version of the bindings use the protoc in the SDK, and so is
39 # compatible with the protobuf library in the SDK, i.e. the one installed
40 # via the ebuild.
41 SDK = enum.auto()
42 # The Chromite version of the bindings uses a protoc binary downloaded from
43 # CIPD that matches the version of the protobuf library in
44 # chromite/third_party/google/protobuf.
45 CHROMITE = enum.auto()
46
47
48def _get_gen_dir(protoc_version: ProtocVersion):
49 """Get the chromite/api directory path."""
50 if protoc_version is ProtocVersion.SDK:
51 return os.path.join(constants.CHROMITE_DIR, 'api', 'gen_sdk')
52 else:
53 return os.path.join(constants.CHROMITE_DIR, 'api', 'gen')
54
55
56def _get_protoc_command(protoc_version: ProtocVersion):
57 """Get the protoc command for the target protoc."""
58 if protoc_version is ProtocVersion.SDK:
59 return 'protoc'
60 else:
61 return os.path.join(_CIPD_ROOT, 'protoc')
62
63
64def _get_proto_dir(_protoc_version):
65 """Get the proto directory for the target protoc."""
66 return os.path.join(constants.CHROMITE_DIR, 'infra', 'proto')
67
68
69def _InstallProtoc(protoc_version: ProtocVersion):
Alex Kleinf9859972019-03-14 17:11:42 -060070 """Install protoc from CIPD."""
Alex Klein098f7982021-03-01 13:15:29 -070071 if protoc_version is not ProtocVersion.CHROMITE:
72 return
73
Alex Klein5534f992019-09-16 16:31:23 -060074 logging.info('Installing protoc.')
Alex Kleinf9859972019-03-14 17:11:42 -060075 cmd = ['cipd', 'ensure']
76 # Clean up the output.
77 cmd.extend(['-log-level', 'warning'])
78 # Set the install location.
79 cmd.extend(['-root', _CIPD_ROOT])
80
81 ensure_content = ('infra/tools/protoc/${platform} '
82 'protobuf_version:v%s' % PROTOC_VERSION)
83 with osutils.TempDir() as tempdir:
84 ensure_file = os.path.join(tempdir, 'cipd_ensure_file')
85 osutils.WriteFile(ensure_file, ensure_content)
86
87 cmd.extend(['-ensure-file', ensure_file])
88
Mike Frysinger45602c72019-09-22 02:15:11 -040089 cros_build_lib.run(cmd, cwd=constants.CHROMITE_DIR, print_cmd=False)
Alex Klein5534f992019-09-16 16:31:23 -060090
Alex Kleinf9859972019-03-14 17:11:42 -060091
Miriam Polzer713e6542021-08-17 10:58:14 +020092def _CleanTargetDirectory(directory: str):
Alex Kleinf9859972019-03-14 17:11:42 -060093 """Remove any existing generated files in the directory.
94
95 This clean only removes the generated files to avoid accidentally destroying
96 __init__.py customizations down the line. That will leave otherwise empty
97 directories in place if things get moved. Neither case is relevant at the
98 time of writing, but lingering empty directories seemed better than
99 diagnosing accidental __init__.py changes.
100
101 Args:
Miriam Polzer713e6542021-08-17 10:58:14 +0200102 directory: Path to be cleaned up.
Alex Kleinf9859972019-03-14 17:11:42 -0600103 """
Alex Klein098f7982021-03-01 13:15:29 -0700104 logging.info('Cleaning old files from %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600105 for dirpath, _dirnames, filenames in os.walk(directory):
106 old = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
Alex Klein5534f992019-09-16 16:31:23 -0600107 # Remove empty init files to clean up otherwise empty directories.
108 if '__init__.py' in filenames:
109 init = os.path.join(dirpath, '__init__.py')
110 if not osutils.ReadFile(init):
111 old.append(init)
112
Alex Kleinf9859972019-03-14 17:11:42 -0600113 for current in old:
114 osutils.SafeUnlink(current)
115
Alex Klein5534f992019-09-16 16:31:23 -0600116
Alex Klein098f7982021-03-01 13:15:29 -0700117def _GenerateFiles(source: str, output: str, protoc_version: ProtocVersion):
Alex Kleinf9859972019-03-14 17:11:42 -0600118 """Generate the proto files from the |source| tree into |output|.
119
120 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700121 source: Path to the proto source root directory.
122 output: Path to the output root directory.
123 protoc_version: Which protoc to use.
Alex Kleinf9859972019-03-14 17:11:42 -0600124 """
Alex Klein098f7982021-03-01 13:15:29 -0700125 logging.info('Generating files to %s.', output)
126 osutils.SafeMakedirs(output)
127
Alex Kleinf9859972019-03-14 17:11:42 -0600128 targets = []
129
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600130 chromeos_config_path = os.path.realpath(
131 os.path.join(constants.SOURCE_ROOT, 'src/config'))
Alex Klein098f7982021-03-01 13:15:29 -0700132
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600133 with tempfile.TemporaryDirectory() as tempdir:
134 if not os.path.exists(chromeos_config_path):
135 chromeos_config_path = os.path.join(tempdir, 'config')
Alex Klein5534f992019-09-16 16:31:23 -0600136
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600137 logging.info('Creating shallow clone of chromiumos/config')
Alex Klein4fd378d2022-03-29 16:00:49 -0600138 git.Clone(
139 chromeos_config_path,
140 '%s/chromiumos/config' % constants.EXTERNAL_GOB_URL,
141 depth=1)
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600142
Andrew Lamb59ed32e2021-07-26 15:14:37 -0600143 # Only compile the subset we need for the API.
144 subdirs = [
145 os.path.join(source, 'chromite'),
146 os.path.join(source, 'chromiumos'),
147 os.path.join(source, 'client'),
148 os.path.join(source, 'config'),
149 os.path.join(source, 'test_platform'),
150 os.path.join(source, 'device'),
151 os.path.join(chromeos_config_path, 'proto/chromiumos'),
152 ]
153 for basedir in subdirs:
154 for dirpath, _dirnames, filenames in os.walk(basedir):
155 for filename in filenames:
156 if filename.endswith('.proto'):
157 # We have a match, add the file.
158 targets.append(os.path.join(dirpath, filename))
159
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600160 cmd = [
161 _get_protoc_command(protoc_version),
162 '-I',
163 os.path.join(chromeos_config_path, 'proto'),
164 '--python_out',
165 output,
166 '--proto_path',
167 source,
168 ]
169 cmd.extend(targets)
170
171 result = cros_build_lib.run(
172 cmd,
173 cwd=source,
174 print_cmd=False,
175 check=False,
176 enter_chroot=protoc_version is ProtocVersion.SDK)
177
178 if result.returncode:
179 raise GenerationError('Error compiling the proto. See the output for a '
180 'message.')
Alex Kleinf9859972019-03-14 17:11:42 -0600181
182
183def _InstallMissingInits(directory):
184 """Add any __init__.py files not present in the generated protobuf folders."""
Alex Klein098f7982021-03-01 13:15:29 -0700185 logging.info('Adding missing __init__.py files in %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600186 for dirpath, _dirnames, filenames in os.walk(directory):
187 if '__init__.py' not in filenames:
188 osutils.Touch(os.path.join(dirpath, '__init__.py'))
189
190
Alex Klein098f7982021-03-01 13:15:29 -0700191def _PostprocessFiles(directory: str, protoc_version: ProtocVersion):
Alex Kleinf9859972019-03-14 17:11:42 -0600192 """Do postprocessing on the generated files.
193
194 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700195 directory: The root directory containing the generated files that are
Alex Kleinf9859972019-03-14 17:11:42 -0600196 to be processed.
Alex Klein098f7982021-03-01 13:15:29 -0700197 protoc_version: Which protoc is being used to generate the files.
Alex Kleinf9859972019-03-14 17:11:42 -0600198 """
Alex Klein098f7982021-03-01 13:15:29 -0700199 logging.info('Postprocessing: Fix imports in %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600200 # We are using a negative address here (the /address/! portion of the sed
201 # command) to make sure we don't change any imports from protobuf itself.
Alex Kleind20d8162021-06-21 12:40:44 -0600202 address = '^from google.protobuf'
Alex Kleinf9859972019-03-14 17:11:42 -0600203 # Find: 'from x import y_pb2 as x_dot_y_pb2'.
204 # "\(^google.protobuf[^ ]*\)" matches the module we're importing from.
205 # - \( and \) are for groups in sed.
206 # - ^google.protobuf prevents changing the import for protobuf's files.
207 # - [^ ] = Not a space. The [:space:] character set is too broad, but would
208 # technically work too.
209 find = r'^from \([^ ]*\) import \([^ ]*\)_pb2 as \([^ ]*\)$'
Alex Klein098f7982021-03-01 13:15:29 -0700210 # Substitute: 'from chromite.api.gen[_sdk].x import y_pb2 as x_dot_y_pb2'.
211 if protoc_version is ProtocVersion.SDK:
212 sub = 'from chromite.api.gen_sdk.\\1 import \\2_pb2 as \\3'
213 else:
214 sub = 'from chromite.api.gen.\\1 import \\2_pb2 as \\3'
215
Alex Klein5534f992019-09-16 16:31:23 -0600216 from_sed = [
217 'sed', '-i',
218 '/%(address)s/!s/%(find)s/%(sub)s/g' % {
219 'address': address,
220 'find': find,
221 'sub': sub
222 }
223 ]
Alex Kleinf9859972019-03-14 17:11:42 -0600224
Alex Kleind20d8162021-06-21 12:40:44 -0600225 seds = [from_sed]
226 if protoc_version is ProtocVersion.CHROMITE:
227 # We also need to change the google.protobuf imports to point directly
228 # at the chromite.third_party version of the library.
229 # The SDK version of the proto is meant to be used with the protobuf
230 # libraries installed in the SDK, so leave those as google.protobuf.
231 g_p_address = '^from google.protobuf'
232 g_p_find = r'from \([^ ]*\) import \(.*\)$'
233 g_p_sub = 'from chromite.third_party.\\1 import \\2'
234 google_protobuf_sed = [
235 'sed', '-i',
236 '/%(address)s/s/%(find)s/%(sub)s/g' % {
237 'address': g_p_address,
238 'find': g_p_find,
239 'sub': g_p_sub
240 }
241 ]
242 seds.append(google_protobuf_sed)
243
Alex Kleinf9859972019-03-14 17:11:42 -0600244 for dirpath, _dirnames, filenames in os.walk(directory):
Alex Klein098f7982021-03-01 13:15:29 -0700245 # Update the imports in the generated files.
Alex Kleinf9859972019-03-14 17:11:42 -0600246 pb2 = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
247 if pb2:
Alex Kleind20d8162021-06-21 12:40:44 -0600248 for sed in seds:
249 cmd = sed + pb2
250 cros_build_lib.run(cmd, print_cmd=False)
Alex Kleinf9859972019-03-14 17:11:42 -0600251
252
Alex Klein098f7982021-03-01 13:15:29 -0700253def CompileProto(output: str, protoc_version: ProtocVersion):
Alex Kleinf9859972019-03-14 17:11:42 -0600254 """Compile the Build API protobuf files.
255
256 By default this will compile from infra/proto/src to api/gen. The output
257 directory may be changed, but the imports will always be treated as if it is
258 in the default location.
259
260 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700261 output: The output directory.
262 protoc_version: Which protoc to use for the compile.
Alex Kleinf9859972019-03-14 17:11:42 -0600263 """
Alex Klein098f7982021-03-01 13:15:29 -0700264 source = os.path.join(_get_proto_dir(protoc_version), 'src')
265 protoc_version = protoc_version or ProtocVersion.CHROMITE
Alex Kleinf9859972019-03-14 17:11:42 -0600266
Alex Klein098f7982021-03-01 13:15:29 -0700267 _InstallProtoc(protoc_version)
Alex Kleinf9859972019-03-14 17:11:42 -0600268 _CleanTargetDirectory(output)
Alex Klein098f7982021-03-01 13:15:29 -0700269 _GenerateFiles(source, output, protoc_version)
Alex Kleinf9859972019-03-14 17:11:42 -0600270 _InstallMissingInits(output)
Alex Klein098f7982021-03-01 13:15:29 -0700271 _PostprocessFiles(output, protoc_version)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700272
273
274def GetParser():
Alex Kleinf9859972019-03-14 17:11:42 -0600275 """Build the argument parser."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700276 parser = commandline.ArgumentParser(description=__doc__)
Alex Klein098f7982021-03-01 13:15:29 -0700277 standard_group = parser.add_argument_group(
278 'Committed Bindings',
279 description='Options for generating the bindings in chromite/api/.')
280 standard_group.add_argument(
281 '--chromite',
282 dest='protoc_version',
283 action='append_const',
284 const=ProtocVersion.CHROMITE,
285 help='Generate only the chromite bindings. Generates all by default. The '
Alex Klein4fd378d2022-03-29 16:00:49 -0600286 'chromite bindings are compatible with the version of protobuf in '
287 'chromite/third_party.')
Alex Klein098f7982021-03-01 13:15:29 -0700288 standard_group.add_argument(
289 '--sdk',
290 dest='protoc_version',
291 action='append_const',
292 const=ProtocVersion.SDK,
293 help='Generate only the SDK bindings. Generates all by default. The SDK '
Alex Klein4fd378d2022-03-29 16:00:49 -0600294 'bindings are compiled by protoc in the SDK, and is compatible '
295 'with the version of protobuf in the SDK (i.e. the one installed by '
296 'the ebuild).')
Alex Klein098f7982021-03-01 13:15:29 -0700297
298 dest_group = parser.add_argument_group(
299 'Out of Tree Bindings',
300 description='Options for generating bindings in a custom location.')
301 dest_group.add_argument(
Alex Klein5534f992019-09-16 16:31:23 -0600302 '--destination',
303 type='path',
Alex Klein098f7982021-03-01 13:15:29 -0700304 help='A directory where a single version of the proto should be '
Alex Klein4fd378d2022-03-29 16:00:49 -0600305 'generated. When not given, the proto generates in all default '
306 'locations instead.')
Alex Klein098f7982021-03-01 13:15:29 -0700307 dest_group.add_argument(
308 '--dest-sdk',
309 action='store_const',
310 dest='dest_protoc',
311 default=ProtocVersion.CHROMITE,
312 const=ProtocVersion.SDK,
313 help='Generate the SDK version of the protos in --destination instead of '
Alex Klein4fd378d2022-03-29 16:00:49 -0600314 'the chromite version.')
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700315 return parser
316
317
318def _ParseArguments(argv):
319 """Parse and validate arguments."""
320 parser = GetParser()
321 opts = parser.parse_args(argv)
322
Alex Klein098f7982021-03-01 13:15:29 -0700323 if not opts.protoc_version:
324 opts.protoc_version = [ProtocVersion.CHROMITE, ProtocVersion.SDK]
325
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700326 opts.Freeze()
327 return opts
328
329
330def main(argv):
Alex Kleinf9859972019-03-14 17:11:42 -0600331 opts = _ParseArguments(argv)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700332
Alex Klein098f7982021-03-01 13:15:29 -0700333 if opts.destination:
334 # Destination set, only compile a single version in the destination.
335 try:
336 CompileProto(output=opts.destination, protoc_version=opts.dest_protoc)
337 except Error as e:
338 cros_build_lib.Die('Error compiling bindings to destination: %s', str(e))
339 else:
340 return 0
341
342 if ProtocVersion.CHROMITE in opts.protoc_version:
343 # Compile the chromite bindings.
344 try:
345 CompileProto(
346 output=_get_gen_dir(ProtocVersion.CHROMITE),
347 protoc_version=ProtocVersion.CHROMITE)
348 except Error as e:
349 cros_build_lib.Die('Error compiling chromite bindings: %s', str(e))
350
351 if ProtocVersion.SDK in opts.protoc_version:
352 # Compile the SDK bindings.
353 if not cros_build_lib.IsInsideChroot():
354 # Rerun inside of the SDK instead of trying to map all of the paths.
355 cmd = [
356 os.path.join(constants.CHROOT_SOURCE_ROOT, 'chromite', 'api',
357 'compile_build_api_proto'),
358 '--sdk',
359 ]
360 result = cros_build_lib.run(
361 cmd, print_cmd=False, enter_chroot=True, check=False)
362 return result.returncode
363 else:
364 try:
365 CompileProto(
366 output=_get_gen_dir(ProtocVersion.SDK),
367 protoc_version=ProtocVersion.SDK)
368 except Error as e:
369 cros_build_lib.Die('Error compiling SDK bindings: %s', str(e))