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