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