Doug Anderson | a9b1090 | 2011-02-01 17:54:31 -0800 | [diff] [blame] | 1 | # Copyright (c) 2011 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 | """Utility functions shared between files in the chromite shell.""" |
| 6 | |
| 7 | |
| 8 | # Python imports |
| 9 | import ConfigParser |
| 10 | import cPickle |
| 11 | import os |
Doug Anderson | a9b1090 | 2011-02-01 17:54:31 -0800 | [diff] [blame] | 12 | import tempfile |
| 13 | |
| 14 | |
| 15 | # Local imports |
| 16 | import chromite.lib.cros_build_lib as cros_lib |
| 17 | from chromite.lib import text_menu |
| 18 | |
| 19 | |
| 20 | # Find the Chromite root and Chromium OS root... Note: in the chroot we may |
| 21 | # choose to install Chromite somewhere (/usr/lib/chromite?), so we use the |
| 22 | # environment variable to get the right place if it exists. |
| 23 | CHROMITE_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..') |
| 24 | SRCROOT_PATH = os.environ.get('CROS_WORKON_SRCROOT', |
| 25 | os.path.realpath(os.path.join(CHROMITE_PATH, |
| 26 | '..'))) |
| 27 | |
| 28 | |
| 29 | # Commands can take one of these two types of specs. Note that if a command |
| 30 | # takes a build spec, we will find the associated chroot spec. This should be |
| 31 | # a human-readable string. It is printed and also is the name of the spec |
| 32 | # directory. |
| 33 | BUILD_SPEC_TYPE = 'build' |
| 34 | CHROOT_SPEC_TYPE = 'chroot' |
| 35 | |
| 36 | |
| 37 | # This is a special target that indicates that you want to do something just |
| 38 | # to the host. This means different things to different commands. |
| 39 | # TODO(dianders): Good idea or bad idea? |
| 40 | _HOST_TARGET = 'HOST' |
| 41 | |
| 42 | |
| 43 | def GetBoardDir(build_config): |
| 44 | """Returns the board directory (inside the chroot) given the name. |
| 45 | |
| 46 | Args: |
| 47 | build_config: A SafeConfigParser representing the config that we're |
| 48 | building. |
| 49 | |
| 50 | Returns: |
| 51 | The absolute path to the board. |
| 52 | """ |
Doug Anderson | 6f53061 | 2011-02-11 13:19:25 -0800 | [diff] [blame] | 53 | target_name = build_config.get('DEFAULT', 'target') |
Doug Anderson | a9b1090 | 2011-02-01 17:54:31 -0800 | [diff] [blame] | 54 | |
| 55 | # Extra checks on these, since we sometimes might do a rm -f on the board |
| 56 | # directory and these could cause some really bad behavior. |
| 57 | assert target_name, "Didn't expect blank target name." |
| 58 | assert len(target_name.split()) == 1, 'Target name should have no whitespace.' |
| 59 | |
| 60 | return os.path.join('/', 'build', target_name) |
| 61 | |
| 62 | |
| 63 | def GetChrootAbsDir(chroot_config): |
| 64 | """Returns the absolute chroot directory the chroot config. |
| 65 | |
| 66 | Args: |
| 67 | chroot_config: A SafeConfigParser representing the config for the chroot. |
| 68 | |
| 69 | Returns: |
| 70 | The chroot directory, always absolute. |
| 71 | """ |
| 72 | chroot_dir = chroot_config.get('CHROOT', 'path') |
| 73 | chroot_dir = os.path.join(SRCROOT_PATH, chroot_dir) |
| 74 | |
| 75 | return chroot_dir |
| 76 | |
| 77 | |
| 78 | def DoesChrootExist(chroot_config): |
| 79 | """Return True if the chroot folder exists. |
| 80 | |
| 81 | Args: |
| 82 | chroot_config: A SafeConfigParser representing the config for the chroot. |
| 83 | |
| 84 | Returns: |
| 85 | True if the chroot folder exists. |
| 86 | """ |
| 87 | chroot_dir = GetChrootAbsDir(chroot_config) |
| 88 | return os.path.isdir(chroot_dir) |
| 89 | |
| 90 | |
| 91 | def FindSpec(spec_name, spec_type=BUILD_SPEC_TYPE, can_show_ui=True): |
| 92 | """Find the spec with the given name. |
| 93 | |
| 94 | This tries to be smart about helping the user to find the right spec. See |
| 95 | the spec_name parameter for details. |
| 96 | |
| 97 | Args: |
| 98 | spec_name: Can be any of the following: |
| 99 | 1. A full path to a spec file (including the .spec suffix). This is |
| 100 | checked first (i.e. if os.path.isfile(spec_name), we assume we've |
| 101 | got this case). |
| 102 | 2. The full name of a spec file somewhere in the spec search path |
| 103 | (not including the .spec suffix). This is checked second. Putting |
| 104 | this check second means that if one spec name is a substring of |
| 105 | another, you can still specify the shorter spec name and know you |
| 106 | won't get a menu (the exact match prevents the menu). |
| 107 | 3. A substring that will be used to pare-down a menu of spec files |
| 108 | found in the spec search path. Can be the empty string to show a |
| 109 | menu of all spec files in the spec path. NOTE: Only possible if |
| 110 | can_show_ui is True. |
| 111 | spec_type: The type of spec this is: 'chroot' or 'build'. |
| 112 | can_show_ui: If True, enables the spec name to be a substring since we can |
| 113 | then show a menu if the substring matches more than one thing. |
| 114 | |
| 115 | Returns: |
| 116 | A path to the spec file. |
| 117 | """ |
| 118 | spec_suffix = '.spec' |
| 119 | |
| 120 | # Handle 'HOST' for spec name w/ no searching, so it counts as an exact match. |
| 121 | if spec_type == BUILD_SPEC_TYPE and spec_name == _HOST_TARGET.lower(): |
| 122 | return _HOST_TARGET |
| 123 | |
| 124 | # If we have an exact path name, that's it. No searching. |
| 125 | if os.path.isfile(spec_name): |
| 126 | return spec_name |
| 127 | |
| 128 | # Figure out what our search path should be. |
| 129 | # ...make these lists in anticipation of the need to support specs that live |
| 130 | # in private overlays. |
| 131 | # TODO(dianders): Should specs be part of the shell, or part of the main |
| 132 | # chromite? |
| 133 | search_path = [ |
| 134 | os.path.join(CHROMITE_PATH, 'specs', spec_type), |
| 135 | ] |
| 136 | |
| 137 | # Look for an exact match of a spec name. An exact match will go through with |
| 138 | # no menu. |
| 139 | if spec_name: |
| 140 | for dir_path in search_path: |
| 141 | spec_path = os.path.join(dir_path, spec_name + spec_suffix) |
| 142 | if os.path.isfile(spec_path): |
| 143 | return spec_path |
| 144 | |
| 145 | # cros_lib.Die right away if we can't show UI and didn't have an exact match. |
| 146 | if not can_show_ui: |
| 147 | cros_lib.Die("Couldn't find %s spec: %s" % (spec_type, spec_name)) |
| 148 | |
| 149 | # No full path and no exact match. Move onto a menu. |
| 150 | # First step is to construct the options. We'll store in a dict keyed by |
| 151 | # spec name. |
| 152 | options = {} |
| 153 | for dir_path in search_path: |
| 154 | for file_name in os.listdir(dir_path): |
| 155 | # Find any files that end with ".spec" in a case-insensitive manner. |
| 156 | if not file_name.lower().endswith(spec_suffix): |
| 157 | continue |
| 158 | |
| 159 | this_spec_name, _ = os.path.splitext(file_name) |
| 160 | |
| 161 | # Skip if this spec file doesn't contain our substring. We are _always_ |
| 162 | # case insensitive here to be helpful to the user. |
| 163 | if spec_name.lower() not in this_spec_name.lower(): |
| 164 | continue |
| 165 | |
| 166 | # Disallow the spec to appear twice in the search path. This is the |
| 167 | # safest thing for now. We could revisit it later if we ever found a |
| 168 | # good reason (and if we ever have more than one directory in the |
| 169 | # search path). |
| 170 | if this_spec_name in options: |
| 171 | cros_lib.Die('Spec %s was found in two places in the search path' % |
| 172 | this_spec_name) |
| 173 | |
| 174 | # Combine to get a full path. |
| 175 | full_path = os.path.join(dir_path, file_name) |
| 176 | |
| 177 | # Ignore directories or anything else that isn't a file. |
| 178 | if not os.path.isfile(full_path): |
| 179 | continue |
| 180 | |
| 181 | # OK, it's good. Store the path. |
| 182 | options[this_spec_name] = full_path |
| 183 | |
| 184 | # Add 'HOST'. All caps so it sorts first. |
| 185 | if (spec_type == BUILD_SPEC_TYPE and |
| 186 | spec_name.lower() in _HOST_TARGET.lower()): |
| 187 | options[_HOST_TARGET] = _HOST_TARGET |
| 188 | |
| 189 | # If no match, die. |
| 190 | if not options: |
| 191 | cros_lib.Die("Couldn't find any matching %s specs for: %s" % (spec_type, |
| 192 | spec_name)) |
| 193 | |
| 194 | # Avoid showing the user a menu if the user's search string matched exactly |
| 195 | # one item. |
| 196 | if spec_name and len(options) == 1: |
| 197 | _, spec_path = options.popitem() |
| 198 | return spec_path |
| 199 | |
| 200 | # If more than one, show a menu... |
| 201 | option_keys = sorted(options.iterkeys()) |
| 202 | choice = text_menu.TextMenu(option_keys, 'Choose a %s spec' % spec_type) |
| 203 | |
| 204 | if choice is None: |
| 205 | cros_lib.Die('OK, cancelling...') |
| 206 | else: |
| 207 | return options[option_keys[choice]] |
| 208 | |
| 209 | |
| 210 | def ReadConfig(spec_path): |
| 211 | """Read the a build config or chroot config from a spec file. |
| 212 | |
| 213 | This includes adding thue proper _default stuff in. |
| 214 | |
| 215 | Args: |
| 216 | spec_path: The path to the build or chroot spec. |
| 217 | |
| 218 | Returns: |
| 219 | config: A SafeConfigParser representing the config. |
| 220 | """ |
| 221 | spec_name, _ = os.path.splitext(os.path.basename(spec_path)) |
| 222 | spec_dir = os.path.dirname(spec_path) |
| 223 | |
| 224 | config = ConfigParser.SafeConfigParser({'name': spec_name}) |
| 225 | config.read(os.path.join(spec_dir, '_defaults')) |
| 226 | config.read(spec_path) |
| 227 | |
| 228 | return config |
| 229 | |
| 230 | |
| 231 | def GetBuildConfigFromArgs(argv): |
| 232 | """Helper for commands that take a build config in the arg list. |
| 233 | |
| 234 | This function can cros_lib.Die() in some instances. |
| 235 | |
| 236 | Args: |
| 237 | argv: A list of command line arguments. If non-empty, [0] should be the |
| 238 | build spec. These will not be modified. |
| 239 | |
| 240 | Returns: |
| 241 | argv: The argv with the build spec stripped off. This might be argv[1:] or |
| 242 | just argv. Not guaranteed to be new memory. |
| 243 | build_config: The SafeConfigParser for the build config; might be None if |
| 244 | this is a host config. TODO(dianders): Should there be a build spec for |
| 245 | the host? |
| 246 | """ |
| 247 | # The spec name is optional. If no arguments, we'll show a menu... |
| 248 | # Note that if there are arguments, but the first argument is a flag, we'll |
| 249 | # assume that we got called before OptionParser. In that case, they might |
| 250 | # have specified options but still want the board menu. |
| 251 | if argv and not argv[0].startswith('-'): |
| 252 | spec_name = argv[0] |
| 253 | argv = argv[1:] |
| 254 | else: |
| 255 | spec_name = '' |
| 256 | |
| 257 | spec_path = FindSpec(spec_name) |
| 258 | |
| 259 | if spec_path == _HOST_TARGET: |
| 260 | return argv, None |
| 261 | |
| 262 | build_config = ReadConfig(spec_path) |
| 263 | |
| 264 | # TODO(dianders): Add a config checker class that makes sure that the |
| 265 | # target is not a blank string. Might also be good to make sure that the |
| 266 | # target has no whitespace (so we don't screw up a subcommand invoked |
| 267 | # through a shell). |
| 268 | |
| 269 | return argv, build_config |
| 270 | |
| 271 | |
Doug Anderson | a17e010 | 2011-02-07 13:16:55 -0800 | [diff] [blame] | 272 | def EnterChroot(chroot_config, func, *args, **kwargs): |
Doug Anderson | a9b1090 | 2011-02-01 17:54:31 -0800 | [diff] [blame] | 273 | """Re-run the given function inside the chroot. |
| 274 | |
| 275 | When the function is run, it will be run in a SEPARATE INSTANCE of chromite, |
| 276 | which will be run in the chroot. This is a little weird. Specifically: |
| 277 | - When the callee executes, it will be a separate python instance. |
| 278 | - Globals will be reset back to defaults. |
| 279 | - A different version of python (with different modules) may end up running |
| 280 | the script in the chroot. |
| 281 | - All arguments are pickled up into a temp file, whose path is passed on the |
| 282 | command line. |
| 283 | - That means that args must be pickleable. |
| 284 | - It also means that modifications to the parameters by the callee are not |
| 285 | visible to the caller. |
| 286 | - Even the function is "pickled". The way the pickle works, I belive, is it |
| 287 | just passes the name of the function. If this name somehow resolves |
| 288 | differently in the chroot, you may get weirdness. |
| 289 | - Since we're in the chroot, obviously files may have different paths. It's |
| 290 | up to you to convert parameters if you need to. |
| 291 | - The stdin, stdout, and stderr aren't touched. |
| 292 | |
| 293 | Args: |
| 294 | chroot_config: A SafeConfigParser representing the config for the chroot. |
Doug Anderson | a17e010 | 2011-02-07 13:16:55 -0800 | [diff] [blame] | 295 | func: Either: a) the function to call or b) A tuple of an object and the |
Doug Anderson | a9b1090 | 2011-02-01 17:54:31 -0800 | [diff] [blame] | 296 | name of the method to call. |
| 297 | args: All other arguments will be passed to the function as is. |
| 298 | kwargs: All other arguments will be passed to the function as is. |
| 299 | """ |
| 300 | # Make sure that the chroot exists... |
| 301 | chroot_dir = GetChrootAbsDir(chroot_config) |
| 302 | if not DoesChrootExist(chroot_config): |
| 303 | cros_lib.Die( |
| 304 | 'Chroot dir does not exist; try the "build host" command.\n %s.' % |
| 305 | chroot_dir) |
| 306 | |
Doug Anderson | a9b1090 | 2011-02-01 17:54:31 -0800 | [diff] [blame] | 307 | # Save state to a temp file (inside the chroot!) using pickle. |
| 308 | tmp_dir = os.path.join(chroot_dir, 'tmp') |
| 309 | state_file = tempfile.NamedTemporaryFile(prefix='chromite', dir=tmp_dir) |
| 310 | try: |
Doug Anderson | a17e010 | 2011-02-07 13:16:55 -0800 | [diff] [blame] | 311 | cPickle.dump((func, args, kwargs), state_file, cPickle.HIGHEST_PROTOCOL) |
Doug Anderson | a9b1090 | 2011-02-01 17:54:31 -0800 | [diff] [blame] | 312 | state_file.flush() |
| 313 | |
| 314 | # Translate temp file name into a chroot path... |
| 315 | chroot_state_path = os.path.join('/tmp', os.path.basename(state_file.name)) |
| 316 | |
| 317 | # Put together command. We're going to force the shell to do all of the |
| 318 | # splitting of arguments, since we're throwing all of the flags from the |
| 319 | # config file in there. |
| 320 | # TODO(dianders): It might be nice to run chromite as: |
| 321 | # python -m chromite.chromite_main |
| 322 | # ...but, at the moment, that fails if you're in src/scripts |
| 323 | # which already has a chromite folder. |
| 324 | cmd = ( |
| 325 | './enter_chroot.sh --chroot="%s" %s --' |
| 326 | ' python ../../chromite/shell/main.py --resume-state %s') % ( |
| 327 | chroot_dir, |
| 328 | chroot_config.get('CHROOT', 'enter_chroot_flags'), |
| 329 | chroot_state_path) |
| 330 | |
| 331 | # We'll put CWD as src/scripts when running the command. Since everyone |
| 332 | # running by hand has their cwd there, it is probably the safest. |
| 333 | cwd = os.path.join(SRCROOT_PATH, 'src', 'scripts') |
| 334 | |
| 335 | # Run it. We allow "error" so we don't print a confusing error message |
| 336 | # filled with out resume-state garbage on control-C. |
| 337 | cmd_result = cros_lib.RunCommand(cmd, shell=True, cwd=cwd, print_cmd=False, |
| 338 | exit_code=True, error_ok=True, ignore_sigint=True) |
| 339 | |
| 340 | if cmd_result.returncode: |
| 341 | cros_lib.Die('Chroot exited with error code %d' % cmd_result.returncode) |
| 342 | finally: |
| 343 | # Make sure things get closed (and deleted), even upon exception. |
| 344 | state_file.close() |
| 345 | |
| 346 | |
| 347 | def ResumeEnterChrootIfNeeded(argv): |
| 348 | """Should be called as the first line in main() to support EnterChroot(). |
| 349 | |
| 350 | We'll check whether the --resume-state argument is there. If the argument is |
| 351 | there, we'll use it and call the proper function (now that we're in the |
| 352 | chroot). We'll then return True to indicate to main() that it should exit |
| 353 | immediately. |
| 354 | |
| 355 | If the --resume-state argument is not there, this function will return False |
| 356 | without doing anything else. |
| 357 | |
| 358 | Args: |
| 359 | argv: The value of sys.argv |
| 360 | |
| 361 | Returns: |
| 362 | True if we resumed; indicates that main should return without doing any |
| 363 | further work. |
| 364 | """ |
| 365 | if argv[1:2] == ['--resume-state']: |
| 366 | # Internal mechanism (not documented to users) to resume in the chroot. |
| 367 | # ...actual resume state file is passed in argv[2] for simplicity... |
| 368 | assert len(argv) == 3, 'Resume State not passed properly.' |
Doug Anderson | a17e010 | 2011-02-07 13:16:55 -0800 | [diff] [blame] | 369 | func, args, kwargs = cPickle.load(open(argv[2], 'rb')) |
Doug Anderson | a9b1090 | 2011-02-01 17:54:31 -0800 | [diff] [blame] | 370 | |
| 371 | # Handle calling a method in a class; that can't be directly pickled. |
Doug Anderson | a17e010 | 2011-02-07 13:16:55 -0800 | [diff] [blame] | 372 | if isinstance(func, tuple): |
| 373 | obj, method = func |
| 374 | func = getattr(obj, method) |
Doug Anderson | a9b1090 | 2011-02-01 17:54:31 -0800 | [diff] [blame] | 375 | |
Doug Anderson | a17e010 | 2011-02-07 13:16:55 -0800 | [diff] [blame] | 376 | func(*args, **kwargs) # pylint: disable=W0142 |
Doug Anderson | a9b1090 | 2011-02-01 17:54:31 -0800 | [diff] [blame] | 377 | |
| 378 | # Return True to tell main() that it should exit. |
| 379 | return True |
| 380 | else: |
| 381 | # Return False to tell main() that we didn't find the --resume-state |
| 382 | # argument and that it should do normal arugment parsing. |
| 383 | return False |