blob: 32b5192c0110749fa3238836754155d7f0841b2c [file] [log] [blame]
Alex Kleinc05f3d12019-05-29 14:16:21 -06001# -*- coding: utf-8 -*-
2# Copyright 2019 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
6"""Field handler classes.
7
8The field handlers are meant to parse information from or do some other generic
9action for a specific field type for the build_api script.
10"""
11
12from __future__ import print_function
13
14import contextlib
15import os
16import shutil
17
18from chromite.api.gen.chromiumos import common_pb2
19from chromite.lib import chroot_lib
20from chromite.lib import constants
21from chromite.lib import cros_logging as logging
22from chromite.lib import osutils
23
Alex Kleinaa705412019-06-04 15:00:30 -060024from google.protobuf import message as protobuf_message
25
Alex Kleinc05f3d12019-05-29 14:16:21 -060026
27class ChrootHandler(object):
28 """Translate a Chroot message to chroot enter arguments and env."""
29
30 def __init__(self, clear_field):
31 self.clear_field = clear_field
32
33 def handle(self, message):
34 """Parse a message for a chroot field."""
35 # Find the Chroot field. Search for the field by type to prevent it being
36 # tied to a naming convention.
37 for descriptor in message.DESCRIPTOR.fields:
38 field = getattr(message, descriptor.name)
39 if isinstance(field, common_pb2.Chroot):
40 chroot = field
41 if self.clear_field:
42 message.ClearField(descriptor.name)
43 return self.parse_chroot(chroot)
44
45 return None
46
47 def parse_chroot(self, chroot_message):
48 """Parse a Chroot message instance."""
49 path = chroot_message.path or constants.DEFAULT_CHROOT_PATH
50 return chroot_lib.Chroot(path=path, cache_dir=chroot_message.cache_dir,
51 env=self._parse_env(chroot_message))
52
53 def _parse_env(self, chroot_message):
54 """Get chroot environment variables that need to be set.
55
56 Returns:
57 dict - The variable: value pairs.
58 """
59 use_flags = [u.flag for u in chroot_message.env.use_flags]
60 features = [f.feature for f in chroot_message.env.features]
61
62 env = {}
63 if use_flags:
64 env['USE'] = ' '.join(use_flags)
65
66 # TODO(saklein) Remove the default when fully integrated in recipes.
67 env['FEATURES'] = 'separatedebug'
68 if features:
69 env['FEATURES'] = ' '.join(features)
70
71 return env
72
73
74def handle_chroot(message, clear_field=True):
75 """Find and parse the chroot field, returning the Chroot instance.
76
77 Returns:
78 chroot_lib.Chroot
79 """
80 handler = ChrootHandler(clear_field)
81 chroot = handler.handle(message)
82 if chroot:
83 return chroot
84
85 logging.warning('No chroot message found, falling back to defaults.')
86 return handler.parse_chroot(common_pb2.Chroot())
87
88
89class PathHandler(object):
90 """Handles copying a file or directory into or out of the chroot."""
91
92 INSIDE = common_pb2.Path.INSIDE
93 OUTSIDE = common_pb2.Path.OUTSIDE
94 ALL = -1
95
96 def __init__(self, field, destination, delete, prefix=None):
97 """Path handler initialization.
98
99 Args:
100 field (common_pb2.Path): The Path message.
101 destination (str): The destination base path.
102 delete (bool): Whether the copied file(s) should be deleted on cleanup.
103 prefix (str|None): A path prefix to remove from the destination path
104 when building the new Path message to pass back. This is largely meant
105 to support removing the chroot directory for files moved into the chroot
106 for endpoints that execute inside.
107 """
108 assert isinstance(field, common_pb2.Path)
109 assert field.path
110 assert field.location
111
112 self.field = field
113 self.destination = destination
114 self.prefix = prefix or ''
115 self.delete = delete
116 self.tempdir = None
Alex Kleinaa705412019-06-04 15:00:30 -0600117 # For resetting the state.
118 self._transferred = False
119 self._original_message = common_pb2.Path()
120 self._original_message.CopyFrom(self.field)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600121
122 def transfer(self, direction=None):
123 """Copy the file or directory to its destination.
124
125 Args:
126 direction (int): The direction files are being copied (into or out of
127 the chroot). Specifying the direction allows avoiding performing
128 unnecessary copies.
129 """
Alex Kleinaa705412019-06-04 15:00:30 -0600130 if self._transferred:
131 return
132
Alex Kleinc05f3d12019-05-29 14:16:21 -0600133 if direction is None:
134 direction = self.ALL
135 assert direction in [self.INSIDE, self.OUTSIDE, self.ALL]
136
137 if self.field.location == direction:
Alex Kleinaa705412019-06-04 15:00:30 -0600138 # Already in the correct location, nothing to do.
139 return
Alex Kleinc05f3d12019-05-29 14:16:21 -0600140
141 if self.delete:
142 self.tempdir = osutils.TempDir(base_dir=self.destination)
143 destination = self.tempdir.tempdir
144 else:
145 destination = self.destination
146
147 if os.path.isfile(self.field.path):
148 # Use the old file name, just copy it into dest.
149 dest_path = os.path.join(destination, os.path.basename(self.field.path))
150 copy_fn = shutil.copy
151 else:
152 dest_path = destination
153 copy_fn = osutils.CopyDirContents
154
155 logging.debug('Copying %s to %s', self.field.path, dest_path)
156 copy_fn(self.field.path, dest_path)
157
158 # Clean up the destination path for returning, if applicable.
159 return_path = dest_path
160 if return_path.startswith(self.prefix):
161 return_path = return_path[len(self.prefix):]
162
Alex Kleinaa705412019-06-04 15:00:30 -0600163 self.field.path = return_path
164 self.field.location = direction
165 self._transferred = True
Alex Kleinc05f3d12019-05-29 14:16:21 -0600166
167 def cleanup(self):
168 if self.tempdir:
169 self.tempdir.Cleanup()
170 self.tempdir = None
171
Alex Kleinaa705412019-06-04 15:00:30 -0600172 self.field.CopyFrom(self._original_message)
173
Alex Kleinc05f3d12019-05-29 14:16:21 -0600174
175@contextlib.contextmanager
176def handle_paths(message, destination, delete=True, direction=None,
177 prefix=None):
178 """Context manager function to transfer and cleanup all Path messages.
179
180 Args:
181 message (Message): A message whose Path messages should be transferred.
182 destination (str): A base destination path.
183 delete (bool): Whether the file(s) should be deleted.
184 direction (int): One of the PathHandler constants (INSIDE, OUTSIDE, ALL).
185 This allows avoiding unnecessarily copying files already in the right
186 place (e.g. copying a file into the chroot that's already in the chroot).
187 prefix (str|None): A prefix path to remove from the final destination path
188 in the Path message (i.e. remove the chroot path).
189
190 Returns:
191 list[PathHandler]: The path handlers.
192 """
193 assert destination
194 direction = direction or PathHandler.ALL
195
Alex Kleinaa705412019-06-04 15:00:30 -0600196 handlers = _extract_handlers(message, destination, delete, prefix)
197
198 for handler in handlers:
199 handler.transfer(direction)
200
201 try:
202 yield handlers
203 finally:
204 for handler in handlers:
205 handler.cleanup()
206
207
208def _extract_handlers(message, destination, delete, prefix):
209 """Recursive helper for handle_paths to extract Path messages."""
Alex Kleinc05f3d12019-05-29 14:16:21 -0600210 handlers = []
211 for descriptor in message.DESCRIPTOR.fields:
212 field = getattr(message, descriptor.name)
213 if isinstance(field, common_pb2.Path):
214 if not field.path or not field.location:
215 logging.debug('Skipping %s; incomplete.', descriptor.name)
216 continue
217
218 handler = PathHandler(field, destination, delete=delete, prefix=prefix)
Alex Kleinaa705412019-06-04 15:00:30 -0600219 handlers.append(handler)
220 elif isinstance(field, protobuf_message.Message):
221 handlers.extend(_extract_handlers(field, destination, delete, prefix))
Alex Kleinc05f3d12019-05-29 14:16:21 -0600222
Alex Kleinaa705412019-06-04 15:00:30 -0600223 return handlers