Alex Klein | a2ceb19 | 2018-08-17 11:19:32 -0600 | [diff] [blame] | 1 | # 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 Klein | a2ceb19 | 2018-08-17 11:19:32 -0600 | [diff] [blame] | 7 | import functools |
Chris McDonald | 59650c3 | 2021-07-20 15:29:28 -0600 | [diff] [blame] | 8 | import logging |
Alex Klein | a2ceb19 | 2018-08-17 11:19:32 -0600 | [diff] [blame] | 9 | import os |
| 10 | |
Mike Frysinger | 06a51c8 | 2021-04-06 11:39:17 -0400 | [diff] [blame] | 11 | from chromite.lib import build_target_lib |
Alex Klein | a2ceb19 | 2018-08-17 11:19:32 -0600 | [diff] [blame] | 12 | from chromite.lib import commandline |
| 13 | from chromite.lib import cros_build_lib |
Alex Klein | a2ceb19 | 2018-08-17 11:19:32 -0600 | [diff] [blame] | 14 | from chromite.lib import osutils |
| 15 | from chromite.lib import sysroot_lib |
| 16 | |
| 17 | |
| 18 | # Default value constants. |
| 19 | _DEFAULT_PROFILE = 'base' |
| 20 | |
| 21 | |
| 22 | def 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 Frysinger | e852b07 | 2021-05-21 12:39:03 -0400 | [diff] [blame] | 38 | elif not isinstance(result, str): |
Alex Klein | a2ceb19 | 2018-08-17 11:19:32 -0600 | [diff] [blame] | 39 | # 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 | |
| 55 | PathPrefixDecorator.prefix = None |
| 56 | |
| 57 | |
| 58 | class Error(Exception): |
| 59 | """Base error for custom exceptions in this script.""" |
| 60 | |
| 61 | |
| 62 | class InvalidArgumentsError(Error): |
| 63 | """Invalid arguments.""" |
| 64 | |
| 65 | |
| 66 | class MakeProfileIsNotLinkError(Error): |
| 67 | """The make profile exists but is not a link.""" |
| 68 | |
| 69 | |
| 70 | class ProfileDirectoryNotFoundError(Error): |
| 71 | """Unable to find the profile directory.""" |
| 72 | |
| 73 | |
| 74 | def 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 | |
| 120 | class 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 | |
| 133 | def _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 | |
| 165 | class 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 Frysinger | 06a51c8 | 2021-04-06 11:39:17 -0400 | [diff] [blame] | 223 | return build_target_lib.get_default_sysroot_path(self.board_variant) |
Alex Klein | a2ceb19 | 2018-08-17 11:19:32 -0600 | [diff] [blame] | 224 | |
| 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 | |
| 240 | def _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 | |
| 246 | def 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 Klein | 5ff03ca | 2018-09-11 07:46:53 -0600 | [diff] [blame] | 252 | parser.add_argument('-r', '--board-root', |
Alex Klein | a2ceb19 | 2018-08-17 11:19:32 -0600 | [diff] [blame] | 253 | 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 Klein | 3c345ec | 2020-03-30 16:08:40 -0600 | [diff] [blame] | 257 | parser.add_argument('--variant', help='Board variant.') |
Alex Klein | a2ceb19 | 2018-08-17 11:19:32 -0600 | [diff] [blame] | 258 | |
| 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 | |
| 267 | def 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 | |
| 286 | def 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 Frysinger | 6b5c3cd | 2019-08-27 16:51:00 -0400 | [diff] [blame] | 300 | cros_build_lib.Die(e) |
Alex Klein | a2ceb19 | 2018-08-17 11:19:32 -0600 | [diff] [blame] | 301 | |
| 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 Frysinger | 6b5c3cd | 2019-08-27 16:51:00 -0400 | [diff] [blame] | 308 | cros_build_lib.Die(e) |