blob: ff6daff7041856b3758bd7271cea8da3d6926257 [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
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
Miriam Polzer713e6542021-08-17 10:58:14 +020093def _CleanTargetDirectory(directory: str):
Alex Kleinf9859972019-03-14 17:11:42 -060094 """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:
Miriam Polzer713e6542021-08-17 10:58:14 +0200103 directory: Path to be cleaned up.
Alex Kleinf9859972019-03-14 17:11:42 -0600104 """
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
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600131 chromeos_config_path = os.path.realpath(
132 os.path.join(constants.SOURCE_ROOT, 'src/config'))
Alex Klein098f7982021-03-01 13:15:29 -0700133
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600134 with tempfile.TemporaryDirectory() as tempdir:
135 if not os.path.exists(chromeos_config_path):
136 chromeos_config_path = os.path.join(tempdir, 'config')
Alex Klein5534f992019-09-16 16:31:23 -0600137
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600138 logging.info('Creating shallow clone of chromiumos/config')
139 git.Clone(chromeos_config_path,
140 '%s/chromiumos/config' % constants.EXTERNAL_GOB_URL,
141 depth=1
142 )
143
Andrew Lamb59ed32e2021-07-26 15:14:37 -0600144 # Only compile the subset we need for the API.
145 subdirs = [
146 os.path.join(source, 'chromite'),
147 os.path.join(source, 'chromiumos'),
148 os.path.join(source, 'client'),
149 os.path.join(source, 'config'),
150 os.path.join(source, 'test_platform'),
151 os.path.join(source, 'device'),
152 os.path.join(chromeos_config_path, 'proto/chromiumos'),
153 ]
154 for basedir in subdirs:
155 for dirpath, _dirnames, filenames in os.walk(basedir):
156 for filename in filenames:
157 if filename.endswith('.proto'):
158 # We have a match, add the file.
159 targets.append(os.path.join(dirpath, filename))
160
Sean McAllister6a5eaa02021-05-26 10:47:14 -0600161 cmd = [
162 _get_protoc_command(protoc_version),
163 '-I',
164 os.path.join(chromeos_config_path, 'proto'),
165 '--python_out',
166 output,
167 '--proto_path',
168 source,
169 ]
170 cmd.extend(targets)
171
172 result = cros_build_lib.run(
173 cmd,
174 cwd=source,
175 print_cmd=False,
176 check=False,
177 enter_chroot=protoc_version is ProtocVersion.SDK)
178
179 if result.returncode:
180 raise GenerationError('Error compiling the proto. See the output for a '
181 'message.')
Alex Kleinf9859972019-03-14 17:11:42 -0600182
183
184def _InstallMissingInits(directory):
185 """Add any __init__.py files not present in the generated protobuf folders."""
Alex Klein098f7982021-03-01 13:15:29 -0700186 logging.info('Adding missing __init__.py files in %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600187 for dirpath, _dirnames, filenames in os.walk(directory):
188 if '__init__.py' not in filenames:
189 osutils.Touch(os.path.join(dirpath, '__init__.py'))
190
191
Alex Klein098f7982021-03-01 13:15:29 -0700192def _PostprocessFiles(directory: str, protoc_version: ProtocVersion):
Alex Kleinf9859972019-03-14 17:11:42 -0600193 """Do postprocessing on the generated files.
194
195 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700196 directory: The root directory containing the generated files that are
Alex Kleinf9859972019-03-14 17:11:42 -0600197 to be processed.
Alex Klein098f7982021-03-01 13:15:29 -0700198 protoc_version: Which protoc is being used to generate the files.
Alex Kleinf9859972019-03-14 17:11:42 -0600199 """
Alex Klein098f7982021-03-01 13:15:29 -0700200 logging.info('Postprocessing: Fix imports in %s.', directory)
Alex Kleinf9859972019-03-14 17:11:42 -0600201 # We are using a negative address here (the /address/! portion of the sed
202 # command) to make sure we don't change any imports from protobuf itself.
Alex Kleind20d8162021-06-21 12:40:44 -0600203 address = '^from google.protobuf'
Alex Kleinf9859972019-03-14 17:11:42 -0600204 # Find: 'from x import y_pb2 as x_dot_y_pb2'.
205 # "\(^google.protobuf[^ ]*\)" matches the module we're importing from.
206 # - \( and \) are for groups in sed.
207 # - ^google.protobuf prevents changing the import for protobuf's files.
208 # - [^ ] = Not a space. The [:space:] character set is too broad, but would
209 # technically work too.
210 find = r'^from \([^ ]*\) import \([^ ]*\)_pb2 as \([^ ]*\)$'
Alex Klein098f7982021-03-01 13:15:29 -0700211 # Substitute: 'from chromite.api.gen[_sdk].x import y_pb2 as x_dot_y_pb2'.
212 if protoc_version is ProtocVersion.SDK:
213 sub = 'from chromite.api.gen_sdk.\\1 import \\2_pb2 as \\3'
214 else:
215 sub = 'from chromite.api.gen.\\1 import \\2_pb2 as \\3'
216
Alex Klein5534f992019-09-16 16:31:23 -0600217 from_sed = [
218 'sed', '-i',
219 '/%(address)s/!s/%(find)s/%(sub)s/g' % {
220 'address': address,
221 'find': find,
222 'sub': sub
223 }
224 ]
Alex Kleinf9859972019-03-14 17:11:42 -0600225
Alex Kleind20d8162021-06-21 12:40:44 -0600226 seds = [from_sed]
227 if protoc_version is ProtocVersion.CHROMITE:
228 # We also need to change the google.protobuf imports to point directly
229 # at the chromite.third_party version of the library.
230 # The SDK version of the proto is meant to be used with the protobuf
231 # libraries installed in the SDK, so leave those as google.protobuf.
232 g_p_address = '^from google.protobuf'
233 g_p_find = r'from \([^ ]*\) import \(.*\)$'
234 g_p_sub = 'from chromite.third_party.\\1 import \\2'
235 google_protobuf_sed = [
236 'sed', '-i',
237 '/%(address)s/s/%(find)s/%(sub)s/g' % {
238 'address': g_p_address,
239 'find': g_p_find,
240 'sub': g_p_sub
241 }
242 ]
243 seds.append(google_protobuf_sed)
244
Alex Kleinf9859972019-03-14 17:11:42 -0600245 for dirpath, _dirnames, filenames in os.walk(directory):
Alex Klein098f7982021-03-01 13:15:29 -0700246 # Update the imports in the generated files.
Alex Kleinf9859972019-03-14 17:11:42 -0600247 pb2 = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
248 if pb2:
Alex Kleind20d8162021-06-21 12:40:44 -0600249 for sed in seds:
250 cmd = sed + pb2
251 cros_build_lib.run(cmd, print_cmd=False)
Alex Kleinf9859972019-03-14 17:11:42 -0600252
253
Alex Klein098f7982021-03-01 13:15:29 -0700254def CompileProto(output: str, protoc_version: ProtocVersion):
Alex Kleinf9859972019-03-14 17:11:42 -0600255 """Compile the Build API protobuf files.
256
257 By default this will compile from infra/proto/src to api/gen. The output
258 directory may be changed, but the imports will always be treated as if it is
259 in the default location.
260
261 Args:
Alex Klein098f7982021-03-01 13:15:29 -0700262 output: The output directory.
263 protoc_version: Which protoc to use for the compile.
Alex Kleinf9859972019-03-14 17:11:42 -0600264 """
Alex Klein098f7982021-03-01 13:15:29 -0700265 source = os.path.join(_get_proto_dir(protoc_version), 'src')
266 protoc_version = protoc_version or ProtocVersion.CHROMITE
Alex Kleinf9859972019-03-14 17:11:42 -0600267
Alex Klein098f7982021-03-01 13:15:29 -0700268 _InstallProtoc(protoc_version)
Alex Kleinf9859972019-03-14 17:11:42 -0600269 _CleanTargetDirectory(output)
Alex Klein098f7982021-03-01 13:15:29 -0700270 _GenerateFiles(source, output, protoc_version)
Alex Kleinf9859972019-03-14 17:11:42 -0600271 _InstallMissingInits(output)
Alex Klein098f7982021-03-01 13:15:29 -0700272 _PostprocessFiles(output, protoc_version)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700273
274
275def GetParser():
Alex Kleinf9859972019-03-14 17:11:42 -0600276 """Build the argument parser."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700277 parser = commandline.ArgumentParser(description=__doc__)
Alex Klein098f7982021-03-01 13:15:29 -0700278 standard_group = parser.add_argument_group(
279 'Committed Bindings',
280 description='Options for generating the bindings in chromite/api/.')
281 standard_group.add_argument(
282 '--chromite',
283 dest='protoc_version',
284 action='append_const',
285 const=ProtocVersion.CHROMITE,
286 help='Generate only the chromite bindings. Generates all by default. The '
287 'chromite bindings are compatible with the version of protobuf in '
288 'chromite/third_party.')
289 standard_group.add_argument(
290 '--sdk',
291 dest='protoc_version',
292 action='append_const',
293 const=ProtocVersion.SDK,
294 help='Generate only the SDK bindings. Generates all by default. The SDK '
295 'bindings are compiled by protoc in the SDK, and is compatible '
296 'with the version of protobuf in the SDK (i.e. the one installed by '
297 'the ebuild).')
298
299 dest_group = parser.add_argument_group(
300 'Out of Tree Bindings',
301 description='Options for generating bindings in a custom location.')
302 dest_group.add_argument(
Alex Klein5534f992019-09-16 16:31:23 -0600303 '--destination',
304 type='path',
Alex Klein098f7982021-03-01 13:15:29 -0700305 help='A directory where a single version of the proto should be '
306 'generated. When not given, the proto generates in all default '
307 'locations instead.')
308 dest_group.add_argument(
309 '--dest-sdk',
310 action='store_const',
311 dest='dest_protoc',
312 default=ProtocVersion.CHROMITE,
313 const=ProtocVersion.SDK,
314 help='Generate the SDK version of the protos in --destination instead of '
315 'the chromite version.')
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700316 return parser
317
318
319def _ParseArguments(argv):
320 """Parse and validate arguments."""
321 parser = GetParser()
322 opts = parser.parse_args(argv)
323
Alex Klein098f7982021-03-01 13:15:29 -0700324 if not opts.protoc_version:
325 opts.protoc_version = [ProtocVersion.CHROMITE, ProtocVersion.SDK]
326
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700327 opts.Freeze()
328 return opts
329
330
331def main(argv):
Alex Kleinf9859972019-03-14 17:11:42 -0600332 opts = _ParseArguments(argv)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700333
Alex Klein098f7982021-03-01 13:15:29 -0700334 if opts.destination:
335 # Destination set, only compile a single version in the destination.
336 try:
337 CompileProto(output=opts.destination, protoc_version=opts.dest_protoc)
338 except Error as e:
339 cros_build_lib.Die('Error compiling bindings to destination: %s', str(e))
340 else:
341 return 0
342
343 if ProtocVersion.CHROMITE in opts.protoc_version:
344 # Compile the chromite bindings.
345 try:
346 CompileProto(
347 output=_get_gen_dir(ProtocVersion.CHROMITE),
348 protoc_version=ProtocVersion.CHROMITE)
349 except Error as e:
350 cros_build_lib.Die('Error compiling chromite bindings: %s', str(e))
351
352 if ProtocVersion.SDK in opts.protoc_version:
353 # Compile the SDK bindings.
354 if not cros_build_lib.IsInsideChroot():
355 # Rerun inside of the SDK instead of trying to map all of the paths.
356 cmd = [
357 os.path.join(constants.CHROOT_SOURCE_ROOT, 'chromite', 'api',
358 'compile_build_api_proto'),
359 '--sdk',
360 ]
361 result = cros_build_lib.run(
362 cmd, print_cmd=False, enter_chroot=True, check=False)
363 return result.returncode
364 else:
365 try:
366 CompileProto(
367 output=_get_gen_dir(ProtocVersion.SDK),
368 protoc_version=ProtocVersion.SDK)
369 except Error as e:
370 cros_build_lib.Die('Error compiling SDK bindings: %s', str(e))