blob: 7fc2dbc1c3e7beb2f620bae7038779a638d401bd [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
14
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
Alex Klein5534f992019-09-16 16:31:23 -060018from chromite.lib import cros_logging as logging
Alex Kleinf9859972019-03-14 17:11:42 -060019from chromite.lib import osutils
20
21_API_DIR = os.path.join(constants.CHROMITE_DIR, 'api')
22_CIPD_ROOT = os.path.join(constants.CHROMITE_DIR, '.cipd_bin')
23_PROTOC = os.path.join(_CIPD_ROOT, 'protoc')
24_PROTO_DIR = os.path.join(constants.CHROMITE_DIR, 'infra', 'proto')
25
26PROTOC_VERSION = '3.6.1'
27
28
Alex Klein5534f992019-09-16 16:31:23 -060029class Error(Exception):
30 """Base error class for the module."""
31
32
33class GenerationError(Error):
34 """A failure we can't recover from."""
35
36
Alex Kleinf9859972019-03-14 17:11:42 -060037def _InstallProtoc():
38 """Install protoc from CIPD."""
Alex Klein5534f992019-09-16 16:31:23 -060039 logging.info('Installing protoc.')
Alex Kleinf9859972019-03-14 17:11:42 -060040 cmd = ['cipd', 'ensure']
41 # Clean up the output.
42 cmd.extend(['-log-level', 'warning'])
43 # Set the install location.
44 cmd.extend(['-root', _CIPD_ROOT])
45
46 ensure_content = ('infra/tools/protoc/${platform} '
47 'protobuf_version:v%s' % PROTOC_VERSION)
48 with osutils.TempDir() as tempdir:
49 ensure_file = os.path.join(tempdir, 'cipd_ensure_file')
50 osutils.WriteFile(ensure_file, ensure_content)
51
52 cmd.extend(['-ensure-file', ensure_file])
53
Mike Frysinger45602c72019-09-22 02:15:11 -040054 cros_build_lib.run(cmd, cwd=constants.CHROMITE_DIR, print_cmd=False)
Alex Klein5534f992019-09-16 16:31:23 -060055
Alex Kleinf9859972019-03-14 17:11:42 -060056
57def _CleanTargetDirectory(directory):
58 """Remove any existing generated files in the directory.
59
60 This clean only removes the generated files to avoid accidentally destroying
61 __init__.py customizations down the line. That will leave otherwise empty
62 directories in place if things get moved. Neither case is relevant at the
63 time of writing, but lingering empty directories seemed better than
64 diagnosing accidental __init__.py changes.
65
66 Args:
67 directory (str): Path to be cleaned up.
68 """
Alex Klein5534f992019-09-16 16:31:23 -060069 logging.info('Cleaning old files.')
Alex Kleinf9859972019-03-14 17:11:42 -060070 for dirpath, _dirnames, filenames in os.walk(directory):
71 old = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
Alex Klein5534f992019-09-16 16:31:23 -060072 # Remove empty init files to clean up otherwise empty directories.
73 if '__init__.py' in filenames:
74 init = os.path.join(dirpath, '__init__.py')
75 if not osutils.ReadFile(init):
76 old.append(init)
77
Alex Kleinf9859972019-03-14 17:11:42 -060078 for current in old:
79 osutils.SafeUnlink(current)
80
Alex Klein5534f992019-09-16 16:31:23 -060081
Alex Kleinf9859972019-03-14 17:11:42 -060082def _GenerateFiles(source, output):
83 """Generate the proto files from the |source| tree into |output|.
84
85 Args:
86 source (str): Path to the proto source root directory.
87 output (str): Path to the output root directory.
88 """
Alex Klein5534f992019-09-16 16:31:23 -060089 logging.info('Generating files.')
Alex Kleinf9859972019-03-14 17:11:42 -060090 targets = []
91
92 # Only compile the subset we need for the API.
Alex Klein5534f992019-09-16 16:31:23 -060093 subdirs = [
94 os.path.join(source, 'chromite'),
95 os.path.join(source, 'chromiumos'),
Andrew Lamb2cbe4582019-10-29 11:56:25 -060096 os.path.join(source, 'config'),
Xinan Lin2a40ea72020-01-10 23:50:27 +000097 os.path.join(source, 'test_platform'),
98 os.path.join(source, 'device')
Alex Klein5534f992019-09-16 16:31:23 -060099 ]
Alex Kleinf9859972019-03-14 17:11:42 -0600100 for basedir in subdirs:
101 for dirpath, _dirnames, filenames in os.walk(basedir):
102 for filename in filenames:
103 if filename.endswith('.proto'):
104 # We have a match, add the file.
105 targets.append(os.path.join(dirpath, filename))
106
Alex Klein5534f992019-09-16 16:31:23 -0600107 cmd = [_PROTOC, '--python_out', output, '--proto_path', source] + targets
Mike Frysinger45602c72019-09-22 02:15:11 -0400108 result = cros_build_lib.run(
Mike Frysingerf5a3b2d2019-12-12 14:36:17 -0500109 cmd, cwd=source, print_cmd=False, check=False)
Alex Klein5534f992019-09-16 16:31:23 -0600110
111 if result.returncode:
112 raise GenerationError('Error compiling the proto. See the output for a '
113 'message.')
Alex Kleinf9859972019-03-14 17:11:42 -0600114
115
116def _InstallMissingInits(directory):
117 """Add any __init__.py files not present in the generated protobuf folders."""
Alex Klein5534f992019-09-16 16:31:23 -0600118 logging.info('Adding missing __init__.py files.')
Alex Kleinf9859972019-03-14 17:11:42 -0600119 for dirpath, _dirnames, filenames in os.walk(directory):
120 if '__init__.py' not in filenames:
121 osutils.Touch(os.path.join(dirpath, '__init__.py'))
122
123
124def _PostprocessFiles(directory):
125 """Do postprocessing on the generated files.
126
127 Args:
128 directory (str): The root directory containing the generated files that are
129 to be processed.
130 """
Alex Klein5534f992019-09-16 16:31:23 -0600131 logging.info('Postprocessing: Fix imports.')
Alex Kleinf9859972019-03-14 17:11:42 -0600132 # We are using a negative address here (the /address/! portion of the sed
133 # command) to make sure we don't change any imports from protobuf itself.
134 address = '^from google.protobuf'
135 # Find: 'from x import y_pb2 as x_dot_y_pb2'.
136 # "\(^google.protobuf[^ ]*\)" matches the module we're importing from.
137 # - \( and \) are for groups in sed.
138 # - ^google.protobuf prevents changing the import for protobuf's files.
139 # - [^ ] = Not a space. The [:space:] character set is too broad, but would
140 # technically work too.
141 find = r'^from \([^ ]*\) import \([^ ]*\)_pb2 as \([^ ]*\)$'
142 # Substitute: 'from chromite.api.gen.x import y_pb2 as x_dot_y_pb2'.
143 sub = 'from chromite.api.gen.\\1 import \\2_pb2 as \\3'
Alex Klein5534f992019-09-16 16:31:23 -0600144 from_sed = [
145 'sed', '-i',
146 '/%(address)s/!s/%(find)s/%(sub)s/g' % {
147 'address': address,
148 'find': find,
149 'sub': sub
150 }
151 ]
Alex Kleinf9859972019-03-14 17:11:42 -0600152
153 for dirpath, _dirnames, filenames in os.walk(directory):
154 # Update the
155 pb2 = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
156 if pb2:
157 cmd = from_sed + pb2
Mike Frysinger45602c72019-09-22 02:15:11 -0400158 cros_build_lib.run(cmd, print_cmd=False)
Alex Kleinf9859972019-03-14 17:11:42 -0600159
160
161def CompileProto(output=None):
162 """Compile the Build API protobuf files.
163
164 By default this will compile from infra/proto/src to api/gen. The output
165 directory may be changed, but the imports will always be treated as if it is
166 in the default location.
167
168 Args:
169 output (str|None): The output directory.
170 """
171 source = os.path.join(_PROTO_DIR, 'src')
172 output = output or os.path.join(_API_DIR, 'gen')
173
174 _InstallProtoc()
175 _CleanTargetDirectory(output)
176 _GenerateFiles(source, output)
177 _InstallMissingInits(output)
178 _PostprocessFiles(output)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700179
180
181def GetParser():
Alex Kleinf9859972019-03-14 17:11:42 -0600182 """Build the argument parser."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700183 parser = commandline.ArgumentParser(description=__doc__)
Alex Klein5534f992019-09-16 16:31:23 -0600184 parser.add_argument(
185 '--destination',
186 type='path',
187 help='The directory where the proto should be generated. Defaults to '
188 'the correct directory for the API.')
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700189 return parser
190
191
192def _ParseArguments(argv):
193 """Parse and validate arguments."""
194 parser = GetParser()
195 opts = parser.parse_args(argv)
196
197 opts.Freeze()
198 return opts
199
200
201def main(argv):
Alex Kleinf9859972019-03-14 17:11:42 -0600202 opts = _ParseArguments(argv)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700203
Alex Klein5534f992019-09-16 16:31:23 -0600204 try:
205 CompileProto(output=opts.destination)
206 except Error as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400207 logging.error(e)
Alex Klein5534f992019-09-16 16:31:23 -0600208 return 1