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