blob: d2ad78821fe03db4a01746bf35475a49d64f5177 [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
24
25class ChrootHandler(object):
26 """Translate a Chroot message to chroot enter arguments and env."""
27
28 def __init__(self, clear_field):
29 self.clear_field = clear_field
30
31 def handle(self, message):
32 """Parse a message for a chroot field."""
33 # Find the Chroot field. Search for the field by type to prevent it being
34 # tied to a naming convention.
35 for descriptor in message.DESCRIPTOR.fields:
36 field = getattr(message, descriptor.name)
37 if isinstance(field, common_pb2.Chroot):
38 chroot = field
39 if self.clear_field:
40 message.ClearField(descriptor.name)
41 return self.parse_chroot(chroot)
42
43 return None
44
45 def parse_chroot(self, chroot_message):
46 """Parse a Chroot message instance."""
47 path = chroot_message.path or constants.DEFAULT_CHROOT_PATH
48 return chroot_lib.Chroot(path=path, cache_dir=chroot_message.cache_dir,
49 env=self._parse_env(chroot_message))
50
51 def _parse_env(self, chroot_message):
52 """Get chroot environment variables that need to be set.
53
54 Returns:
55 dict - The variable: value pairs.
56 """
57 use_flags = [u.flag for u in chroot_message.env.use_flags]
58 features = [f.feature for f in chroot_message.env.features]
59
60 env = {}
61 if use_flags:
62 env['USE'] = ' '.join(use_flags)
63
64 # TODO(saklein) Remove the default when fully integrated in recipes.
65 env['FEATURES'] = 'separatedebug'
66 if features:
67 env['FEATURES'] = ' '.join(features)
68
69 return env
70
71
72def handle_chroot(message, clear_field=True):
73 """Find and parse the chroot field, returning the Chroot instance.
74
75 Returns:
76 chroot_lib.Chroot
77 """
78 handler = ChrootHandler(clear_field)
79 chroot = handler.handle(message)
80 if chroot:
81 return chroot
82
83 logging.warning('No chroot message found, falling back to defaults.')
84 return handler.parse_chroot(common_pb2.Chroot())
85
86
87class PathHandler(object):
88 """Handles copying a file or directory into or out of the chroot."""
89
90 INSIDE = common_pb2.Path.INSIDE
91 OUTSIDE = common_pb2.Path.OUTSIDE
92 ALL = -1
93
94 def __init__(self, field, destination, delete, prefix=None):
95 """Path handler initialization.
96
97 Args:
98 field (common_pb2.Path): The Path message.
99 destination (str): The destination base path.
100 delete (bool): Whether the copied file(s) should be deleted on cleanup.
101 prefix (str|None): A path prefix to remove from the destination path
102 when building the new Path message to pass back. This is largely meant
103 to support removing the chroot directory for files moved into the chroot
104 for endpoints that execute inside.
105 """
106 assert isinstance(field, common_pb2.Path)
107 assert field.path
108 assert field.location
109
110 self.field = field
111 self.destination = destination
112 self.prefix = prefix or ''
113 self.delete = delete
114 self.tempdir = None
115
116 def transfer(self, direction=None):
117 """Copy the file or directory to its destination.
118
119 Args:
120 direction (int): The direction files are being copied (into or out of
121 the chroot). Specifying the direction allows avoiding performing
122 unnecessary copies.
123 """
124 if direction is None:
125 direction = self.ALL
126 assert direction in [self.INSIDE, self.OUTSIDE, self.ALL]
127
128 if self.field.location == direction:
129 return None
130
131 if self.delete:
132 self.tempdir = osutils.TempDir(base_dir=self.destination)
133 destination = self.tempdir.tempdir
134 else:
135 destination = self.destination
136
137 if os.path.isfile(self.field.path):
138 # Use the old file name, just copy it into dest.
139 dest_path = os.path.join(destination, os.path.basename(self.field.path))
140 copy_fn = shutil.copy
141 else:
142 dest_path = destination
143 copy_fn = osutils.CopyDirContents
144
145 logging.debug('Copying %s to %s', self.field.path, dest_path)
146 copy_fn(self.field.path, dest_path)
147
148 # Clean up the destination path for returning, if applicable.
149 return_path = dest_path
150 if return_path.startswith(self.prefix):
151 return_path = return_path[len(self.prefix):]
152
153 path = common_pb2.Path()
154 path.path = return_path
155 path.location = direction
156
157 return path
158
159 def cleanup(self):
160 if self.tempdir:
161 self.tempdir.Cleanup()
162 self.tempdir = None
163
164
165@contextlib.contextmanager
166def handle_paths(message, destination, delete=True, direction=None,
167 prefix=None):
168 """Context manager function to transfer and cleanup all Path messages.
169
170 Args:
171 message (Message): A message whose Path messages should be transferred.
172 destination (str): A base destination path.
173 delete (bool): Whether the file(s) should be deleted.
174 direction (int): One of the PathHandler constants (INSIDE, OUTSIDE, ALL).
175 This allows avoiding unnecessarily copying files already in the right
176 place (e.g. copying a file into the chroot that's already in the chroot).
177 prefix (str|None): A prefix path to remove from the final destination path
178 in the Path message (i.e. remove the chroot path).
179
180 Returns:
181 list[PathHandler]: The path handlers.
182 """
183 assert destination
184 direction = direction or PathHandler.ALL
185
186 # field-name, handler pairs.
187 handlers = []
188 for descriptor in message.DESCRIPTOR.fields:
189 field = getattr(message, descriptor.name)
190 if isinstance(field, common_pb2.Path):
191 if not field.path or not field.location:
192 logging.debug('Skipping %s; incomplete.', descriptor.name)
193 continue
194
195 handler = PathHandler(field, destination, delete=delete, prefix=prefix)
196 handlers.append((descriptor.name, handler))
197
198 for field_name, handler in handlers:
199 new_field = handler.transfer(direction)
200 if not new_field:
201 # When no copy is needed.
202 continue
203
204 old_field = getattr(message, field_name)
205 old_field.path = new_field.path
206 old_field.location = new_field.location
207
208 try:
209 yield handlers
210 finally:
211 for field_name, handler in handlers:
212 handler.cleanup()