blob: a8a0a242cd66fb4ad40d1507821fe421d416164a [file] [log] [blame]
#!/usr/bin/python -Bu
#
# Copyright (c) 2012 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.
import argparse
import logging
import multiprocessing
import os
import re
import shutil
import sys
import tempfile
import factory_common # pylint: disable=W0611
from cros.factory.tools.build_board import BuildBoard
from cros.factory.utils import file_utils
from cros.factory.utils.sys_utils import MountPartition
from cros.factory.utils.process_utils import Spawn
FIRMWARE_UPDATE_BINARIES = [
'crossystem', 'dump_fmap', 'flashrom', 'gbb_utility', 'mosys', 'vpd']
SRC = os.path.join(os.environ['CROS_WORKON_SRCROOT'], 'src')
def GetDefaultBoardOrNone():
try:
return open(os.path.join(SRC, 'scripts', '.default_board')).read().strip()
except IOError:
return None
def ParseListArg(value):
value = sum([re.split('[ ,:]', x) for x in value], [])
return [x for x in value if x]
def ContentsDiffer(src_path, dest_path):
"""Returns True if the two files differ.
The files are considered to differ if:
- Their modes are different.
- If the files are firmware updaters, and they contain any differing files
*except* for the binaries listed in FIRMWARE_UPDATE_BINARIES.
- If the files are not firmware updaters, and they have different contents.
"""
src_stat = os.stat(src_path)
dest_stat = os.stat(dest_path)
if (src_stat.st_mode & ~7) != (dest_stat.st_mode & ~7):
return True
if all(os.path.basename(x) == 'chromeos-firmwareupdate'
for x in (src_path, dest_path)):
hashes = []
for p in src_path, dest_path:
output = Spawn([p, '-V'], log=True, check_output=True).stdout_data
hash_codes = {}
for line in output.rpartition('Package Content:')[2].strip().split('\n'):
hash_code, _, filename = line.rpartition(' *./')
assert hash_code and filename, (
'Unable to parse chromeos-firmwareupdate output')
if filename not in FIRMWARE_UPDATE_BINARIES:
# Not a binary file; add to the hashes
hash_codes[filename] = hash_code
hashes.append(hash_codes)
logging.info('Firmware hashes: %s', hashes)
return hashes[0] != hashes[1]
else:
return ((src_stat.st_size != dest_stat.st_size) or
(open(src_path).read() != open(dest_path).read()))
PACKAGES = {
'factory':
dict(path='platform/factory',
package='chromeos-base/factory',
workon=True),
'chromeos-factory-board':
dict(path='%(overlay_relpath)s',
package='chromeos-base/chromeos-factory-board',
workon=False),
'autotest-private-board':
dict(path='%(overlay_relpath)s',
package='chromeos-base/autotest-private-board',
workon=False),
'autotest':
dict(path='third_party/autotest/files',
package='chromeos-base/autotest',
workon=True),
'autotest-factory':
dict(path='third_party/autotest/files',
package='chromeos-base/autotest-factory',
workon=True),
}
# A secret value for 'output' to make the script modify the image in place.
# Only for testing.
IN_PLACE = 'IN_PLACE'
OLD_IMAGE_MOUNT_POINT = '/tmp/old_image_mount'
NEW_IMAGE_MOUNT_POINT = '/tmp/new_image_mount'
ALL = 'ALL'
def HasDeletedComponent(old_image_mount_path, rel_path):
"""Returns True if rel_path has a deleted component.
This is true if any directory that is an ancestor of rel_path has been
deleted by finalize_bundle and thus has a _DELETED marker.
"""
path = old_image_mount_path
for component in rel_path.split(os.sep):
path = os.path.join(path, component)
if os.path.exists(path + '_DELETED'):
return True
return False
def main():
parser = argparse.ArgumentParser(
description='Patches a factory image according with particular commits.')
parser.add_argument('--input', '-i', help='Input image', required=True)
parser.add_argument('--output', '-o', help='Output image', required=True)
parser.add_argument('--output-updater', help='Output factory.tar.bz2')
parser.add_argument('--branch', '-b',
help='Branch to patch (e.g., factory-2848.B or HEAD)',
required=True)
parser.add_argument('--verbose', '-v', action='count')
parser.add_argument('--packages', '--package',
help=('Packages to patch in (%s, or '
'one or more of [%s])' % (
ALL, ','.join(sorted(PACKAGES.keys())))),
action='append', required=True)
parser.add_argument('--board', help='Board name', default=None)
parser.add_argument('--no-clean', action='store_false', dest='clean',
help="Don't insist on clean repositories (be careful!)")
parser.add_argument('--no-emerge', action='store_false', dest='emerge',
help="Don't emerge (for debugging only)")
parser.add_argument('--no-sync', action='store_false', dest='sync',
help="Don't emerge (for debugging only)")
parser.add_argument('--no-firmware', action='store_false', dest='firmware',
help='Never update chromeos-firmwareupdater')
parser.add_argument('--yes', '-y', action='store_true',
help="Don't ask for confirmation")
args = parser.parse_args()
logging.basicConfig(level=logging.INFO - 10 * (args.verbose or 0))
if not os.path.exists(args.input):
parser.error('Input image %s does not exist' % args.input)
if args.output != IN_PLACE and os.path.exists(args.output):
parser.error('Output file %s exists; please remove it first' % args.output)
if args.output_updater and os.path.exists(args.output_updater):
parser.error('Output update file %s exists; please remove it first' %
args.output_updater)
args.packages = set(ParseListArg(args.packages))
if ALL in args.packages:
args.packages = set(PACKAGES.keys())
bad_packages = args.packages - set(PACKAGES.keys())
if bad_packages:
parser.error('Bad packages %s (should be in %s)' % (
list(bad_packages), sorted(PACKAGES.keys())))
board = BuildBoard(args.board)
repo_relpaths = [
PACKAGES[k]['path'] % {'overlay_relpath': board.overlay_relpath}
for k in args.packages]
repo_paths = sorted(set([os.path.join(SRC, x) for x in repo_relpaths]))
packages = sorted(set(PACKAGES[k]['package'] for k in args.packages))
workon_packages = sorted(set(PACKAGES[k]['package']
for k in args.packages
if PACKAGES[k]['workon']))
# Check the packages are all clean
for path in repo_paths:
stdout = Spawn(['git', 'status', '--ignored', '--porcelain'],
cwd=path, check_output=True).stdout_data
if stdout:
logging.error('Repository %s is not clean:\n%s', path, stdout)
if args.clean:
logging.error('To clean it (but be careful!):\n\n'
'( cd %s && git clean -xdf )', path)
sys.exit(1)
def _CheckoutAppropriateBranchInEachRepo():
"""Check out the appropriate branch in each repo."""
for path in repo_paths:
if args.branch.startswith('factory-'):
branch = (('cros-internal/' if path.endswith('-private') else 'cros/')
+ args.branch)
else:
branch = args.branch
Spawn(['git', 'checkout', branch], cwd=path, log=True, check_call=True)
# Check out the appropriate branch in each repo.
# If HEAD is on a local branch, repo sync will try to rebase on it. We don't
# want it to happen here.
_CheckoutAppropriateBranchInEachRepo()
# Do workons
if workon_packages:
Spawn(['cros_workon', '--board', board.full_name, 'start']
+ workon_packages, log=True, call=True)
# Do repo syncs in parallel (followed by a rebase+sync if it fails)
if args.sync:
for i, process in enumerate(
[Spawn('repo sync . || (repo rebase . && repo sync .)',
log=True, cwd=path, shell=True) for path in repo_paths]):
if process.wait() != 0:
sys.exit('git fetch in %s failed' % repo_paths[i])
for path in repo_paths:
process = Spawn(['repo', 'rebase', '.'], read_stderr=True,
cwd=path, log=True, call=True)
if process.wait() != 0:
if (process.returncode == 255 and
re.search('has a detat?ched HEAD', process.stderr_data)):
pass
else:
sys.exit('repo rebase in %s failed' % path)
# Check out the appropriate branch in each repo.
# If this chroot is not inited -b with the factory branch we want,
# HEAD will be on the remote tracking branch specified in the manifest.xml
# (maybe on master branch or other factory branch). So after repo sync,
# we have to checkout to the branch we want.
_CheckoutAppropriateBranchInEachRepo()
# Emerge the packages
tarballs = []
if args.emerge:
# If there are any autotest packages required, unmerge them all so
# that any old packages don't get in the way. There's probably a
# better way to do this.
if [x for x in args.packages if x.startswith('autotest')]:
Spawn('emerge-%s --unmerge $(cros_workon-%s list --all | grep autotest)'
% (board.full_name, board.full_name),
log=True, shell=True, check_call=True)
Spawn(['emerge-%s' % board.full_name, '--buildpkg',
'-j', str(multiprocessing.cpu_count())] +
packages,
log=True, check_call=True)
for package in packages:
ebuild = Spawn(
['equery-%s' % board.full_name, 'w', package],
check_output=True).stdout_data.strip()
tarball = os.path.join(
'/build', board.full_name, 'packages',
os.path.dirname(package),
os.path.basename(ebuild).replace('.ebuild', '.tbz2'))
logging.info('%s %s (%d bytes)', 'Built' if args.emerge else 'Reusing',
tarball, os.path.getsize(tarball))
tarballs.append(tarball)
# Create staging directory
staging_dir = tempfile.mkdtemp(prefix='new-image.')
os.chmod(staging_dir, 0755)
# Create the /usr/local/factory/custom symlink.
factory_dir = os.path.join(staging_dir, 'usr', 'local', 'factory')
file_utils.TryMakeDirs(factory_dir)
os.symlink('../autotest/client/site_tests/suite_Factory',
os.path.join(factory_dir, 'custom'))
# Unpack tarballs to staging directory
for t in tarballs:
Spawn(['tar', 'xfj', t, '-C', staging_dir],
check_call=True, log=True)
# Apply install mask
install_mask = Spawn(
['source %s && echo "$FACTORY_TEST_INSTALL_MASK"' %
os.path.join(SRC, 'scripts', 'common.sh')],
shell=True, check_output=True, log=True).stdout_data.strip().split()
for f in install_mask:
# Use shell to expand glob since Python's globbing is a bit stupid
assert not re.search(r'\s', f)
Spawn(['shopt -s nullglob; rm -rf %s/%s' % (staging_dir, f)],
shell=True, check_call=True)
# Move /usr/local/factory to dev_image.
dev_image = os.path.join(staging_dir, 'dev_image')
os.mkdir(dev_image)
path = os.path.join(staging_dir, 'usr', 'local', 'factory')
if os.path.exists(path):
shutil.move(path, dev_image)
# Move /usr/local/autotest/client to /usr/local/autotest.
path = os.path.join(staging_dir, 'usr', 'local', 'autotest', 'client')
if os.path.exists(path):
shutil.move(path, os.path.join(dev_image, 'autotest'))
# Delete usr and var directories
for f in ['usr', 'var']:
path = os.path.join(staging_dir, f)
if os.path.exists(path):
shutil.rmtree(path)
diffs = tempfile.NamedTemporaryFile(prefix='patch_image.diff.',
delete=False)
# Find and remove identical files to avoid massive mtime changes.
file_utils.TryMakeDirs(OLD_IMAGE_MOUNT_POINT)
with MountPartition(args.input, 1, OLD_IMAGE_MOUNT_POINT):
for root, dirs, files in os.walk(staging_dir):
for is_dir in [False, True]:
for f in dirs if is_dir else files:
path = os.path.join(root, f)
assert path.startswith(staging_dir + '/')
rel_path = os.path.relpath(path, staging_dir)
dest_path = os.path.join(OLD_IMAGE_MOUNT_POINT, rel_path)
if not os.path.exists(dest_path):
if HasDeletedComponent(OLD_IMAGE_MOUNT_POINT, rel_path):
# Removed from the image; forget it.
if not is_dir:
os.unlink(path)
continue
diffs.write('*** File %s does not exist in old image\n' % rel_path)
continue
src_islink = os.path.islink(path)
dest_islink = os.path.islink(dest_path)
if src_islink != dest_islink:
continue
if src_islink:
if os.readlink(path) == os.readlink(dest_path):
# They are identical. No need to rsync; delete it.
os.unlink(path)
continue
if is_dir:
continue
if f in ['.keep', 'chromedriver']:
# Just to tell Gentoo to keep the directory; delete it
os.unlink(path)
continue
if ((not args.firmware) and
os.path.basename(path) == 'chromeos-firmwareupdate'):
os.unlink(path)
continue
if ContentsDiffer(path, dest_path):
# They are different; write a diff
Spawn(['diff', '-u', dest_path, path], stdout=diffs, call=True)
else:
# They are identical. No need to rsync; delete the src file.
os.unlink(path)
# Delete empty directories in dev_image
for root, dirs, files in os.walk(dev_image, topdown=False):
for d in dirs:
try:
os.rmdir(os.path.join(root, d))
except OSError:
pass # Not empty, no worries
diffs.close()
# Do a "find" command to show all affected paths.
sys.stdout.write(
('\n'
'\n'
'*** The following changes files will be patched into the image.\n'
'***\n'
'*** Note that the individual changes that you mentioned will not\n'
'*** be cherry-picked; rather the LATEST VERSION of the file in the\n'
'*** LATEST TREE you specified on the command line will be chosen.\n'
'***\n'
'*** DISCLAIMER: This script is experimental! Make sure to\n'
'*** double-check that all the changes you expect are really included!\n'
'***\n'
'\n'
'cd %s\n'
'\n'
'%s'
'\n'
'*** Diffs are available in %s\n'
'*** Check them carefully!\n'
'***\n') %
(staging_dir,
Spawn('find . ! -type d -print0 | xargs -0 ls -ld',
cwd=staging_dir, shell=True,
check_output=True).stdout_data,
diffs.name))
if not args.yes:
sys.stdout.write('*** Is this correct? [y/N] ')
answer = sys.stdin.readline()
if not answer or answer[0] not in 'yY':
sys.exit('Aborting.')
if args.output == IN_PLACE:
logging.warn('Modifying image %s in place! Be very afraid!', args.input)
args.output = args.input
else:
logging.info('Copying %s to %s', args.input, args.output)
shutil.copyfile(args.input, args.output)
file_utils.TryMakeDirs(NEW_IMAGE_MOUNT_POINT)
with MountPartition(args.output, 1, NEW_IMAGE_MOUNT_POINT, rw=True):
Spawn(['rsync', '-av', staging_dir + '/', NEW_IMAGE_MOUNT_POINT + '/'],
sudo=True, log=True, check_output=True)
logging.info('\n'
'***\n'
'*** Created %s (%d bytes)\n'
'***', args.output, os.path.getsize(args.output))
if args.output_updater:
Spawn([os.path.join(os.path.dirname(os.path.realpath(__file__)),
'make_update_bundle.py'),
'-i', args.output, '-o', args.output_updater],
log=True, check_call=True)
if __name__ == '__main__':
main()