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