blob: 02ff9f93fc04ba2b49bf1ddd1975de67242a6f3b [file] [log] [blame]
Alex Kleina2ceb192018-08-17 11:19:32 -06001# -*- 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
6"""Choose the profile for a board that has been or is being setup."""
7
8from __future__ import print_function
9
10import functools
11import os
12
Mike Frysingerea625a72019-08-24 03:02:42 -040013import six
14
Alex Kleina2ceb192018-08-17 11:19:32 -060015from chromite.lib import commandline
16from chromite.lib import cros_build_lib
17from chromite.lib import cros_logging as logging
18from chromite.lib import osutils
19from chromite.lib import sysroot_lib
20
21
22# Default value constants.
23_DEFAULT_PROFILE = 'base'
24
25
26def PathPrefixDecorator(f):
27 """Add a prefix to the path or paths returned by the decorated function.
28
29 Will not prepend the prefix if the path already starts with the prefix, so the
30 decorator may be applied to functions that have mixed sources that may
31 or may not already have applied them. This is especially useful for allowing
32 tests and CLI args a little more leniency in how paths are provided.
33 """
34 @functools.wraps(f)
35 def wrapper(*args, **kwargs):
36 result = f(*args, **kwargs)
37 prefix = PathPrefixDecorator.prefix
38
39 if not prefix or not result:
40 # Nothing to do.
41 return result
Mike Frysingerea625a72019-08-24 03:02:42 -040042 elif not isinstance(result, six.string_types):
Alex Kleina2ceb192018-08-17 11:19:32 -060043 # Transform each path in the collection.
44 new_result = []
45 for path in result:
46 prefixed_path = os.path.join(prefix, path.lstrip(os.sep))
47 new_result.append(path if path.startswith(prefix) else prefixed_path)
48
49 return new_result
50 elif not result.startswith(prefix):
51 # Add the prefix.
52 return os.path.join(prefix, result.lstrip(os.sep))
53
54 # An already prefixed path.
55 return result
56
57 return wrapper
58
59PathPrefixDecorator.prefix = None
60
61
62class Error(Exception):
63 """Base error for custom exceptions in this script."""
64
65
66class InvalidArgumentsError(Error):
67 """Invalid arguments."""
68
69
70class MakeProfileIsNotLinkError(Error):
71 """The make profile exists but is not a link."""
72
73
74class ProfileDirectoryNotFoundError(Error):
75 """Unable to find the profile directory."""
76
77
78def ChooseProfile(board, profile):
79 """Make the link to choose the profile, print relevant warnings.
80
81 Args:
82 board: Board - the board being used.
83 profile: Profile - the profile being used.
84
85 Raises:
86 OSError when the board's make_profile path exists and is not a link.
87 """
88 if not os.path.isfile(os.path.join(profile.directory, 'parent')):
89 logging.warning("Portage profile directory %s has no 'parent' file. "
90 'This likely means your profile directory is invalid and '
91 'build_packages will fail.', profile.directory)
92
93 current_profile = None
94 if os.path.exists(board.make_profile):
95 # Only try to read if it exists; we only want it to raise an error when the
96 # path exists and is not a link.
97 try:
98 current_profile = os.readlink(board.make_profile)
99 except OSError:
100 raise MakeProfileIsNotLinkError('%s is not a link.' % board.make_profile)
101
102 if current_profile == profile.directory:
103 # The existing link is what we were going to make, so nothing to do.
104 return
105 elif current_profile is not None:
106 # It exists and is changing, emit warning.
107 fmt = {'board': board.board_variant, 'profile': profile.name}
108 msg = ('You are switching profiles for a board that is already setup. This '
109 'can cause trouble for Portage. If you experience problems with '
110 'build_packages you may need to run:\n'
111 "\t'setup_board --board %(board)s --force --profile %(profile)s'\n"
112 '\nAlternatively, you can correct the dependency graph by using '
113 "'emerge-%(board)s -c' or 'emerge-%(board)s -C <ebuild>'.")
114 logging.warning(msg, fmt)
115
116 # Make the symlink, overwrites existing link if one already exists.
117 osutils.SafeSymlink(profile.directory, board.make_profile, sudo=True)
118
119 # Update the profile override value.
120 if profile.override:
121 board.profile_override = profile.override
122
123
124class Profile(object):
125 """Simple data container class for the profile data."""
126 def __init__(self, name, directory, override):
127 self.name = name
128 self._directory = directory
129 self.override = override
130
131 @property
132 @PathPrefixDecorator
133 def directory(self):
134 return self._directory
135
136
137def _GetProfile(opts, board):
138 """Get the profile list."""
139 # Determine the override value - which profile is being selected.
140 override = opts.profile if opts.profile else board.profile_override
141
142 profile = _DEFAULT_PROFILE
143 profile_directory = None
144
145 if override and os.path.exists(override):
146 profile_directory = os.path.abspath(override)
147 profile = os.path.basename(profile_directory)
148 elif override:
149 profile = override
150
151 if profile_directory is None:
152 # Build profile directories in reverse order so we can search from most to
153 # least specific.
154 profile_dirs = ['%s/profiles/%s' % (overlay, profile) for overlay in
155 reversed(board.overlays)]
156
157 for profile_dir in profile_dirs:
158 if os.path.isdir(profile_dir):
159 profile_directory = profile_dir
160 break
161 else:
162 searched = ', '.join(profile_dirs)
163 raise ProfileDirectoryNotFoundError(
164 'Profile directory not found, searched in (%s).' % searched)
165
166 return Profile(profile, profile_directory, override)
167
168
169class Board(object):
170 """Manage the board arguments and configs."""
171
172 # Files located on the board.
173 MAKE_PROFILE = '%(board_root)s/etc/portage/make.profile'
174
175 def __init__(self, board=None, variant=None, board_root=None):
176 """Board constructor.
177
178 board [+ variant] is given preference when both board and board_root are
179 provided.
180
181 Preconditions:
182 Either board and build_root are not None, or board_root is not None.
183 With board + build_root [+ variant] we can construct the board root.
184 With the board root we can have the board[_variant] directory.
185
186 Args:
187 board: str|None - The board name.
188 variant: str|None - The variant name.
189 board_root: str|None - The boards fully qualified build directory path.
190 """
191 if not board and not board_root:
192 # Enforce preconditions.
193 raise InvalidArgumentsError('Either board or board_root must be '
194 'provided.')
195 elif board:
196 # The board and variant can be specified separately, or can both be
197 # contained in the board name, separated by an underscore.
198 board_split = board.split('_')
199 variant_default = variant
200
201 self._board_root = None
202 else:
203 self._board_root = os.path.normpath(board_root)
204
205 board_split = os.path.basename(self._board_root).split('_')
206 variant_default = None
207
208 self.board = board_split.pop(0)
209 self.variant = board_split.pop(0) if board_split else variant_default
210
211 if self.variant:
212 self.board_variant = '%s_%s' % (self.board, self.variant)
213 else:
214 self.board_variant = self.board
215
216 self.make_profile = self.MAKE_PROFILE % {'board_root': self.root}
217 # This must come after the arguments required to build each variant of the
218 # build root have been processed.
219 self._sysroot_config = sysroot_lib.Sysroot(self.root)
220
221 @property
222 @PathPrefixDecorator
223 def root(self):
224 if self._board_root:
225 return self._board_root
226
227 return os.path.join(cros_build_lib.GetSysroot(self.board_variant))
228
229 @property
230 @PathPrefixDecorator
231 def overlays(self):
232 return self._sysroot_config.GetStandardField(
233 sysroot_lib.STANDARD_FIELD_BOARD_OVERLAY).split()
234
235 @property
236 def profile_override(self):
237 return self._sysroot_config.GetCachedField('PROFILE_OVERRIDE')
238
239 @profile_override.setter
240 def profile_override(self, value):
241 self._sysroot_config.SetCachedField('PROFILE_OVERRIDE', value)
242
243
244def _GetBoard(opts):
245 """Factory method to build a Board from the parsed CLI arguments."""
246 return Board(board=opts.board, variant=opts.variant,
247 board_root=opts.board_root)
248
249
250def GetParser():
251 """ArgumentParser builder and argument definitions."""
252 parser = commandline.ArgumentParser(description=__doc__)
253 parser.add_argument('-b', '--board',
254 default=os.environ.get('DEFAULT_BOARD'),
255 help='The name of the board to set up.')
Alex Klein5ff03ca2018-09-11 07:46:53 -0600256 parser.add_argument('-r', '--board-root',
Alex Kleina2ceb192018-08-17 11:19:32 -0600257 type='path',
258 help='Board root where the profile should be created.')
259 parser.add_argument('-p', '--profile',
260 help='The portage configuration profile to use.')
261 parser.add_argument('-v', '--variant', help='Board variant.')
262
263 group = parser.add_argument_group('Advanced options')
264 group.add_argument('--filesystem-prefix',
265 type='path',
266 help='Force filesystem accesses to be prefixed by the '
267 'given path.')
268 return parser
269
270
271def ParseArgs(argv):
272 """Parse and validate the arguments."""
273 parser = GetParser()
274 opts = parser.parse_args(argv)
275
276 # See Board.__init__ Preconditions.
277 board_valid = opts.board is not None
278 board_root_valid = opts.board_root and os.path.exists(opts.board_root)
279
280 if not board_valid and not board_root_valid:
281 parser.error('Either board or board_root must be provided.')
282
283 PathPrefixDecorator.prefix = opts.filesystem_prefix
284 del opts.filesystem_prefix
285
286 opts.Freeze()
287 return opts
288
289
290def main(argv):
291 # Parse arguments.
292 opts = ParseArgs(argv)
293
294 # Build and validate the board and profile.
295 board = _GetBoard(opts)
296
297 if not os.path.exists(board.root):
298 cros_build_lib.Die('The board has not been setup, please run setup_board '
299 'first.')
300
301 try:
302 profile = _GetProfile(opts, board)
303 except ProfileDirectoryNotFoundError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400304 cros_build_lib.Die(e)
Alex Kleina2ceb192018-08-17 11:19:32 -0600305
306 # Change the profile to the selected.
307 logging.info('Selecting profile: %s for %s', profile.directory, board.root)
308
309 try:
310 ChooseProfile(board, profile)
311 except MakeProfileIsNotLinkError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400312 cros_build_lib.Die(e)