blob: 35bf4676ef7f2a5296022bcf9dcc561e7ebd6a93 [file] [log] [blame]
Alex Kleinf4dc4f52018-12-05 13:55:12 -07001# -*- coding: utf-8 -*-
2# Copyright 2018 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Alex Kleinf9859972019-03-14 17:11:42 -06006"""Compile the Build API's proto.
7
8Install proto using CIPD to ensure a consistent protoc version.
9"""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070010
11from __future__ import print_function
12
Alex Klein098f7982021-03-01 13:15:29 -070013import enum
Alex Kleinf4dc4f52018-12-05 13:55:12 -070014import os
Mike Frysingeref94e4c2020-02-10 23:59:54 -050015import sys
Alex Kleinf4dc4f52018-12-05 13:55:12 -070016
17from chromite.lib import commandline
Alex Kleinc33c1912019-02-15 10:29:13 -070018from chromite.lib import constants
Alex Kleinf4dc4f52018-12-05 13:55:12 -070019from chromite.lib import cros_build_lib
Alex Klein5534f992019-09-16 16:31:23 -060020from chromite.lib import cros_logging as logging
Alex Kleinf9859972019-03-14 17:11:42 -060021from chromite.lib import osutils
22
Mike Frysingeref94e4c2020-02-10 23:59:54 -050023assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
24
Alex Kleinf9859972019-03-14 17:11:42 -060025_CIPD_ROOT = os.path.join(constants.CHROMITE_DIR, '.cipd_bin')
Alex Kleinf9859972019-03-14 17:11:42 -060026
Alex Klein098f7982021-03-01 13:15:29 -070027# Chromite's protobuf library version (third_party/google/protobuf).
Mike Nicholse366e7a2021-02-22 18:14:57 -070028PROTOC_VERSION = '3.13.0'
Alex Kleinf9859972019-03-14 17:11:42 -060029
30
Alex Klein5534f992019-09-16 16:31:23 -060031class Error(Exception):
32 """Base error class for the module."""
33
34
35class GenerationError(Error):
36 """A failure we can't recover from."""
37
38
Alex Klein098f7982021-03-01 13:15:29 -070039@enum.unique
40class ProtocVersion(enum.Enum):
41 """Enum for possible protoc versions."""
42 # The SDK version of the bindings use the protoc in the SDK, and so is
43 # compatible with the protobuf library in the SDK, i.e. the one installed
44 # via the ebuild.
45 SDK = enum.auto()
46 # The Chromite version of the bindings uses a protoc binary downloaded from
47 # CIPD that matches the version of the protobuf library in
48 # chromite/third_party/google/protobuf.
49 CHROMITE = enum.auto()
50
51
52def _get_gen_dir(protoc_version: ProtocVersion):
53 """Get the chromite/api directory path."""
54 if protoc_version is ProtocVersion.SDK:
55 return os.path.join(constants.CHROMITE_DIR, 'api', 'gen_sdk')
56 else:
57 return os.path.join(constants.CHROMITE_DIR, 'api', 'gen')
58
59
60def _get_protoc_command(protoc_version: ProtocVersion):
61 """Get the protoc command for the target protoc."""
62 if protoc_version is ProtocVersion.SDK:
63 return 'protoc'
64 else:
65 return os.path.join(_CIPD_ROOT, 'protoc')
66
67
68def _get_proto_dir(_protoc_version):
69 """Get the proto directory for the target protoc."""
70 return os.path.join(constants.CHROMITE_DIR, 'infra', 'proto')
71
72
73def _InstallProtoc(protoc_version: ProtocVersion):
Alex Kleinf9859972019-03-14 17:11:42 -060074 """Install protoc from CIPD."""
Alex Klein098f7982021-03-01 13:15:29 -070075 if protoc_version is not ProtocVersion.CHROMITE:
76 return
77
Alex Klein5534f992019-09-16 16:31:23 -060078 logging.info('Installing protoc.')
Alex Kleinf9859972019-03-14 17:11:42 -060079 cmd = ['cipd', 'ensure']
80 # Clean up the output.
81 cmd.extend(['-log-level', 'warning'])
82 # Set the install location.
83 cmd.extend(['-root', _CIPD_ROOT])
84
85 ensure_content = ('infra/tools/protoc/${platform} '
86 'protobuf_version:v%s' % PROTOC_VERSION)
87 with osutils.TempDir() as tempdir:
88 ensure_file = os.path.join(tempdir, 'cipd_ensure_file')
89 osutils.WriteFile(ensure_file, ensure_content)
90
91 cmd.extend(['-ensure-file', ensure_file])
92
Mike Frysinger45602c72019-09-22 02:15:11 -040093 cros_build_lib.run(cmd, cwd=constants.CHROMITE_DIR, print_cmd=False)
Alex Klein5534f992019-09-16 16:31:23 -060094
Alex Kleinf9859972019-03-14 17:11:42 -060095
96def _CleanTargetDirectory(directory):
97 """Remove any existing generated files in the directory.
98
99 This clean only removes the generated files to avoid accidentally destroying
100 __init__.py customizations down the line. That will leave otherwise empty
101 directories in place if things get moved. Neither case is relevant at the
102 time of writing, but lingering empty directories seemed better than
103 diagnosing accidental __init__.py changes.
104
105 Args:
106 directory (str): Path to be cleaned up.
107 """
Alex Klein098f7982021-03-01 13:15:29 -0700108 logging.info('Cleaning old files from %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600109 for dirpath, _dirnames, filenames in os.walk(directory):
110 old = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
Alex Klein5534f992019-09-16 16:31:23 -0600111 # Remove empty init files to clean up otherwise empty directories.
112 if '__init__.py' in filenames:
113 init = os.path.join(dirpath, '__init__.py')
114 if not osutils.ReadFile(init):
115 old.append(init)
116
Alex Kleinf9859972019-03-14 17:11:42 -0600117 for current in old:
118 osutils.SafeUnlink(current)
119
Alex Klein5534f992019-09-16 16:31:23 -0600120
Alex Klein098f7982021-03-01 13:15:29 -0700121def _GenerateFiles(source: str, output: str, protoc_version: ProtocVersion):
Alex Kleinf9859972019-03-14 17:11:42 -0600122 """Generate the proto files from the |source| tree into |output|.
123
124 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700125 source: Path to the proto source root directory.
126 output: Path to the output root directory.
127 protoc_version: Which protoc to use.
Alex Kleinf9859972019-03-14 17:11:42 -0600128 """
Alex Klein098f7982021-03-01 13:15:29 -0700129 logging.info('Generating files to %s.', output)
130 osutils.SafeMakedirs(output)
131
Alex Kleinf9859972019-03-14 17:11:42 -0600132 targets = []
133
134 # Only compile the subset we need for the API.
Alex Klein5534f992019-09-16 16:31:23 -0600135 subdirs = [
136 os.path.join(source, 'chromite'),
137 os.path.join(source, 'chromiumos'),
LaMont Jones4a78ec52020-07-27 17:23:45 -0600138 os.path.join(source, 'client'),
Andrew Lamb2cbe4582019-10-29 11:56:25 -0600139 os.path.join(source, 'config'),
Xinan Lin2a40ea72020-01-10 23:50:27 +0000140 os.path.join(source, 'test_platform'),
141 os.path.join(source, 'device')
Alex Klein5534f992019-09-16 16:31:23 -0600142 ]
Alex Kleinf9859972019-03-14 17:11:42 -0600143 for basedir in subdirs:
144 for dirpath, _dirnames, filenames in os.walk(basedir):
145 for filename in filenames:
146 if filename.endswith('.proto'):
147 # We have a match, add the file.
148 targets.append(os.path.join(dirpath, filename))
149
Alex Klein098f7982021-03-01 13:15:29 -0700150 cmd = [
151 _get_protoc_command(protoc_version),
152 '--python_out',
153 output,
154 '--proto_path',
155 source,
156 ]
157 cmd.extend(targets)
158
Mike Frysinger45602c72019-09-22 02:15:11 -0400159 result = cros_build_lib.run(
Alex Klein098f7982021-03-01 13:15:29 -0700160 cmd,
161 cwd=source,
162 print_cmd=False,
163 check=False,
164 enter_chroot=protoc_version is ProtocVersion.SDK)
Alex Klein5534f992019-09-16 16:31:23 -0600165
166 if result.returncode:
167 raise GenerationError('Error compiling the proto. See the output for a '
168 'message.')
Alex Kleinf9859972019-03-14 17:11:42 -0600169
170
171def _InstallMissingInits(directory):
172 """Add any __init__.py files not present in the generated protobuf folders."""
Alex Klein098f7982021-03-01 13:15:29 -0700173 logging.info('Adding missing __init__.py files in %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600174 for dirpath, _dirnames, filenames in os.walk(directory):
175 if '__init__.py' not in filenames:
176 osutils.Touch(os.path.join(dirpath, '__init__.py'))
177
178
Alex Klein098f7982021-03-01 13:15:29 -0700179def _PostprocessFiles(directory: str, protoc_version: ProtocVersion):
Alex Kleinf9859972019-03-14 17:11:42 -0600180 """Do postprocessing on the generated files.
181
182 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700183 directory: The root directory containing the generated files that are
Alex Kleinf9859972019-03-14 17:11:42 -0600184 to be processed.
Alex Klein098f7982021-03-01 13:15:29 -0700185 protoc_version: Which protoc is being used to generate the files.
Alex Kleinf9859972019-03-14 17:11:42 -0600186 """
Alex Klein098f7982021-03-01 13:15:29 -0700187 logging.info('Postprocessing: Fix imports in %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600188 # We are using a negative address here (the /address/! portion of the sed
189 # command) to make sure we don't change any imports from protobuf itself.
190 address = '^from google.protobuf'
191 # Find: 'from x import y_pb2 as x_dot_y_pb2'.
192 # "\(^google.protobuf[^ ]*\)" matches the module we're importing from.
193 # - \( and \) are for groups in sed.
194 # - ^google.protobuf prevents changing the import for protobuf's files.
195 # - [^ ] = Not a space. The [:space:] character set is too broad, but would
196 # technically work too.
197 find = r'^from \([^ ]*\) import \([^ ]*\)_pb2 as \([^ ]*\)$'
Alex Klein098f7982021-03-01 13:15:29 -0700198 # Substitute: 'from chromite.api.gen[_sdk].x import y_pb2 as x_dot_y_pb2'.
199 if protoc_version is ProtocVersion.SDK:
200 sub = 'from chromite.api.gen_sdk.\\1 import \\2_pb2 as \\3'
201 else:
202 sub = 'from chromite.api.gen.\\1 import \\2_pb2 as \\3'
203
Alex Klein5534f992019-09-16 16:31:23 -0600204 from_sed = [
205 'sed', '-i',
206 '/%(address)s/!s/%(find)s/%(sub)s/g' % {
207 'address': address,
208 'find': find,
209 'sub': sub
210 }
211 ]
Alex Kleinf9859972019-03-14 17:11:42 -0600212
213 for dirpath, _dirnames, filenames in os.walk(directory):
Alex Klein098f7982021-03-01 13:15:29 -0700214 # Update the imports in the generated files.
Alex Kleinf9859972019-03-14 17:11:42 -0600215 pb2 = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
216 if pb2:
217 cmd = from_sed + pb2
Mike Frysinger45602c72019-09-22 02:15:11 -0400218 cros_build_lib.run(cmd, print_cmd=False)
Alex Kleinf9859972019-03-14 17:11:42 -0600219
220
Alex Klein098f7982021-03-01 13:15:29 -0700221def CompileProto(output: str, protoc_version: ProtocVersion):
Alex Kleinf9859972019-03-14 17:11:42 -0600222 """Compile the Build API protobuf files.
223
224 By default this will compile from infra/proto/src to api/gen. The output
225 directory may be changed, but the imports will always be treated as if it is
226 in the default location.
227
228 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700229 output: The output directory.
230 protoc_version: Which protoc to use for the compile.
Alex Kleinf9859972019-03-14 17:11:42 -0600231 """
Alex Klein098f7982021-03-01 13:15:29 -0700232 source = os.path.join(_get_proto_dir(protoc_version), 'src')
233 protoc_version = protoc_version or ProtocVersion.CHROMITE
Alex Kleinf9859972019-03-14 17:11:42 -0600234
Alex Klein098f7982021-03-01 13:15:29 -0700235 _InstallProtoc(protoc_version)
Alex Kleinf9859972019-03-14 17:11:42 -0600236 _CleanTargetDirectory(output)
Alex Klein098f7982021-03-01 13:15:29 -0700237 _GenerateFiles(source, output, protoc_version)
Alex Kleinf9859972019-03-14 17:11:42 -0600238 _InstallMissingInits(output)
Alex Klein098f7982021-03-01 13:15:29 -0700239 _PostprocessFiles(output, protoc_version)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700240
241
242def GetParser():
Alex Kleinf9859972019-03-14 17:11:42 -0600243 """Build the argument parser."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700244 parser = commandline.ArgumentParser(description=__doc__)
Alex Klein098f7982021-03-01 13:15:29 -0700245 standard_group = parser.add_argument_group(
246 'Committed Bindings',
247 description='Options for generating the bindings in chromite/api/.')
248 standard_group.add_argument(
249 '--chromite',
250 dest='protoc_version',
251 action='append_const',
252 const=ProtocVersion.CHROMITE,
253 help='Generate only the chromite bindings. Generates all by default. The '
254 'chromite bindings are compatible with the version of protobuf in '
255 'chromite/third_party.')
256 standard_group.add_argument(
257 '--sdk',
258 dest='protoc_version',
259 action='append_const',
260 const=ProtocVersion.SDK,
261 help='Generate only the SDK bindings. Generates all by default. The SDK '
262 'bindings are compiled by protoc in the SDK, and is compatible '
263 'with the version of protobuf in the SDK (i.e. the one installed by '
264 'the ebuild).')
265
266 dest_group = parser.add_argument_group(
267 'Out of Tree Bindings',
268 description='Options for generating bindings in a custom location.')
269 dest_group.add_argument(
Alex Klein5534f992019-09-16 16:31:23 -0600270 '--destination',
271 type='path',
Alex Klein098f7982021-03-01 13:15:29 -0700272 help='A directory where a single version of the proto should be '
273 'generated. When not given, the proto generates in all default '
274 'locations instead.')
275 dest_group.add_argument(
276 '--dest-sdk',
277 action='store_const',
278 dest='dest_protoc',
279 default=ProtocVersion.CHROMITE,
280 const=ProtocVersion.SDK,
281 help='Generate the SDK version of the protos in --destination instead of '
282 'the chromite version.')
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700283 return parser
284
285
286def _ParseArguments(argv):
287 """Parse and validate arguments."""
288 parser = GetParser()
289 opts = parser.parse_args(argv)
290
Alex Klein098f7982021-03-01 13:15:29 -0700291 if not opts.protoc_version:
292 opts.protoc_version = [ProtocVersion.CHROMITE, ProtocVersion.SDK]
293
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700294 opts.Freeze()
295 return opts
296
297
298def main(argv):
Alex Kleinf9859972019-03-14 17:11:42 -0600299 opts = _ParseArguments(argv)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700300
Alex Klein098f7982021-03-01 13:15:29 -0700301 if opts.destination:
302 # Destination set, only compile a single version in the destination.
303 try:
304 CompileProto(output=opts.destination, protoc_version=opts.dest_protoc)
305 except Error as e:
306 cros_build_lib.Die('Error compiling bindings to destination: %s', str(e))
307 else:
308 return 0
309
310 if ProtocVersion.CHROMITE in opts.protoc_version:
311 # Compile the chromite bindings.
312 try:
313 CompileProto(
314 output=_get_gen_dir(ProtocVersion.CHROMITE),
315 protoc_version=ProtocVersion.CHROMITE)
316 except Error as e:
317 cros_build_lib.Die('Error compiling chromite bindings: %s', str(e))
318
319 if ProtocVersion.SDK in opts.protoc_version:
320 # Compile the SDK bindings.
321 if not cros_build_lib.IsInsideChroot():
322 # Rerun inside of the SDK instead of trying to map all of the paths.
323 cmd = [
324 os.path.join(constants.CHROOT_SOURCE_ROOT, 'chromite', 'api',
325 'compile_build_api_proto'),
326 '--sdk',
327 ]
328 result = cros_build_lib.run(
329 cmd, print_cmd=False, enter_chroot=True, check=False)
330 return result.returncode
331 else:
332 try:
333 CompileProto(
334 output=_get_gen_dir(ProtocVersion.SDK),
335 protoc_version=ProtocVersion.SDK)
336 except Error as e:
337 cros_build_lib.Die('Error compiling SDK bindings: %s', str(e))