blob: 944efc98d99e17acd3d89d62a4f2eef7250d4d96 [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'),
Alex Klein5534f992019-09-16 16:31:23 -060097 os.path.join(source, 'test_platform')
98 ]
Alex Kleinf9859972019-03-14 17:11:42 -060099 for basedir in subdirs:
100 for dirpath, _dirnames, filenames in os.walk(basedir):
101 for filename in filenames:
102 if filename.endswith('.proto'):
103 # We have a match, add the file.
104 targets.append(os.path.join(dirpath, filename))
105
Alex Klein5534f992019-09-16 16:31:23 -0600106 cmd = [_PROTOC, '--python_out', output, '--proto_path', source] + targets
Mike Frysinger45602c72019-09-22 02:15:11 -0400107 result = cros_build_lib.run(
Alex Klein5534f992019-09-16 16:31:23 -0600108 cmd, cwd=source, print_cmd=False, error_code_ok=True)
109
110 if result.returncode:
111 raise GenerationError('Error compiling the proto. See the output for a '
112 'message.')
Alex Kleinf9859972019-03-14 17:11:42 -0600113
114
115def _InstallMissingInits(directory):
116 """Add any __init__.py files not present in the generated protobuf folders."""
Alex Klein5534f992019-09-16 16:31:23 -0600117 logging.info('Adding missing __init__.py files.')
Alex Kleinf9859972019-03-14 17:11:42 -0600118 for dirpath, _dirnames, filenames in os.walk(directory):
119 if '__init__.py' not in filenames:
120 osutils.Touch(os.path.join(dirpath, '__init__.py'))
121
122
123def _PostprocessFiles(directory):
124 """Do postprocessing on the generated files.
125
126 Args:
127 directory (str): The root directory containing the generated files that are
128 to be processed.
129 """
Alex Klein5534f992019-09-16 16:31:23 -0600130 logging.info('Postprocessing: Fix imports.')
Alex Kleinf9859972019-03-14 17:11:42 -0600131 # We are using a negative address here (the /address/! portion of the sed
132 # command) to make sure we don't change any imports from protobuf itself.
133 address = '^from google.protobuf'
134 # Find: 'from x import y_pb2 as x_dot_y_pb2'.
135 # "\(^google.protobuf[^ ]*\)" matches the module we're importing from.
136 # - \( and \) are for groups in sed.
137 # - ^google.protobuf prevents changing the import for protobuf's files.
138 # - [^ ] = Not a space. The [:space:] character set is too broad, but would
139 # technically work too.
140 find = r'^from \([^ ]*\) import \([^ ]*\)_pb2 as \([^ ]*\)$'
141 # Substitute: 'from chromite.api.gen.x import y_pb2 as x_dot_y_pb2'.
142 sub = 'from chromite.api.gen.\\1 import \\2_pb2 as \\3'
Alex Klein5534f992019-09-16 16:31:23 -0600143 from_sed = [
144 'sed', '-i',
145 '/%(address)s/!s/%(find)s/%(sub)s/g' % {
146 'address': address,
147 'find': find,
148 'sub': sub
149 }
150 ]
Alex Kleinf9859972019-03-14 17:11:42 -0600151
152 for dirpath, _dirnames, filenames in os.walk(directory):
153 # Update the
154 pb2 = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
155 if pb2:
156 cmd = from_sed + pb2
Mike Frysinger45602c72019-09-22 02:15:11 -0400157 cros_build_lib.run(cmd, print_cmd=False)
Alex Kleinf9859972019-03-14 17:11:42 -0600158
159
160def CompileProto(output=None):
161 """Compile the Build API protobuf files.
162
163 By default this will compile from infra/proto/src to api/gen. The output
164 directory may be changed, but the imports will always be treated as if it is
165 in the default location.
166
167 Args:
168 output (str|None): The output directory.
169 """
170 source = os.path.join(_PROTO_DIR, 'src')
171 output = output or os.path.join(_API_DIR, 'gen')
172
173 _InstallProtoc()
174 _CleanTargetDirectory(output)
175 _GenerateFiles(source, output)
176 _InstallMissingInits(output)
177 _PostprocessFiles(output)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700178
179
180def GetParser():
Alex Kleinf9859972019-03-14 17:11:42 -0600181 """Build the argument parser."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700182 parser = commandline.ArgumentParser(description=__doc__)
Alex Klein5534f992019-09-16 16:31:23 -0600183 parser.add_argument(
184 '--destination',
185 type='path',
186 help='The directory where the proto should be generated. Defaults to '
187 'the correct directory for the API.')
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700188 return parser
189
190
191def _ParseArguments(argv):
192 """Parse and validate arguments."""
193 parser = GetParser()
194 opts = parser.parse_args(argv)
195
196 opts.Freeze()
197 return opts
198
199
200def main(argv):
Alex Kleinf9859972019-03-14 17:11:42 -0600201 opts = _ParseArguments(argv)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700202
Alex Klein5534f992019-09-16 16:31:23 -0600203 try:
204 CompileProto(output=opts.destination)
205 except Error as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400206 logging.error(e)
Alex Klein5534f992019-09-16 16:31:23 -0600207 return 1