blob: 8a58362f5b14a36de2c258ef214070fa087abcf2 [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'),
Andrew Lamb2cbe4582019-10-29 11:56:25 -0600101 os.path.join(source, 'config'),
Xinan Lin2a40ea72020-01-10 23:50:27 +0000102 os.path.join(source, 'test_platform'),
103 os.path.join(source, 'device')
Alex Klein5534f992019-09-16 16:31:23 -0600104 ]
Alex Kleinf9859972019-03-14 17:11:42 -0600105 for basedir in subdirs:
106 for dirpath, _dirnames, filenames in os.walk(basedir):
107 for filename in filenames:
108 if filename.endswith('.proto'):
109 # We have a match, add the file.
110 targets.append(os.path.join(dirpath, filename))
111
Alex Klein5534f992019-09-16 16:31:23 -0600112 cmd = [_PROTOC, '--python_out', output, '--proto_path', source] + targets
Mike Frysinger45602c72019-09-22 02:15:11 -0400113 result = cros_build_lib.run(
Mike Frysingerf5a3b2d2019-12-12 14:36:17 -0500114 cmd, cwd=source, print_cmd=False, check=False)
Alex Klein5534f992019-09-16 16:31:23 -0600115
116 if result.returncode:
117 raise GenerationError('Error compiling the proto. See the output for a '
118 'message.')
Alex Kleinf9859972019-03-14 17:11:42 -0600119
120
121def _InstallMissingInits(directory):
122 """Add any __init__.py files not present in the generated protobuf folders."""
Alex Klein5534f992019-09-16 16:31:23 -0600123 logging.info('Adding missing __init__.py files.')
Alex Kleinf9859972019-03-14 17:11:42 -0600124 for dirpath, _dirnames, filenames in os.walk(directory):
125 if '__init__.py' not in filenames:
126 osutils.Touch(os.path.join(dirpath, '__init__.py'))
127
128
129def _PostprocessFiles(directory):
130 """Do postprocessing on the generated files.
131
132 Args:
133 directory (str): The root directory containing the generated files that are
134 to be processed.
135 """
Alex Klein5534f992019-09-16 16:31:23 -0600136 logging.info('Postprocessing: Fix imports.')
Alex Kleinf9859972019-03-14 17:11:42 -0600137 # We are using a negative address here (the /address/! portion of the sed
138 # command) to make sure we don't change any imports from protobuf itself.
139 address = '^from google.protobuf'
140 # Find: 'from x import y_pb2 as x_dot_y_pb2'.
141 # "\(^google.protobuf[^ ]*\)" matches the module we're importing from.
142 # - \( and \) are for groups in sed.
143 # - ^google.protobuf prevents changing the import for protobuf's files.
144 # - [^ ] = Not a space. The [:space:] character set is too broad, but would
145 # technically work too.
146 find = r'^from \([^ ]*\) import \([^ ]*\)_pb2 as \([^ ]*\)$'
147 # Substitute: 'from chromite.api.gen.x import y_pb2 as x_dot_y_pb2'.
148 sub = 'from chromite.api.gen.\\1 import \\2_pb2 as \\3'
Alex Klein5534f992019-09-16 16:31:23 -0600149 from_sed = [
150 'sed', '-i',
151 '/%(address)s/!s/%(find)s/%(sub)s/g' % {
152 'address': address,
153 'find': find,
154 'sub': sub
155 }
156 ]
Alex Kleinf9859972019-03-14 17:11:42 -0600157
158 for dirpath, _dirnames, filenames in os.walk(directory):
159 # Update the
160 pb2 = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
161 if pb2:
162 cmd = from_sed + pb2
Mike Frysinger45602c72019-09-22 02:15:11 -0400163 cros_build_lib.run(cmd, print_cmd=False)
Alex Kleinf9859972019-03-14 17:11:42 -0600164
165
166def CompileProto(output=None):
167 """Compile the Build API protobuf files.
168
169 By default this will compile from infra/proto/src to api/gen. The output
170 directory may be changed, but the imports will always be treated as if it is
171 in the default location.
172
173 Args:
174 output (str|None): The output directory.
175 """
176 source = os.path.join(_PROTO_DIR, 'src')
177 output = output or os.path.join(_API_DIR, 'gen')
178
179 _InstallProtoc()
180 _CleanTargetDirectory(output)
181 _GenerateFiles(source, output)
182 _InstallMissingInits(output)
183 _PostprocessFiles(output)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700184
185
186def GetParser():
Alex Kleinf9859972019-03-14 17:11:42 -0600187 """Build the argument parser."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700188 parser = commandline.ArgumentParser(description=__doc__)
Alex Klein5534f992019-09-16 16:31:23 -0600189 parser.add_argument(
190 '--destination',
191 type='path',
192 help='The directory where the proto should be generated. Defaults to '
193 'the correct directory for the API.')
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700194 return parser
195
196
197def _ParseArguments(argv):
198 """Parse and validate arguments."""
199 parser = GetParser()
200 opts = parser.parse_args(argv)
201
202 opts.Freeze()
203 return opts
204
205
206def main(argv):
Alex Kleinf9859972019-03-14 17:11:42 -0600207 opts = _ParseArguments(argv)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700208
Alex Klein5534f992019-09-16 16:31:23 -0600209 try:
210 CompileProto(output=opts.destination)
211 except Error as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400212 logging.error(e)
Alex Klein5534f992019-09-16 16:31:23 -0600213 return 1