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