blob: dda8cf5766ee09eb64b9b69d14ec4a277e4d9adb [file] [log] [blame]
# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Utility functions shared between files in the chromite shell."""
# Python imports
import ConfigParser
import cPickle
import os
import tempfile
# Local imports
import chromite.lib.cros_build_lib as cros_lib
from chromite.lib import text_menu
# Find the Chromite root and Chromium OS root... Note: in the chroot we may
# choose to install Chromite somewhere (/usr/lib/chromite?), so we use the
# environment variable to get the right place if it exists.
CHROMITE_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..')
SRCROOT_PATH = os.environ.get('CROS_WORKON_SRCROOT',
os.path.realpath(os.path.join(CHROMITE_PATH,
'..')))
# Commands can take one of these two types of specs. Note that if a command
# takes a build spec, we will find the associated chroot spec. This should be
# a human-readable string. It is printed and also is the name of the spec
# directory.
BUILD_SPEC_TYPE = 'build'
CHROOT_SPEC_TYPE = 'chroot'
# This is a special target that indicates that you want to do something just
# to the host. This means different things to different commands.
# TODO(dianders): Good idea or bad idea?
_HOST_TARGET = 'HOST'
def GetBoardDir(build_config):
"""Returns the board directory (inside the chroot) given the name.
Args:
build_config: A SafeConfigParser representing the config that we're
building.
Returns:
The absolute path to the board.
"""
target_name = build_config.get('DEFAULT', 'target')
# Extra checks on these, since we sometimes might do a rm -f on the board
# directory and these could cause some really bad behavior.
assert target_name, "Didn't expect blank target name."
assert len(target_name.split()) == 1, 'Target name should have no whitespace.'
return os.path.join('/', 'build', target_name)
def GetChrootAbsDir(chroot_config):
"""Returns the absolute chroot directory the chroot config.
Args:
chroot_config: A SafeConfigParser representing the config for the chroot.
Returns:
The chroot directory, always absolute.
"""
chroot_dir = chroot_config.get('CHROOT', 'path')
chroot_dir = os.path.join(SRCROOT_PATH, chroot_dir)
return chroot_dir
def DoesChrootExist(chroot_config):
"""Return True if the chroot folder exists.
Args:
chroot_config: A SafeConfigParser representing the config for the chroot.
Returns:
True if the chroot folder exists.
"""
chroot_dir = GetChrootAbsDir(chroot_config)
return os.path.isdir(chroot_dir)
def FindSpec(spec_name, spec_type=BUILD_SPEC_TYPE, can_show_ui=True):
"""Find the spec with the given name.
This tries to be smart about helping the user to find the right spec. See
the spec_name parameter for details.
Args:
spec_name: Can be any of the following:
1. A full path to a spec file (including the .spec suffix). This is
checked first (i.e. if os.path.isfile(spec_name), we assume we've
got this case).
2. The full name of a spec file somewhere in the spec search path
(not including the .spec suffix). This is checked second. Putting
this check second means that if one spec name is a substring of
another, you can still specify the shorter spec name and know you
won't get a menu (the exact match prevents the menu).
3. A substring that will be used to pare-down a menu of spec files
found in the spec search path. Can be the empty string to show a
menu of all spec files in the spec path. NOTE: Only possible if
can_show_ui is True.
spec_type: The type of spec this is: 'chroot' or 'build'.
can_show_ui: If True, enables the spec name to be a substring since we can
then show a menu if the substring matches more than one thing.
Returns:
A path to the spec file.
"""
spec_suffix = '.spec'
# Handle 'HOST' for spec name w/ no searching, so it counts as an exact match.
if spec_type == BUILD_SPEC_TYPE and spec_name == _HOST_TARGET.lower():
return _HOST_TARGET
# If we have an exact path name, that's it. No searching.
if os.path.isfile(spec_name):
return spec_name
# Figure out what our search path should be.
# ...make these lists in anticipation of the need to support specs that live
# in private overlays.
# TODO(dianders): Should specs be part of the shell, or part of the main
# chromite?
search_path = [
os.path.join(CHROMITE_PATH, 'specs', spec_type),
]
# Look for an exact match of a spec name. An exact match will go through with
# no menu.
if spec_name:
for dir_path in search_path:
spec_path = os.path.join(dir_path, spec_name + spec_suffix)
if os.path.isfile(spec_path):
return spec_path
# cros_lib.Die right away if we can't show UI and didn't have an exact match.
if not can_show_ui:
cros_lib.Die("Couldn't find %s spec: %s" % (spec_type, spec_name))
# No full path and no exact match. Move onto a menu.
# First step is to construct the options. We'll store in a dict keyed by
# spec name.
options = {}
for dir_path in search_path:
for file_name in os.listdir(dir_path):
# Find any files that end with ".spec" in a case-insensitive manner.
if not file_name.lower().endswith(spec_suffix):
continue
this_spec_name, _ = os.path.splitext(file_name)
# Skip if this spec file doesn't contain our substring. We are _always_
# case insensitive here to be helpful to the user.
if spec_name.lower() not in this_spec_name.lower():
continue
# Disallow the spec to appear twice in the search path. This is the
# safest thing for now. We could revisit it later if we ever found a
# good reason (and if we ever have more than one directory in the
# search path).
if this_spec_name in options:
cros_lib.Die('Spec %s was found in two places in the search path' %
this_spec_name)
# Combine to get a full path.
full_path = os.path.join(dir_path, file_name)
# Ignore directories or anything else that isn't a file.
if not os.path.isfile(full_path):
continue
# OK, it's good. Store the path.
options[this_spec_name] = full_path
# Add 'HOST'. All caps so it sorts first.
if (spec_type == BUILD_SPEC_TYPE and
spec_name.lower() in _HOST_TARGET.lower()):
options[_HOST_TARGET] = _HOST_TARGET
# If no match, die.
if not options:
cros_lib.Die("Couldn't find any matching %s specs for: %s" % (spec_type,
spec_name))
# Avoid showing the user a menu if the user's search string matched exactly
# one item.
if spec_name and len(options) == 1:
_, spec_path = options.popitem()
return spec_path
# If more than one, show a menu...
option_keys = sorted(options.iterkeys())
choice = text_menu.TextMenu(option_keys, 'Choose a %s spec' % spec_type)
if choice is None:
cros_lib.Die('OK, cancelling...')
else:
return options[option_keys[choice]]
def ReadConfig(spec_path):
"""Read the a build config or chroot config from a spec file.
This includes adding thue proper _default stuff in.
Args:
spec_path: The path to the build or chroot spec.
Returns:
config: A SafeConfigParser representing the config.
"""
spec_name, _ = os.path.splitext(os.path.basename(spec_path))
spec_dir = os.path.dirname(spec_path)
config = ConfigParser.SafeConfigParser({'name': spec_name})
config.read(os.path.join(spec_dir, '_defaults'))
config.read(spec_path)
return config
def GetBuildConfigFromArgs(argv):
"""Helper for commands that take a build config in the arg list.
This function can cros_lib.Die() in some instances.
Args:
argv: A list of command line arguments. If non-empty, [0] should be the
build spec. These will not be modified.
Returns:
argv: The argv with the build spec stripped off. This might be argv[1:] or
just argv. Not guaranteed to be new memory.
build_config: The SafeConfigParser for the build config; might be None if
this is a host config. TODO(dianders): Should there be a build spec for
the host?
"""
# The spec name is optional. If no arguments, we'll show a menu...
# Note that if there are arguments, but the first argument is a flag, we'll
# assume that we got called before OptionParser. In that case, they might
# have specified options but still want the board menu.
if argv and not argv[0].startswith('-'):
spec_name = argv[0]
argv = argv[1:]
else:
spec_name = ''
spec_path = FindSpec(spec_name)
if spec_path == _HOST_TARGET:
return argv, None
build_config = ReadConfig(spec_path)
# TODO(dianders): Add a config checker class that makes sure that the
# target is not a blank string. Might also be good to make sure that the
# target has no whitespace (so we don't screw up a subcommand invoked
# through a shell).
return argv, build_config
def EnterChroot(chroot_config, func, *args, **kwargs):
"""Re-run the given function inside the chroot.
When the function is run, it will be run in a SEPARATE INSTANCE of chromite,
which will be run in the chroot. This is a little weird. Specifically:
- When the callee executes, it will be a separate python instance.
- Globals will be reset back to defaults.
- A different version of python (with different modules) may end up running
the script in the chroot.
- All arguments are pickled up into a temp file, whose path is passed on the
command line.
- That means that args must be pickleable.
- It also means that modifications to the parameters by the callee are not
visible to the caller.
- Even the function is "pickled". The way the pickle works, I belive, is it
just passes the name of the function. If this name somehow resolves
differently in the chroot, you may get weirdness.
- Since we're in the chroot, obviously files may have different paths. It's
up to you to convert parameters if you need to.
- The stdin, stdout, and stderr aren't touched.
Args:
chroot_config: A SafeConfigParser representing the config for the chroot.
func: Either: a) the function to call or b) A tuple of an object and the
name of the method to call.
args: All other arguments will be passed to the function as is.
kwargs: All other arguments will be passed to the function as is.
"""
# Make sure that the chroot exists...
chroot_dir = GetChrootAbsDir(chroot_config)
if not DoesChrootExist(chroot_config):
cros_lib.Die(
'Chroot dir does not exist; try the "build host" command.\n %s.' %
chroot_dir)
# Save state to a temp file (inside the chroot!) using pickle.
tmp_dir = os.path.join(chroot_dir, 'tmp')
state_file = tempfile.NamedTemporaryFile(prefix='chromite', dir=tmp_dir)
try:
cPickle.dump((func, args, kwargs), state_file, cPickle.HIGHEST_PROTOCOL)
state_file.flush()
# Translate temp file name into a chroot path...
chroot_state_path = os.path.join('/tmp', os.path.basename(state_file.name))
# Put together command. We're going to force the shell to do all of the
# splitting of arguments, since we're throwing all of the flags from the
# config file in there.
# TODO(dianders): It might be nice to run chromite as:
# python -m chromite.chromite_main
# ...but, at the moment, that fails if you're in src/scripts
# which already has a chromite folder.
cmd = (
'./enter_chroot.sh --chroot="%s" %s --'
' python ../../chromite/shell/main.py --resume-state %s') % (
chroot_dir,
chroot_config.get('CHROOT', 'enter_chroot_flags'),
chroot_state_path)
# We'll put CWD as src/scripts when running the command. Since everyone
# running by hand has their cwd there, it is probably the safest.
cwd = os.path.join(SRCROOT_PATH, 'src', 'scripts')
# Run it. We allow "error" so we don't print a confusing error message
# filled with out resume-state garbage on control-C.
cmd_result = cros_lib.RunCommand(cmd, shell=True, cwd=cwd, print_cmd=False,
exit_code=True, error_ok=True, ignore_sigint=True)
if cmd_result.returncode:
cros_lib.Die('Chroot exited with error code %d' % cmd_result.returncode)
finally:
# Make sure things get closed (and deleted), even upon exception.
state_file.close()
def ResumeEnterChrootIfNeeded(argv):
"""Should be called as the first line in main() to support EnterChroot().
We'll check whether the --resume-state argument is there. If the argument is
there, we'll use it and call the proper function (now that we're in the
chroot). We'll then return True to indicate to main() that it should exit
immediately.
If the --resume-state argument is not there, this function will return False
without doing anything else.
Args:
argv: The value of sys.argv
Returns:
True if we resumed; indicates that main should return without doing any
further work.
"""
if argv[1:2] == ['--resume-state']:
# Internal mechanism (not documented to users) to resume in the chroot.
# ...actual resume state file is passed in argv[2] for simplicity...
assert len(argv) == 3, 'Resume State not passed properly.'
func, args, kwargs = cPickle.load(open(argv[2], 'rb'))
# Handle calling a method in a class; that can't be directly pickled.
if isinstance(func, tuple):
obj, method = func
func = getattr(obj, method)
func(*args, **kwargs) # pylint: disable=W0142
# Return True to tell main() that it should exit.
return True
else:
# Return False to tell main() that we didn't find the --resume-state
# argument and that it should do normal arugment parsing.
return False