blob: f81814294fc319e81991c55319310391b1366ba8 [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
Alex Kleinf4dc4f52018-12-05 13:55:12 -070011import os
Sean McAllister6a5eaa02021-05-26 10:47:14 -060012import tempfile
Alex Kleinf4dc4f52018-12-05 13:55:12 -070013
14from chromite.lib import commandline
Alex Kleinc33c1912019-02-15 10:29:13 -070015from chromite.lib import constants
Alex Kleinf4dc4f52018-12-05 13:55:12 -070016from chromite.lib import cros_build_lib
Alex Klein5534f992019-09-16 16:31:23 -060017from chromite.lib import cros_logging as logging
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
Mike Frysingeref94e4c2020-02-10 23:59:54 -050021
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
49def _get_gen_dir(protoc_version: ProtocVersion):
50 """Get the chromite/api directory path."""
51 if protoc_version is ProtocVersion.SDK:
52 return os.path.join(constants.CHROMITE_DIR, 'api', 'gen_sdk')
53 else:
54 return os.path.join(constants.CHROMITE_DIR, 'api', 'gen')
55
56
57def _get_protoc_command(protoc_version: ProtocVersion):
58 """Get the protoc command for the target protoc."""
59 if protoc_version is ProtocVersion.SDK:
60 return 'protoc'
61 else:
62 return os.path.join(_CIPD_ROOT, 'protoc')
63
64
65def _get_proto_dir(_protoc_version):
66 """Get the proto directory for the target protoc."""
67 return os.path.join(constants.CHROMITE_DIR, 'infra', 'proto')
68
69
70def _InstallProtoc(protoc_version: ProtocVersion):
Alex Kleinf9859972019-03-14 17:11:42 -060071 """Install protoc from CIPD."""
Alex Klein098f7982021-03-01 13:15:29 -070072 if protoc_version is not ProtocVersion.CHROMITE:
73 return
74
Alex Klein5534f992019-09-16 16:31:23 -060075 logging.info('Installing protoc.')
Alex Kleinf9859972019-03-14 17:11:42 -060076 cmd = ['cipd', 'ensure']
77 # Clean up the output.
78 cmd.extend(['-log-level', 'warning'])
79 # Set the install location.
80 cmd.extend(['-root', _CIPD_ROOT])
81
82 ensure_content = ('infra/tools/protoc/${platform} '
83 'protobuf_version:v%s' % PROTOC_VERSION)
84 with osutils.TempDir() as tempdir:
85 ensure_file = os.path.join(tempdir, 'cipd_ensure_file')
86 osutils.WriteFile(ensure_file, ensure_content)
87
88 cmd.extend(['-ensure-file', ensure_file])
89
Mike Frysinger45602c72019-09-22 02:15:11 -040090 cros_build_lib.run(cmd, cwd=constants.CHROMITE_DIR, print_cmd=False)
Alex Klein5534f992019-09-16 16:31:23 -060091
Alex Kleinf9859972019-03-14 17:11:42 -060092
93def _CleanTargetDirectory(directory):
94 """Remove any existing generated files in the directory.
95
96 This clean only removes the generated files to avoid accidentally destroying
97 __init__.py customizations down the line. That will leave otherwise empty
98 directories in place if things get moved. Neither case is relevant at the
99 time of writing, but lingering empty directories seemed better than
100 diagnosing accidental __init__.py changes.
101
102 Args:
103 directory (str): Path to be cleaned up.
104 """
Alex Klein098f7982021-03-01 13:15:29 -0700105 logging.info('Cleaning old files from %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600106 for dirpath, _dirnames, filenames in os.walk(directory):
107 old = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
Alex Klein5534f992019-09-16 16:31:23 -0600108 # Remove empty init files to clean up otherwise empty directories.
109 if '__init__.py' in filenames:
110 init = os.path.join(dirpath, '__init__.py')
111 if not osutils.ReadFile(init):
112 old.append(init)
113
Alex Kleinf9859972019-03-14 17:11:42 -0600114 for current in old:
115 osutils.SafeUnlink(current)
116
Alex Klein5534f992019-09-16 16:31:23 -0600117
Alex Klein098f7982021-03-01 13:15:29 -0700118def _GenerateFiles(source: str, output: str, protoc_version: ProtocVersion):
Alex Kleinf9859972019-03-14 17:11:42 -0600119 """Generate the proto files from the |source| tree into |output|.
120
121 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700122 source: Path to the proto source root directory.
123 output: Path to the output root directory.
124 protoc_version: Which protoc to use.
Alex Kleinf9859972019-03-14 17:11:42 -0600125 """
Alex Klein098f7982021-03-01 13:15:29 -0700126 logging.info('Generating files to %s.', output)
127 osutils.SafeMakedirs(output)
128
Alex Kleinf9859972019-03-14 17:11:42 -0600129 targets = []
130
131 # Only compile the subset we need for the API.
Alex Klein5534f992019-09-16 16:31:23 -0600132 subdirs = [
133 os.path.join(source, 'chromite'),
134 os.path.join(source, 'chromiumos'),
LaMont Jones4a78ec52020-07-27 17:23:45 -0600135 os.path.join(source, 'client'),
Andrew Lamb2cbe4582019-10-29 11:56:25 -0600136 os.path.join(source, 'config'),
Xinan Lin2a40ea72020-01-10 23:50:27 +0000137 os.path.join(source, 'test_platform'),
138 os.path.join(source, 'device')
Alex Klein5534f992019-09-16 16:31:23 -0600139 ]
Alex Kleinf9859972019-03-14 17:11:42 -0600140 for basedir in subdirs:
141 for dirpath, _dirnames, filenames in os.walk(basedir):
142 for filename in filenames:
143 if filename.endswith('.proto'):
144 # We have a match, add the file.
145 targets.append(os.path.join(dirpath, filename))
146
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600147 chromeos_config_path = os.path.realpath(
148 os.path.join(constants.SOURCE_ROOT, 'src/config'))
Alex Klein098f7982021-03-01 13:15:29 -0700149
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600150 with tempfile.TemporaryDirectory() as tempdir:
151 if not os.path.exists(chromeos_config_path):
152 chromeos_config_path = os.path.join(tempdir, 'config')
Alex Klein5534f992019-09-16 16:31:23 -0600153
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600154 logging.info('Creating shallow clone of chromiumos/config')
155 git.Clone(chromeos_config_path,
156 '%s/chromiumos/config' % constants.EXTERNAL_GOB_URL,
157 depth=1
158 )
159
160 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 '
286 'chromite bindings are compatible with the version of protobuf in '
287 'chromite/third_party.')
288 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 '
294 '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).')
297
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 '
305 'generated. When not given, the proto generates in all default '
306 'locations instead.')
307 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 '
314 '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))