blob: e55558c0a518b879a6304554b5bdd63306d3043d [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
12
13from chromite.lib import commandline
Alex Kleinc33c1912019-02-15 10:29:13 -070014from chromite.lib import constants
Alex Kleinf4dc4f52018-12-05 13:55:12 -070015from chromite.lib import cros_build_lib
Alex Klein5534f992019-09-16 16:31:23 -060016from chromite.lib import cros_logging as logging
Alex Kleinf9859972019-03-14 17:11:42 -060017from chromite.lib import osutils
18
Mike Frysingeref94e4c2020-02-10 23:59:54 -050019
Alex Kleinf9859972019-03-14 17:11:42 -060020_CIPD_ROOT = os.path.join(constants.CHROMITE_DIR, '.cipd_bin')
Alex Kleinf9859972019-03-14 17:11:42 -060021
Alex Klein098f7982021-03-01 13:15:29 -070022# Chromite's protobuf library version (third_party/google/protobuf).
Mike Nicholse366e7a2021-02-22 18:14:57 -070023PROTOC_VERSION = '3.13.0'
Alex Kleinf9859972019-03-14 17:11:42 -060024
25
Alex Klein5534f992019-09-16 16:31:23 -060026class Error(Exception):
27 """Base error class for the module."""
28
29
30class GenerationError(Error):
31 """A failure we can't recover from."""
32
33
Alex Klein098f7982021-03-01 13:15:29 -070034@enum.unique
35class ProtocVersion(enum.Enum):
36 """Enum for possible protoc versions."""
37 # The SDK version of the bindings use the protoc in the SDK, and so is
38 # compatible with the protobuf library in the SDK, i.e. the one installed
39 # via the ebuild.
40 SDK = enum.auto()
41 # The Chromite version of the bindings uses a protoc binary downloaded from
42 # CIPD that matches the version of the protobuf library in
43 # chromite/third_party/google/protobuf.
44 CHROMITE = enum.auto()
45
46
47def _get_gen_dir(protoc_version: ProtocVersion):
48 """Get the chromite/api directory path."""
49 if protoc_version is ProtocVersion.SDK:
50 return os.path.join(constants.CHROMITE_DIR, 'api', 'gen_sdk')
51 else:
52 return os.path.join(constants.CHROMITE_DIR, 'api', 'gen')
53
54
55def _get_protoc_command(protoc_version: ProtocVersion):
56 """Get the protoc command for the target protoc."""
57 if protoc_version is ProtocVersion.SDK:
58 return 'protoc'
59 else:
60 return os.path.join(_CIPD_ROOT, 'protoc')
61
62
63def _get_proto_dir(_protoc_version):
64 """Get the proto directory for the target protoc."""
65 return os.path.join(constants.CHROMITE_DIR, 'infra', 'proto')
66
67
68def _InstallProtoc(protoc_version: ProtocVersion):
Alex Kleinf9859972019-03-14 17:11:42 -060069 """Install protoc from CIPD."""
Alex Klein098f7982021-03-01 13:15:29 -070070 if protoc_version is not ProtocVersion.CHROMITE:
71 return
72
Alex Klein5534f992019-09-16 16:31:23 -060073 logging.info('Installing protoc.')
Alex Kleinf9859972019-03-14 17:11:42 -060074 cmd = ['cipd', 'ensure']
75 # Clean up the output.
76 cmd.extend(['-log-level', 'warning'])
77 # Set the install location.
78 cmd.extend(['-root', _CIPD_ROOT])
79
80 ensure_content = ('infra/tools/protoc/${platform} '
81 'protobuf_version:v%s' % PROTOC_VERSION)
82 with osutils.TempDir() as tempdir:
83 ensure_file = os.path.join(tempdir, 'cipd_ensure_file')
84 osutils.WriteFile(ensure_file, ensure_content)
85
86 cmd.extend(['-ensure-file', ensure_file])
87
Mike Frysinger45602c72019-09-22 02:15:11 -040088 cros_build_lib.run(cmd, cwd=constants.CHROMITE_DIR, print_cmd=False)
Alex Klein5534f992019-09-16 16:31:23 -060089
Alex Kleinf9859972019-03-14 17:11:42 -060090
91def _CleanTargetDirectory(directory):
92 """Remove any existing generated files in the directory.
93
94 This clean only removes the generated files to avoid accidentally destroying
95 __init__.py customizations down the line. That will leave otherwise empty
96 directories in place if things get moved. Neither case is relevant at the
97 time of writing, but lingering empty directories seemed better than
98 diagnosing accidental __init__.py changes.
99
100 Args:
101 directory (str): Path to be cleaned up.
102 """
Alex Klein098f7982021-03-01 13:15:29 -0700103 logging.info('Cleaning old files from %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600104 for dirpath, _dirnames, filenames in os.walk(directory):
105 old = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
Alex Klein5534f992019-09-16 16:31:23 -0600106 # Remove empty init files to clean up otherwise empty directories.
107 if '__init__.py' in filenames:
108 init = os.path.join(dirpath, '__init__.py')
109 if not osutils.ReadFile(init):
110 old.append(init)
111
Alex Kleinf9859972019-03-14 17:11:42 -0600112 for current in old:
113 osutils.SafeUnlink(current)
114
Alex Klein5534f992019-09-16 16:31:23 -0600115
Alex Klein098f7982021-03-01 13:15:29 -0700116def _GenerateFiles(source: str, output: str, protoc_version: ProtocVersion):
Alex Kleinf9859972019-03-14 17:11:42 -0600117 """Generate the proto files from the |source| tree into |output|.
118
119 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700120 source: Path to the proto source root directory.
121 output: Path to the output root directory.
122 protoc_version: Which protoc to use.
Alex Kleinf9859972019-03-14 17:11:42 -0600123 """
Alex Klein098f7982021-03-01 13:15:29 -0700124 logging.info('Generating files to %s.', output)
125 osutils.SafeMakedirs(output)
126
Alex Kleinf9859972019-03-14 17:11:42 -0600127 targets = []
128
129 # Only compile the subset we need for the API.
Alex Klein5534f992019-09-16 16:31:23 -0600130 subdirs = [
131 os.path.join(source, 'chromite'),
132 os.path.join(source, 'chromiumos'),
LaMont Jones4a78ec52020-07-27 17:23:45 -0600133 os.path.join(source, 'client'),
Andrew Lamb2cbe4582019-10-29 11:56:25 -0600134 os.path.join(source, 'config'),
Xinan Lin2a40ea72020-01-10 23:50:27 +0000135 os.path.join(source, 'test_platform'),
136 os.path.join(source, 'device')
Alex Klein5534f992019-09-16 16:31:23 -0600137 ]
Alex Kleinf9859972019-03-14 17:11:42 -0600138 for basedir in subdirs:
139 for dirpath, _dirnames, filenames in os.walk(basedir):
140 for filename in filenames:
141 if filename.endswith('.proto'):
142 # We have a match, add the file.
143 targets.append(os.path.join(dirpath, filename))
144
Alex Klein098f7982021-03-01 13:15:29 -0700145 cmd = [
146 _get_protoc_command(protoc_version),
147 '--python_out',
148 output,
149 '--proto_path',
150 source,
151 ]
152 cmd.extend(targets)
153
Mike Frysinger45602c72019-09-22 02:15:11 -0400154 result = cros_build_lib.run(
Alex Klein098f7982021-03-01 13:15:29 -0700155 cmd,
156 cwd=source,
157 print_cmd=False,
158 check=False,
159 enter_chroot=protoc_version is ProtocVersion.SDK)
Alex Klein5534f992019-09-16 16:31:23 -0600160
161 if result.returncode:
162 raise GenerationError('Error compiling the proto. See the output for a '
163 'message.')
Alex Kleinf9859972019-03-14 17:11:42 -0600164
165
166def _InstallMissingInits(directory):
167 """Add any __init__.py files not present in the generated protobuf folders."""
Alex Klein098f7982021-03-01 13:15:29 -0700168 logging.info('Adding missing __init__.py files in %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600169 for dirpath, _dirnames, filenames in os.walk(directory):
170 if '__init__.py' not in filenames:
171 osutils.Touch(os.path.join(dirpath, '__init__.py'))
172
173
Alex Klein098f7982021-03-01 13:15:29 -0700174def _PostprocessFiles(directory: str, protoc_version: ProtocVersion):
Alex Kleinf9859972019-03-14 17:11:42 -0600175 """Do postprocessing on the generated files.
176
177 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700178 directory: The root directory containing the generated files that are
Alex Kleinf9859972019-03-14 17:11:42 -0600179 to be processed.
Alex Klein098f7982021-03-01 13:15:29 -0700180 protoc_version: Which protoc is being used to generate the files.
Alex Kleinf9859972019-03-14 17:11:42 -0600181 """
Alex Klein098f7982021-03-01 13:15:29 -0700182 logging.info('Postprocessing: Fix imports in %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600183 # We are using a negative address here (the /address/! portion of the sed
184 # command) to make sure we don't change any imports from protobuf itself.
185 address = '^from google.protobuf'
186 # Find: 'from x import y_pb2 as x_dot_y_pb2'.
187 # "\(^google.protobuf[^ ]*\)" matches the module we're importing from.
188 # - \( and \) are for groups in sed.
189 # - ^google.protobuf prevents changing the import for protobuf's files.
190 # - [^ ] = Not a space. The [:space:] character set is too broad, but would
191 # technically work too.
192 find = r'^from \([^ ]*\) import \([^ ]*\)_pb2 as \([^ ]*\)$'
Alex Klein098f7982021-03-01 13:15:29 -0700193 # Substitute: 'from chromite.api.gen[_sdk].x import y_pb2 as x_dot_y_pb2'.
194 if protoc_version is ProtocVersion.SDK:
195 sub = 'from chromite.api.gen_sdk.\\1 import \\2_pb2 as \\3'
196 else:
197 sub = 'from chromite.api.gen.\\1 import \\2_pb2 as \\3'
198
Alex Klein5534f992019-09-16 16:31:23 -0600199 from_sed = [
200 'sed', '-i',
201 '/%(address)s/!s/%(find)s/%(sub)s/g' % {
202 'address': address,
203 'find': find,
204 'sub': sub
205 }
206 ]
Alex Kleinf9859972019-03-14 17:11:42 -0600207
208 for dirpath, _dirnames, filenames in os.walk(directory):
Alex Klein098f7982021-03-01 13:15:29 -0700209 # Update the imports in the generated files.
Alex Kleinf9859972019-03-14 17:11:42 -0600210 pb2 = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
211 if pb2:
212 cmd = from_sed + pb2
Mike Frysinger45602c72019-09-22 02:15:11 -0400213 cros_build_lib.run(cmd, print_cmd=False)
Alex Kleinf9859972019-03-14 17:11:42 -0600214
215
Alex Klein098f7982021-03-01 13:15:29 -0700216def CompileProto(output: str, protoc_version: ProtocVersion):
Alex Kleinf9859972019-03-14 17:11:42 -0600217 """Compile the Build API protobuf files.
218
219 By default this will compile from infra/proto/src to api/gen. The output
220 directory may be changed, but the imports will always be treated as if it is
221 in the default location.
222
223 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700224 output: The output directory.
225 protoc_version: Which protoc to use for the compile.
Alex Kleinf9859972019-03-14 17:11:42 -0600226 """
Alex Klein098f7982021-03-01 13:15:29 -0700227 source = os.path.join(_get_proto_dir(protoc_version), 'src')
228 protoc_version = protoc_version or ProtocVersion.CHROMITE
Alex Kleinf9859972019-03-14 17:11:42 -0600229
Alex Klein098f7982021-03-01 13:15:29 -0700230 _InstallProtoc(protoc_version)
Alex Kleinf9859972019-03-14 17:11:42 -0600231 _CleanTargetDirectory(output)
Alex Klein098f7982021-03-01 13:15:29 -0700232 _GenerateFiles(source, output, protoc_version)
Alex Kleinf9859972019-03-14 17:11:42 -0600233 _InstallMissingInits(output)
Alex Klein098f7982021-03-01 13:15:29 -0700234 _PostprocessFiles(output, protoc_version)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700235
236
237def GetParser():
Alex Kleinf9859972019-03-14 17:11:42 -0600238 """Build the argument parser."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700239 parser = commandline.ArgumentParser(description=__doc__)
Alex Klein098f7982021-03-01 13:15:29 -0700240 standard_group = parser.add_argument_group(
241 'Committed Bindings',
242 description='Options for generating the bindings in chromite/api/.')
243 standard_group.add_argument(
244 '--chromite',
245 dest='protoc_version',
246 action='append_const',
247 const=ProtocVersion.CHROMITE,
248 help='Generate only the chromite bindings. Generates all by default. The '
249 'chromite bindings are compatible with the version of protobuf in '
250 'chromite/third_party.')
251 standard_group.add_argument(
252 '--sdk',
253 dest='protoc_version',
254 action='append_const',
255 const=ProtocVersion.SDK,
256 help='Generate only the SDK bindings. Generates all by default. The SDK '
257 'bindings are compiled by protoc in the SDK, and is compatible '
258 'with the version of protobuf in the SDK (i.e. the one installed by '
259 'the ebuild).')
260
261 dest_group = parser.add_argument_group(
262 'Out of Tree Bindings',
263 description='Options for generating bindings in a custom location.')
264 dest_group.add_argument(
Alex Klein5534f992019-09-16 16:31:23 -0600265 '--destination',
266 type='path',
Alex Klein098f7982021-03-01 13:15:29 -0700267 help='A directory where a single version of the proto should be '
268 'generated. When not given, the proto generates in all default '
269 'locations instead.')
270 dest_group.add_argument(
271 '--dest-sdk',
272 action='store_const',
273 dest='dest_protoc',
274 default=ProtocVersion.CHROMITE,
275 const=ProtocVersion.SDK,
276 help='Generate the SDK version of the protos in --destination instead of '
277 'the chromite version.')
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700278 return parser
279
280
281def _ParseArguments(argv):
282 """Parse and validate arguments."""
283 parser = GetParser()
284 opts = parser.parse_args(argv)
285
Alex Klein098f7982021-03-01 13:15:29 -0700286 if not opts.protoc_version:
287 opts.protoc_version = [ProtocVersion.CHROMITE, ProtocVersion.SDK]
288
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700289 opts.Freeze()
290 return opts
291
292
293def main(argv):
Alex Kleinf9859972019-03-14 17:11:42 -0600294 opts = _ParseArguments(argv)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700295
Alex Klein098f7982021-03-01 13:15:29 -0700296 if opts.destination:
297 # Destination set, only compile a single version in the destination.
298 try:
299 CompileProto(output=opts.destination, protoc_version=opts.dest_protoc)
300 except Error as e:
301 cros_build_lib.Die('Error compiling bindings to destination: %s', str(e))
302 else:
303 return 0
304
305 if ProtocVersion.CHROMITE in opts.protoc_version:
306 # Compile the chromite bindings.
307 try:
308 CompileProto(
309 output=_get_gen_dir(ProtocVersion.CHROMITE),
310 protoc_version=ProtocVersion.CHROMITE)
311 except Error as e:
312 cros_build_lib.Die('Error compiling chromite bindings: %s', str(e))
313
314 if ProtocVersion.SDK in opts.protoc_version:
315 # Compile the SDK bindings.
316 if not cros_build_lib.IsInsideChroot():
317 # Rerun inside of the SDK instead of trying to map all of the paths.
318 cmd = [
319 os.path.join(constants.CHROOT_SOURCE_ROOT, 'chromite', 'api',
320 'compile_build_api_proto'),
321 '--sdk',
322 ]
323 result = cros_build_lib.run(
324 cmd, print_cmd=False, enter_chroot=True, check=False)
325 return result.returncode
326 else:
327 try:
328 CompileProto(
329 output=_get_gen_dir(ProtocVersion.SDK),
330 protoc_version=ProtocVersion.SDK)
331 except Error as e:
332 cros_build_lib.Die('Error compiling SDK bindings: %s', str(e))