blob: ff60e71708c1612882373ae528c6d44920be36b2 [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'),
74 os.path.join(source, 'chromiumos')]
75 for basedir in subdirs:
76 for dirpath, _dirnames, filenames in os.walk(basedir):
77 for filename in filenames:
78 if filename.endswith('.proto'):
79 # We have a match, add the file.
80 targets.append(os.path.join(dirpath, filename))
81
82 template = ('%(protoc)s --python_out %(output)s '
83 '--proto_path %(src)s %(targets)s')
84 cmd = template % {'protoc': _PROTOC, 'output': output, 'src': source,
85 'targets': ' '.join(targets)}
86 cros_build_lib.RunCommand(cmd, shell=True, cwd=source)
87
88
89def _InstallMissingInits(directory):
90 """Add any __init__.py files not present in the generated protobuf folders."""
91 for dirpath, _dirnames, filenames in os.walk(directory):
92 if '__init__.py' not in filenames:
93 osutils.Touch(os.path.join(dirpath, '__init__.py'))
94
95
96def _PostprocessFiles(directory):
97 """Do postprocessing on the generated files.
98
99 Args:
100 directory (str): The root directory containing the generated files that are
101 to be processed.
102 """
103 # We are using a negative address here (the /address/! portion of the sed
104 # command) to make sure we don't change any imports from protobuf itself.
105 address = '^from google.protobuf'
106 # Find: 'from x import y_pb2 as x_dot_y_pb2'.
107 # "\(^google.protobuf[^ ]*\)" matches the module we're importing from.
108 # - \( and \) are for groups in sed.
109 # - ^google.protobuf prevents changing the import for protobuf's files.
110 # - [^ ] = Not a space. The [:space:] character set is too broad, but would
111 # technically work too.
112 find = r'^from \([^ ]*\) import \([^ ]*\)_pb2 as \([^ ]*\)$'
113 # Substitute: 'from chromite.api.gen.x import y_pb2 as x_dot_y_pb2'.
114 sub = 'from chromite.api.gen.\\1 import \\2_pb2 as \\3'
115 from_sed = ['sed', '-i', '/%(address)s/!s/%(find)s/%(sub)s/g' %
116 {'address': address, 'find': find, 'sub': sub}]
117
118 for dirpath, _dirnames, filenames in os.walk(directory):
119 # Update the
120 pb2 = [os.path.join(dirpath, f) for f in filenames if f.endswith('_pb2.py')]
121 if pb2:
122 cmd = from_sed + pb2
123 cros_build_lib.RunCommand(cmd)
124
125
126def CompileProto(output=None):
127 """Compile the Build API protobuf files.
128
129 By default this will compile from infra/proto/src to api/gen. The output
130 directory may be changed, but the imports will always be treated as if it is
131 in the default location.
132
133 Args:
134 output (str|None): The output directory.
135 """
136 source = os.path.join(_PROTO_DIR, 'src')
137 output = output or os.path.join(_API_DIR, 'gen')
138
139 _InstallProtoc()
140 _CleanTargetDirectory(output)
141 _GenerateFiles(source, output)
142 _InstallMissingInits(output)
143 _PostprocessFiles(output)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700144
145
146def GetParser():
Alex Kleinf9859972019-03-14 17:11:42 -0600147 """Build the argument parser."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700148 parser = commandline.ArgumentParser(description=__doc__)
Alex Kleinf9859972019-03-14 17:11:42 -0600149 parser.add_argument('--destination', type='path',
150 help='The directory where the proto should be generated.'
151 'Defaults to the correct directory for the API.')
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700152 return parser
153
154
155def _ParseArguments(argv):
156 """Parse and validate arguments."""
157 parser = GetParser()
158 opts = parser.parse_args(argv)
159
160 opts.Freeze()
161 return opts
162
163
164def main(argv):
Alex Kleinf9859972019-03-14 17:11:42 -0600165 opts = _ParseArguments(argv)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700166
Alex Kleinf9859972019-03-14 17:11:42 -0600167 CompileProto(output=opts.destination)