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