blob: a9cf206f459f93783ea0d260b04a46e4cd24e347 [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.
202 address = '^from google.protobuf'
203 # 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
225 for dirpath, _dirnames, filenames in os.walk(directory):
Alex Klein098f7982021-03-01 13:15:29 -0700226 # Update the imports in the generated files.
Alex Kleinf9859972019-03-14 17:11:42 -0600227 pb2 = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
228 if pb2:
229 cmd = from_sed + pb2
Mike Frysinger45602c72019-09-22 02:15:11 -0400230 cros_build_lib.run(cmd, print_cmd=False)
Alex Kleinf9859972019-03-14 17:11:42 -0600231
232
Alex Klein098f7982021-03-01 13:15:29 -0700233def CompileProto(output: str, protoc_version: ProtocVersion):
Alex Kleinf9859972019-03-14 17:11:42 -0600234 """Compile the Build API protobuf files.
235
236 By default this will compile from infra/proto/src to api/gen. The output
237 directory may be changed, but the imports will always be treated as if it is
238 in the default location.
239
240 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700241 output: The output directory.
242 protoc_version: Which protoc to use for the compile.
Alex Kleinf9859972019-03-14 17:11:42 -0600243 """
Alex Klein098f7982021-03-01 13:15:29 -0700244 source = os.path.join(_get_proto_dir(protoc_version), 'src')
245 protoc_version = protoc_version or ProtocVersion.CHROMITE
Alex Kleinf9859972019-03-14 17:11:42 -0600246
Alex Klein098f7982021-03-01 13:15:29 -0700247 _InstallProtoc(protoc_version)
Alex Kleinf9859972019-03-14 17:11:42 -0600248 _CleanTargetDirectory(output)
Alex Klein098f7982021-03-01 13:15:29 -0700249 _GenerateFiles(source, output, protoc_version)
Alex Kleinf9859972019-03-14 17:11:42 -0600250 _InstallMissingInits(output)
Alex Klein098f7982021-03-01 13:15:29 -0700251 _PostprocessFiles(output, protoc_version)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700252
253
254def GetParser():
Alex Kleinf9859972019-03-14 17:11:42 -0600255 """Build the argument parser."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700256 parser = commandline.ArgumentParser(description=__doc__)
Alex Klein098f7982021-03-01 13:15:29 -0700257 standard_group = parser.add_argument_group(
258 'Committed Bindings',
259 description='Options for generating the bindings in chromite/api/.')
260 standard_group.add_argument(
261 '--chromite',
262 dest='protoc_version',
263 action='append_const',
264 const=ProtocVersion.CHROMITE,
265 help='Generate only the chromite bindings. Generates all by default. The '
266 'chromite bindings are compatible with the version of protobuf in '
267 'chromite/third_party.')
268 standard_group.add_argument(
269 '--sdk',
270 dest='protoc_version',
271 action='append_const',
272 const=ProtocVersion.SDK,
273 help='Generate only the SDK bindings. Generates all by default. The SDK '
274 'bindings are compiled by protoc in the SDK, and is compatible '
275 'with the version of protobuf in the SDK (i.e. the one installed by '
276 'the ebuild).')
277
278 dest_group = parser.add_argument_group(
279 'Out of Tree Bindings',
280 description='Options for generating bindings in a custom location.')
281 dest_group.add_argument(
Alex Klein5534f992019-09-16 16:31:23 -0600282 '--destination',
283 type='path',
Alex Klein098f7982021-03-01 13:15:29 -0700284 help='A directory where a single version of the proto should be '
285 'generated. When not given, the proto generates in all default '
286 'locations instead.')
287 dest_group.add_argument(
288 '--dest-sdk',
289 action='store_const',
290 dest='dest_protoc',
291 default=ProtocVersion.CHROMITE,
292 const=ProtocVersion.SDK,
293 help='Generate the SDK version of the protos in --destination instead of '
294 'the chromite version.')
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700295 return parser
296
297
298def _ParseArguments(argv):
299 """Parse and validate arguments."""
300 parser = GetParser()
301 opts = parser.parse_args(argv)
302
Alex Klein098f7982021-03-01 13:15:29 -0700303 if not opts.protoc_version:
304 opts.protoc_version = [ProtocVersion.CHROMITE, ProtocVersion.SDK]
305
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700306 opts.Freeze()
307 return opts
308
309
310def main(argv):
Alex Kleinf9859972019-03-14 17:11:42 -0600311 opts = _ParseArguments(argv)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700312
Alex Klein098f7982021-03-01 13:15:29 -0700313 if opts.destination:
314 # Destination set, only compile a single version in the destination.
315 try:
316 CompileProto(output=opts.destination, protoc_version=opts.dest_protoc)
317 except Error as e:
318 cros_build_lib.Die('Error compiling bindings to destination: %s', str(e))
319 else:
320 return 0
321
322 if ProtocVersion.CHROMITE in opts.protoc_version:
323 # Compile the chromite bindings.
324 try:
325 CompileProto(
326 output=_get_gen_dir(ProtocVersion.CHROMITE),
327 protoc_version=ProtocVersion.CHROMITE)
328 except Error as e:
329 cros_build_lib.Die('Error compiling chromite bindings: %s', str(e))
330
331 if ProtocVersion.SDK in opts.protoc_version:
332 # Compile the SDK bindings.
333 if not cros_build_lib.IsInsideChroot():
334 # Rerun inside of the SDK instead of trying to map all of the paths.
335 cmd = [
336 os.path.join(constants.CHROOT_SOURCE_ROOT, 'chromite', 'api',
337 'compile_build_api_proto'),
338 '--sdk',
339 ]
340 result = cros_build_lib.run(
341 cmd, print_cmd=False, enter_chroot=True, check=False)
342 return result.returncode
343 else:
344 try:
345 CompileProto(
346 output=_get_gen_dir(ProtocVersion.SDK),
347 protoc_version=ProtocVersion.SDK)
348 except Error as e:
349 cros_build_lib.Die('Error compiling SDK bindings: %s', str(e))