blob: ff1049f61ab621a7fe65bdbd6f112e353be73594 [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 Kleinf9859972019-03-14 17:11:42 -060018from chromite.lib import osutils
19
20_API_DIR = os.path.join(constants.CHROMITE_DIR, 'api')
21_CIPD_ROOT = os.path.join(constants.CHROMITE_DIR, '.cipd_bin')
22_PROTOC = os.path.join(_CIPD_ROOT, 'protoc')
23_PROTO_DIR = os.path.join(constants.CHROMITE_DIR, 'infra', 'proto')
24
25PROTOC_VERSION = '3.6.1'
26
27
28def _InstallProtoc():
29 """Install protoc from CIPD."""
30 cmd = ['cipd', 'ensure']
31 # Clean up the output.
32 cmd.extend(['-log-level', 'warning'])
33 # Set the install location.
34 cmd.extend(['-root', _CIPD_ROOT])
35
36 ensure_content = ('infra/tools/protoc/${platform} '
37 'protobuf_version:v%s' % PROTOC_VERSION)
38 with osutils.TempDir() as tempdir:
39 ensure_file = os.path.join(tempdir, 'cipd_ensure_file')
40 osutils.WriteFile(ensure_file, ensure_content)
41
42 cmd.extend(['-ensure-file', ensure_file])
43
44 cros_build_lib.RunCommand(cmd, cwd=constants.CHROMITE_DIR)
45
46def _CleanTargetDirectory(directory):
47 """Remove any existing generated files in the directory.
48
49 This clean only removes the generated files to avoid accidentally destroying
50 __init__.py customizations down the line. That will leave otherwise empty
51 directories in place if things get moved. Neither case is relevant at the
52 time of writing, but lingering empty directories seemed better than
53 diagnosing accidental __init__.py changes.
54
55 Args:
56 directory (str): Path to be cleaned up.
57 """
58 for dirpath, _dirnames, filenames in os.walk(directory):
59 old = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
60 for current in old:
61 osutils.SafeUnlink(current)
62
63def _GenerateFiles(source, output):
64 """Generate the proto files from the |source| tree into |output|.
65
66 Args:
67 source (str): Path to the proto source root directory.
68 output (str): Path to the output root directory.
69 """
70 targets = []
71
72 # Only compile the subset we need for the API.
73 subdirs = [os.path.join(source, 'chromite'),
Xinan Lin11576072019-07-29 16:39:39 -070074 os.path.join(source, 'chromiumos'),
75 os.path.join(source, 'test_platform')]
Alex Kleinf9859972019-03-14 17:11:42 -060076 for basedir in subdirs:
77 for dirpath, _dirnames, filenames in os.walk(basedir):
78 for filename in filenames:
79 if filename.endswith('.proto'):
80 # We have a match, add the file.
81 targets.append(os.path.join(dirpath, filename))
82
83 template = ('%(protoc)s --python_out %(output)s '
84 '--proto_path %(src)s %(targets)s')
85 cmd = template % {'protoc': _PROTOC, 'output': output, 'src': source,
86 'targets': ' '.join(targets)}
87 cros_build_lib.RunCommand(cmd, shell=True, cwd=source)
88
89
90def _InstallMissingInits(directory):
91 """Add any __init__.py files not present in the generated protobuf folders."""
92 for dirpath, _dirnames, filenames in os.walk(directory):
93 if '__init__.py' not in filenames:
94 osutils.Touch(os.path.join(dirpath, '__init__.py'))
95
96
97def _PostprocessFiles(directory):
98 """Do postprocessing on the generated files.
99
100 Args:
101 directory (str): The root directory containing the generated files that are
102 to be processed.
103 """
104 # We are using a negative address here (the /address/! portion of the sed
105 # command) to make sure we don't change any imports from protobuf itself.
106 address = '^from google.protobuf'
107 # Find: 'from x import y_pb2 as x_dot_y_pb2'.
108 # "\(^google.protobuf[^ ]*\)" matches the module we're importing from.
109 # - \( and \) are for groups in sed.
110 # - ^google.protobuf prevents changing the import for protobuf's files.
111 # - [^ ] = Not a space. The [:space:] character set is too broad, but would
112 # technically work too.
113 find = r'^from \([^ ]*\) import \([^ ]*\)_pb2 as \([^ ]*\)$'
114 # Substitute: 'from chromite.api.gen.x import y_pb2 as x_dot_y_pb2'.
115 sub = 'from chromite.api.gen.\\1 import \\2_pb2 as \\3'
116 from_sed = ['sed', '-i', '/%(address)s/!s/%(find)s/%(sub)s/g' %
117 {'address': address, 'find': find, 'sub': sub}]
118
119 for dirpath, _dirnames, filenames in os.walk(directory):
120 # Update the
121 pb2 = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
122 if pb2:
123 cmd = from_sed + pb2
124 cros_build_lib.RunCommand(cmd)
125
126
127def CompileProto(output=None):
128 """Compile the Build API protobuf files.
129
130 By default this will compile from infra/proto/src to api/gen. The output
131 directory may be changed, but the imports will always be treated as if it is
132 in the default location.
133
134 Args:
135 output (str|None): The output directory.
136 """
137 source = os.path.join(_PROTO_DIR, 'src')
138 output = output or os.path.join(_API_DIR, 'gen')
139
140 _InstallProtoc()
141 _CleanTargetDirectory(output)
142 _GenerateFiles(source, output)
143 _InstallMissingInits(output)
144 _PostprocessFiles(output)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700145
146
147def GetParser():
Alex Kleinf9859972019-03-14 17:11:42 -0600148 """Build the argument parser."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700149 parser = commandline.ArgumentParser(description=__doc__)
Alex Kleinf9859972019-03-14 17:11:42 -0600150 parser.add_argument('--destination', type='path',
151 help='The directory where the proto should be generated.'
152 'Defaults to the correct directory for the API.')
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700153 return parser
154
155
156def _ParseArguments(argv):
157 """Parse and validate arguments."""
158 parser = GetParser()
159 opts = parser.parse_args(argv)
160
161 opts.Freeze()
162 return opts
163
164
165def main(argv):
Alex Kleinf9859972019-03-14 17:11:42 -0600166 opts = _ParseArguments(argv)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700167
Alex Kleinf9859972019-03-14 17:11:42 -0600168 CompileProto(output=opts.destination)