blob: e1b641e27a73608674c38f8d600a8bc28c698325 [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
13import os
Mike Frysingeref94e4c2020-02-10 23:59:54 -050014import sys
Alex Kleinf4dc4f52018-12-05 13:55:12 -070015
16from chromite.lib import commandline
Alex Kleinc33c1912019-02-15 10:29:13 -070017from chromite.lib import constants
Alex Kleinf4dc4f52018-12-05 13:55:12 -070018from chromite.lib import cros_build_lib
Alex Klein5534f992019-09-16 16:31:23 -060019from chromite.lib import cros_logging as logging
Alex Kleinf9859972019-03-14 17:11:42 -060020from chromite.lib import osutils
21
Mike Frysingeref94e4c2020-02-10 23:59:54 -050022
23assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
24
25
Alex Kleinf9859972019-03-14 17:11:42 -060026_API_DIR = os.path.join(constants.CHROMITE_DIR, 'api')
27_CIPD_ROOT = os.path.join(constants.CHROMITE_DIR, '.cipd_bin')
28_PROTOC = os.path.join(_CIPD_ROOT, 'protoc')
29_PROTO_DIR = os.path.join(constants.CHROMITE_DIR, 'infra', 'proto')
30
31PROTOC_VERSION = '3.6.1'
32
33
Alex Klein5534f992019-09-16 16:31:23 -060034class Error(Exception):
35 """Base error class for the module."""
36
37
38class GenerationError(Error):
39 """A failure we can't recover from."""
40
41
Alex Kleinf9859972019-03-14 17:11:42 -060042def _InstallProtoc():
43 """Install protoc from CIPD."""
Alex Klein5534f992019-09-16 16:31:23 -060044 logging.info('Installing protoc.')
Alex Kleinf9859972019-03-14 17:11:42 -060045 cmd = ['cipd', 'ensure']
46 # Clean up the output.
47 cmd.extend(['-log-level', 'warning'])
48 # Set the install location.
49 cmd.extend(['-root', _CIPD_ROOT])
50
51 ensure_content = ('infra/tools/protoc/${platform} '
52 'protobuf_version:v%s' % PROTOC_VERSION)
53 with osutils.TempDir() as tempdir:
54 ensure_file = os.path.join(tempdir, 'cipd_ensure_file')
55 osutils.WriteFile(ensure_file, ensure_content)
56
57 cmd.extend(['-ensure-file', ensure_file])
58
Mike Frysinger45602c72019-09-22 02:15:11 -040059 cros_build_lib.run(cmd, cwd=constants.CHROMITE_DIR, print_cmd=False)
Alex Klein5534f992019-09-16 16:31:23 -060060
Alex Kleinf9859972019-03-14 17:11:42 -060061
62def _CleanTargetDirectory(directory):
63 """Remove any existing generated files in the directory.
64
65 This clean only removes the generated files to avoid accidentally destroying
66 __init__.py customizations down the line. That will leave otherwise empty
67 directories in place if things get moved. Neither case is relevant at the
68 time of writing, but lingering empty directories seemed better than
69 diagnosing accidental __init__.py changes.
70
71 Args:
72 directory (str): Path to be cleaned up.
73 """
Alex Klein5534f992019-09-16 16:31:23 -060074 logging.info('Cleaning old files.')
Alex Kleinf9859972019-03-14 17:11:42 -060075 for dirpath, _dirnames, filenames in os.walk(directory):
76 old = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
Alex Klein5534f992019-09-16 16:31:23 -060077 # Remove empty init files to clean up otherwise empty directories.
78 if '__init__.py' in filenames:
79 init = os.path.join(dirpath, '__init__.py')
80 if not osutils.ReadFile(init):
81 old.append(init)
82
Alex Kleinf9859972019-03-14 17:11:42 -060083 for current in old:
84 osutils.SafeUnlink(current)
85
Alex Klein5534f992019-09-16 16:31:23 -060086
Alex Kleinf9859972019-03-14 17:11:42 -060087def _GenerateFiles(source, output):
88 """Generate the proto files from the |source| tree into |output|.
89
90 Args:
91 source (str): Path to the proto source root directory.
92 output (str): Path to the output root directory.
93 """
Alex Klein5534f992019-09-16 16:31:23 -060094 logging.info('Generating files.')
Alex Kleinf9859972019-03-14 17:11:42 -060095 targets = []
96
97 # Only compile the subset we need for the API.
Alex Klein5534f992019-09-16 16:31:23 -060098 subdirs = [
99 os.path.join(source, 'chromite'),
100 os.path.join(source, 'chromiumos'),
LaMont Jones4a78ec52020-07-27 17:23:45 -0600101 os.path.join(source, 'client'),
Andrew Lamb2cbe4582019-10-29 11:56:25 -0600102 os.path.join(source, 'config'),
Xinan Lin2a40ea72020-01-10 23:50:27 +0000103 os.path.join(source, 'test_platform'),
104 os.path.join(source, 'device')
Alex Klein5534f992019-09-16 16:31:23 -0600105 ]
Alex Kleinf9859972019-03-14 17:11:42 -0600106 for basedir in subdirs:
107 for dirpath, _dirnames, filenames in os.walk(basedir):
108 for filename in filenames:
109 if filename.endswith('.proto'):
110 # We have a match, add the file.
111 targets.append(os.path.join(dirpath, filename))
112
Alex Klein5534f992019-09-16 16:31:23 -0600113 cmd = [_PROTOC, '--python_out', output, '--proto_path', source] + targets
Mike Frysinger45602c72019-09-22 02:15:11 -0400114 result = cros_build_lib.run(
Mike Frysingerf5a3b2d2019-12-12 14:36:17 -0500115 cmd, cwd=source, print_cmd=False, check=False)
Alex Klein5534f992019-09-16 16:31:23 -0600116
117 if result.returncode:
118 raise GenerationError('Error compiling the proto. See the output for a '
119 'message.')
Alex Kleinf9859972019-03-14 17:11:42 -0600120
121
122def _InstallMissingInits(directory):
123 """Add any __init__.py files not present in the generated protobuf folders."""
Alex Klein5534f992019-09-16 16:31:23 -0600124 logging.info('Adding missing __init__.py files.')
Alex Kleinf9859972019-03-14 17:11:42 -0600125 for dirpath, _dirnames, filenames in os.walk(directory):
126 if '__init__.py' not in filenames:
127 osutils.Touch(os.path.join(dirpath, '__init__.py'))
128
129
130def _PostprocessFiles(directory):
131 """Do postprocessing on the generated files.
132
133 Args:
134 directory (str): The root directory containing the generated files that are
135 to be processed.
136 """
Alex Klein5534f992019-09-16 16:31:23 -0600137 logging.info('Postprocessing: Fix imports.')
Alex Kleinf9859972019-03-14 17:11:42 -0600138 # We are using a negative address here (the /address/! portion of the sed
139 # command) to make sure we don't change any imports from protobuf itself.
140 address = '^from google.protobuf'
141 # Find: 'from x import y_pb2 as x_dot_y_pb2'.
142 # "\(^google.protobuf[^ ]*\)" matches the module we're importing from.
143 # - \( and \) are for groups in sed.
144 # - ^google.protobuf prevents changing the import for protobuf's files.
145 # - [^ ] = Not a space. The [:space:] character set is too broad, but would
146 # technically work too.
147 find = r'^from \([^ ]*\) import \([^ ]*\)_pb2 as \([^ ]*\)$'
148 # Substitute: 'from chromite.api.gen.x import y_pb2 as x_dot_y_pb2'.
149 sub = 'from chromite.api.gen.\\1 import \\2_pb2 as \\3'
Alex Klein5534f992019-09-16 16:31:23 -0600150 from_sed = [
151 'sed', '-i',
152 '/%(address)s/!s/%(find)s/%(sub)s/g' % {
153 'address': address,
154 'find': find,
155 'sub': sub
156 }
157 ]
Alex Kleinf9859972019-03-14 17:11:42 -0600158
159 for dirpath, _dirnames, filenames in os.walk(directory):
160 # Update the
161 pb2 = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
162 if pb2:
163 cmd = from_sed + pb2
Mike Frysinger45602c72019-09-22 02:15:11 -0400164 cros_build_lib.run(cmd, print_cmd=False)
Alex Kleinf9859972019-03-14 17:11:42 -0600165
166
167def CompileProto(output=None):
168 """Compile the Build API protobuf files.
169
170 By default this will compile from infra/proto/src to api/gen. The output
171 directory may be changed, but the imports will always be treated as if it is
172 in the default location.
173
174 Args:
175 output (str|None): The output directory.
176 """
177 source = os.path.join(_PROTO_DIR, 'src')
178 output = output or os.path.join(_API_DIR, 'gen')
179
180 _InstallProtoc()
181 _CleanTargetDirectory(output)
182 _GenerateFiles(source, output)
183 _InstallMissingInits(output)
184 _PostprocessFiles(output)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700185
186
187def GetParser():
Alex Kleinf9859972019-03-14 17:11:42 -0600188 """Build the argument parser."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700189 parser = commandline.ArgumentParser(description=__doc__)
Alex Klein5534f992019-09-16 16:31:23 -0600190 parser.add_argument(
191 '--destination',
192 type='path',
193 help='The directory where the proto should be generated. Defaults to '
194 'the correct directory for the API.')
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700195 return parser
196
197
198def _ParseArguments(argv):
199 """Parse and validate arguments."""
200 parser = GetParser()
201 opts = parser.parse_args(argv)
202
203 opts.Freeze()
204 return opts
205
206
207def main(argv):
Alex Kleinf9859972019-03-14 17:11:42 -0600208 opts = _ParseArguments(argv)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700209
Alex Klein5534f992019-09-16 16:31:23 -0600210 try:
211 CompileProto(output=opts.destination)
212 except Error as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400213 logging.error(e)
Alex Klein5534f992019-09-16 16:31:23 -0600214 return 1