blob: 12427551c41a581d1399310986a114cf0eeed25b [file] [log] [blame]
#!/usr/bin/env python2
# Copyright 2017 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.
"""Packages firmware images into an executable "shell-ball".
It requires:
- at least one firmware image (*.bin, should be AP or EC or ...)
- pack_dist/updater.sh main script
- pack_stub as the template/stub script for output
- any other additional files used by updater.sh in pack_dist folder
"""
from __future__ import print_function
import argparse
import collections
import glob
import md5
import os
import re
import shutil
import struct
import sys
from StringIO import StringIO
import tarfile
import tempfile
from chromite.lib import cros_build_lib
from chromite.lib import osutils
import libfdt
sys.path.append('utils')
import merge_file
IMAGE_MAIN = 'bios.bin'
IMAGE_MAIN_RW = 'bios_rw.bin'
IMAGE_EC = 'ec.bin'
IMAGE_PD = 'pd.bin'
MODELS_DIR = 'models'
Section = collections.namedtuple('Section', ['offset', 'size'])
ImageFile = collections.namedtuple('ImageFile', ['filename', 'version'])
# File execution permissions. We could use state.S_... but that's confusing.
CHMOD_ALL_READ = 0444
CHMOD_ALL_EXEC = 0555
# For testing
packer = None
class PackError(Exception):
"""Exception returned by FirmwarePacker when something goes wrong"""
pass
class FirmwarePacker(object):
"""Handles building a shell-ball firmware update.
Most member functions raise an exception on error. This can be
RunCommandError if an executed tool fails, or PackError on some other error.
Private members:
_args: Parsed arguments.
_pack_dist: Path to 'pack_dist' directory.
_script_base: Base directory with useful files (src/platform/firmware).
_stub_file: Path to 'pack_stub'.
_shflags_file: Path to shflags script.
_testing: True if running tests.
_basedir: Base temporary directory.
_tmpdir: Temporary directory for use for running tools.
_tmp_dirs: List of temporary directories created.
_versions: Collected version information (StringIO).
_force_dash: Replace the /bin/sh shebang at the top of all scripts with
/bin/dash. This is only used for testing.
_config: Master configuration file Fdt object.
"""
def __init__(self, progname):
# This may or may not provide the full path to the script, but in any case
# we can access the script files using the same path as the script.
self._script_base = os.path.dirname(progname)
self._args = None
self._pack_dist = os.path.join(self._script_base, 'pack_dist')
self._stub_file = os.path.join(self._script_base, 'pack_stub')
self._setvars_template_file = os.path.join(self._script_base,
'setvars_template')
self._shflags_file = os.path.join(self._script_base, 'lib/shflags/shflags')
self._testing = False
self._basedir = None
self._tmpdir = None
self._tmp_dirs = []
self._versions = StringIO()
self._force_dash = False
self._config = None
@staticmethod
def ParseArgs(argv):
"""Parse the available arguments.
Invalid arguments or -h cause this function to print a message and exit.
Args:
argv: List of string arguments (excluding program name / argv[0])
Returns:
argparse.Namespace object containing the attributes.
"""
parser = argparse.ArgumentParser(
description='Produce a firmware update shell-ball')
parser.add_argument('-m', '--model', type=str, dest='models',
action='append',
help='Model name to include in firmware update')
parser.add_argument('-c', '--config', type=str,
help='Filename of master configuration .dtb file')
parser.add_argument(
'-l', '--local', action='store_true',
help='Build a local firmware image. With this option you must provide '
'-b, -e and -p flags to indicate where to find the images for '
'each model. You can use MODEL in the filenames as a placeholder '
'for the model name. For example '
'-b "${root}/firmware/image-MODEL.bin"')
parser.add_argument('-i', '--imagedir', type=str, default='.',
help='Default locations for source images')
parser.add_argument('-b', '--bios_image', type=str,
help='Path of input AP (BIOS) firmware image')
parser.add_argument('-w', '--bios_rw_image', type=str,
help='Path of input BIOS RW firmware image')
parser.add_argument('-e', '--ec_image', type=str,
help='Path of input Embedded Controller firmware image')
parser.add_argument('-p', '--pd_image', type=str,
help='Path of input Power Delivery firmware image')
parser.add_argument('--script', type=str, default='updater.sh',
help='File name of main script file')
parser.add_argument('-o', '--output', type=str,
help='Path of output filename')
parser.add_argument(
'--extra', type=str,
help='Directory list (separated by :) of files to be merged')
arg_parser = parser.add_mutually_exclusive_group(required=False)
arg_parser.add_argument(
'--remove_inactive_updaters', default=True,
action='store_true', help='Remove inactive updater scripts')
arg_parser.add_argument(
'--no-remove_inactive_updaters',
action='store_false', dest='remove_inactive_updaters',
help="Don't remove inactive updater scripts")
parser.add_argument('--create_bios_rw_image', action='store_true',
help='Resign and generate a BIOS RW image')
arg_parser = parser.add_mutually_exclusive_group(required=False)
arg_parser.add_argument(
'--merge_bios_rw_image', default=True, action='store_true',
help='Merge the --bios_rw_image into --bios_image RW sections')
arg_parser.add_argument(
'--no-merge_bios_rw_image', action='store_false',
dest='merge_bios_rw_image',
help="Don't Merge the --bios_rw_image into --bios_image RW sections")
# stable settings
parser.add_argument('--stable_main_version', type=str,
help='Version of stable main firmware')
parser.add_argument('--stable_ec_version', type=str,
help='Version of stable EC firmware')
parser.add_argument('--stable_pd_version', type=str,
help='Version of stable PD firmware')
# embedded tools
parser.add_argument(
'--tools', type=str,
default='flashrom mosys crossystem gbb_utility vpd dump_fmap',
help='List of tool programs to be bundled into updater')
# TODO(sjg@chromium.org: Consider making this accumulate rather than using
# the ':' separator.
parser.add_argument(
'--tool_base', type=str, default='',
help='Default source locations for tools programs (delimited by colon)')
parser.add_argument('-q', '--quiet', action='store_true',
help='Avoid output except for warnings/errors')
return parser.parse_args(argv)
@staticmethod
def _EnsureCommand(cmd, package):
"""Ensure that a command is available, raising an exception if not.
Args:
cmd: Command to check (just the name, not the full path).
package: Name of package to install to obtain this tool.
"""
result = cros_build_lib.RunCommand('type %s' % cmd, shell=True, quiet=True,
error_code_ok=True)
if result.returncode:
raise PackError("You need '%s' (package '%s')" % (cmd, package))
@staticmethod
def _FindTool(tool_base, tool):
"""Find a tool in the tool_base path list, raising an exception if missing.
Args:
tool_base: List of directories to check.
tool: Name of tool to find (just the name, not the full path).
"""
for path in tool_base:
fname = os.path.join(path, tool)
if os.path.exists(fname):
return os.path.realpath(fname)
raise PackError("Cannot find tool program '%s' to bundle" % tool)
def _CreateTmpDir(self):
"""Create a temporary directory, and remember it for later removal.
Returns:
Path name of temporary directory.
"""
fname = tempfile.mkdtemp('.pack_firmware-%d' % os.getpid())
self._tmp_dirs.append(fname)
return fname
def _RemoveTmpdirs(self):
"""Remove all the temporary directories."""
for fname in self._tmp_dirs:
shutil.rmtree(fname)
self._tmp_dirs = []
def _AddFlashromVersion(self, tool_base):
"""Add flashrom version info to the collection of version information.
Args:
tool_base: List of directories to check.
"""
flashrom = self._FindTool(tool_base, 'flashrom')
# Look for a string ending in UTC.
with open(flashrom, 'rb') as fd:
data = fd.read()
m = re.search(r'([0-9.]+ +: +[a-z0-9]+ +: +.+UTC)', data)
if m:
version = m.group(1)
else:
# As a fallback, look for a version without git hash / date.
m = re.search(r'([0-9.]+ +: +: +)', data)
if not m:
raise PackError('Could not find flashrom version number')
# Don't show a partial version, it is not very useful and the
# pack_firmware.sh script (which predates this script) did not show it.
version = ''
# crbug.com/695904: Can we use a SHA2-based algorithm?
digest = md5.new()
digest.update(data)
result = cros_build_lib.RunCommand(['file', '-b', flashrom], quiet=True)
print('\nflashrom(8): %s *%s\n %s\n %s' %
(digest.hexdigest(), flashrom, result.output.strip(), version),
file=self._versions)
def _AddVersionInfo(self, name, fname, version):
"""Add version info for a single file.
Calculates the MD5 hash of the file and adds this and other file details
into the collection of version information.
Args:
name: User-readable name of the file (e.g. 'BIOS').
fname: Filename to read.
version: Version string (e.g. 'Google_Reef.9042.40.0').
"""
if fname:
with open(fname, 'rb') as fd:
digest = md5.new()
digest.update(fd.read())
# Modify the filename to replace any use of our base directory with a
# constant string, so we produce the same output on each run. Also drop
# the build directory since it is not useful to the user.
short_fname = fname
if self._basedir:
short_fname = short_fname.replace(
self._basedir,
os.path.join(os.path.dirname(self._basedir), 'tmp'))
short_fname = re.sub(r'/build/.*/work/', '', short_fname)
print('%s image:%s%s *%s' % (name, ' ' * max(3, 7 - len(name)),
digest.hexdigest(), short_fname),
file=self._versions)
if version:
print('%s version:%s%s' % (name, ' ' * max(1, 5 - len(name)), version),
file=self._versions)
def _ExtractFrid(self, image_file, section_name='RO_FRID'):
"""Extracts the firmware ID from an image file.
Args:
image_file: File to process.
section_name: Name of the section of image_file which contains the
firmware ID.
Returns:
Firmware ID as a string, if found, else ''
"""
fname = os.path.join(self._tmpdir, section_name)
# Remove any file that might be in the way (if not testing).
if not self._testing and os.path.exists(fname):
os.remove(fname)
cros_build_lib.RunCommand(['dump_fmap', '-x', image_file], quiet=True,
cwd=self._tmpdir, error_code_ok=True)
if os.path.exists(fname):
with open(fname) as fd:
return fd.read().replace('\x00', '').strip()
return ''
def _BaseDirPath(self, basename):
"""Build a filename in the temporary base directory.
Args:
basename: Leafname (with no directory) of file to build.
Returns:
New filename within the self._basedir directory.
"""
return os.path.join(self._basedir, basename)
def _GetPreambleFlags(self, fname):
"""Get the preamble flags from an image.
Args:
fname: Image to check (relative or absolute path).
Returns:
Preamble flags as an integer. See VB2_FIRMWARE_PREAMBLE_... for available
flags; the most common one is VB2_FIRMWARE_PREAMBLE_USE_RO_NORMAL.
"""
cros_build_lib.RunCommand(['dump_fmap', '-x', fname],
quiet=True, cwd=self._tmpdir)
cros_build_lib.RunCommand(['gbb_utility', '--rootkey=rootkey.bin', 'GBB'],
quiet=True, cwd=self._tmpdir)
result = cros_build_lib.RunCommand(
['vbutil_firmware', '--verify', 'VBLOCK_A', '--signpubkey',
'rootkey.bin', '--fv', 'FW_MAIN_A'], quiet=True, cwd=self._tmpdir)
lines = ([line for line in result.output.splitlines()
if 'Preamble flags' in line])
if len(lines) != 1:
raise PackError("vbutil_firmware returned %d 'Preamble flags' lines",
len(lines))
return int(lines[0].split()[-1])
def _SetPreambleFlags(self, infile, outfile, preamble_flags):
"""Set the preamble flags for an image.
Args:
infile: Input image file (relative or absolute path).
outfile: Output image file (relative or absolute path).
preamble_flags: Preamble flags as an integer.
"""
keydir = '/usr/share/vboot/devkeys'
cros_build_lib.RunCommand(
['resign_firmwarefd.sh', infile, outfile,
os.path.join(keydir, 'firmware_data_key.vbprivk'),
os.path.join(keydir, 'firmware.keyblock'),
os.path.join(keydir, 'dev_firmware_data_key.vbprivk'),
os.path.join(keydir, 'dev_firmware.keyblock'),
os.path.join(keydir, 'kernel_subkey.vbpubk'),
'0', str(preamble_flags)],
quiet=True, cwd=self._tmpdir)
@staticmethod
def _CopyTimestamp(reference_fname, fname):
"""Copy the timestamp from a reference file to another file.
reference_fname: Reference file for timestamp.
fname: File to copy timestamp to.
"""
mtime = os.stat(reference_fname).st_mtime
os.utime(fname, (mtime, mtime))
def _CreateRwFirmware(self, ro_fname, rw_fname):
"""Build a RW firmware file from an input RO file.
This works by clearing bit 0 of the preamble flags, this indicating this is
RW firmware.
Args:
ro_fname: Filename of RO firmware (relative or absolute path).
rw_fname: Filename of RW firmware (relative or absolute path).
"""
preamble_flags = self._GetPreambleFlags(ro_fname)
if not preamble_flags & 1:
raise PackError("Firmware image '%s' is NOT RO_NORMAL firmware" %
ro_fname)
self._SetPreambleFlags(ro_fname, rw_fname, preamble_flags ^ 1)
self._CopyTimestamp(ro_fname, rw_fname)
if not self._args.quiet:
print("RW firmware image '%s' created" % rw_fname)
def _CheckRwFirmware(self, fname):
"""Check that the firmware file is RW firmware.
Args:
fname: Filename of firmware image to check.
Raises:
PackError or RunCommandError if the flags could not be read or indicate
that the firmware file is not RW firmware.
"""
if self._GetPreambleFlags(fname) & 1:
raise PackError("Firmware image '%s' is NOT RW-firmware" % fname)
def _GetFMAP(self, fname):
"""Get the FMAP (flash map) from a firmware image.
Args:
fname: Filename of firmware image.
Returns:
A dict comprising:
key: Section name.
value: Section() named tuple containing offset and size.
"""
result = cros_build_lib.RunCommand(['dump_fmap', '-p', fname],
quiet=True, cwd=self._tmpdir)
sections = {}
for line in result.output.splitlines():
name, offset, size = line.split()
sections[name] = Section(int(offset), int(size))
return sections
def _CloneFirmwareSection(self, dst, src, section):
"""Clone a section in one file from another.
Args:
dst: Destination file (relative or absolute path).
src: Source file (relative or absolute path).
section: Section to clone.
"""
src_section = self._GetFMAP(src)[section]
dst_section = self._GetFMAP(dst)[section]
if not src_section.size:
raise PackError("Firmware section '%s' is invalid" % section)
if src_section.size != dst_section.size:
raise PackError("Firmware section '%s' size is different, cannot clone" %
section)
if src_section.offset != dst_section.offset:
raise PackError("Firmware section '%s' is not in same location, cannot "
"clone" % section)
merge_file.merge_file(dst, src, dst_section.offset, src_section.offset,
src_section.size)
def _MergeRwFirmware(self, ro_fname, rw_fname):
"""Merge RW sections from RW firmware to RO firmware.
Args:
ro_fname: RO firmware image file (relative or absolute path).
rw_fname: RW firmware image file (relative or absolute path).
"""
self._CloneFirmwareSection(ro_fname, rw_fname, 'RW_SECTION_A')
self._CloneFirmwareSection(ro_fname, rw_fname, 'RW_SECTION_B')
self._CopyTimestamp(rw_fname, ro_fname)
def _ExtractEcRwUsingFMAP(self, fname, ecrw_fname):
"""Use the FMAP to extract the EC_MAIN_A section containing an EC binary.
Args:
fname: Filename of firmware image (relative or absolute path).
ecrw_fname: Filename to put EC binary into (relative or absolute path).
"""
cros_build_lib.RunCommand(['dump_fmap', '-x', fname, 'EC_MAIN_A'],
quiet=True, cwd=self._tmpdir)
ec_main_a = os.path.join(self._tmpdir, 'EC_MAIN_A')
with open(ec_main_a) as fd:
count, offset, size = struct.unpack('<III', fd.read(12))
if count != 1 or offset != 12:
raise PackError('Unexpected EC_MAIN_A (%d, %d). Cannot merge EC RW' %
count, offset)
# To make sure files to be merged are both prepared, merge_file.py will
# only accept existing files, so we have to create ecrw now.
osutils.Touch(ecrw_fname)
merge_file.merge_file(ecrw_fname, ec_main_a, 0, offset, size)
def _ExtractEcRwUsingCBFS(self, fname, cbfs_name, ecrw_fname):
"""Extract an EC binary from a CBFS image.
Args:
fname: Filename of firmware image (relative or absolute path).
cbfs_name: Name of file in CBFS which contains the EC binary.
ecrw_fname: Filename to put EC binary into (relative or absolute path).
"""
cros_build_lib.RunCommand(
['cbfstool', fname, 'extract', '-n', cbfs_name, '-f', ecrw_fname, '-r',
'FW_MAIN_A'], quiet=True, cwd=self._tmpdir)
def _ExtractEcRw(self, fname, cbfs_name, ecrw_fname):
"""Obtain the RW EC binary.
Args:
fname: Filename of firmware image (relative or absolute path).
cbfs_name: Name of file in CBFS which contains the EC binary, if the
image does not have an EC_MAIN_A section.
ecrw_fname: Filename to put EC binary into (relative or absolute path).
Raises:
PackError or RunCommandError if an error occurs.
"""
if 'EC_MAIN_A' in self._GetFMAP(fname):
self._ExtractEcRwUsingFMAP(fname, ecrw_fname)
else:
self._ExtractEcRwUsingCBFS(fname, cbfs_name, ecrw_fname)
def _MergeRwEcFirmware(self, ec_fname, rw_fname, cbfs_name):
"""Merge EC firmware from an image into the given file.
Args:
ec_fname: Filename to merge RW EC binary into (relative or absolute path).
rw_fname: Filename of firmware image (relative or absolute path).
cbfs_name: Name of file in CBFS which contains the EC binary, if the
image does not have an EC_MAIN_A section.
"""
ecrw_fname = os.path.join(self._tmpdir, 'ecrw')
self._ExtractEcRw(rw_fname, cbfs_name, ecrw_fname)
section = self._GetFMAP(ec_fname)['EC_RW']
ecrw_size = os.stat(ecrw_fname).st_size
if section.size < ecrw_size:
raise PackError('New RW payload size %#x is larger than preserved FMAP '
'section %#x, cannot merge' % (section.size, ecrw_size))
merge_file.merge_file(ec_fname, ecrw_fname, section.offset)
self._CopyTimestamp(rw_fname, ec_fname)
def _CopyFirmwareFiles(self, bios_image, bios_rw_image, ec_image, pd_image,
create_bios_rw_image, image_main, image_main_rw,
image_ec, image_pd):
"""Process firmware files and copy them into the working directory.
Args:
bios_image: Input filename of main BIOS (main) image.
bios_rw_image: Input filename of RW BIOS image, or None if none.
ec_image: Input filename of EC (Embedded Controller) image.
pd_image: Input filename of PD (Power Delivery) image.
create_bios_rw_image: True to create a RW BIOS image from the main one
if not provided.
image_main: Output filename to use for main image.
image_main_rw: Output filename to use for main RW image.
image_ec: Output filename to use for EC image.
image_pd: Output filename to use for PDimage.
Returns:
Dict containing information on the output image files:
key: Name, one of BIOS, BIOS (RW), EC, EC (RW), PD, PD (RW).
value: ImageFile, containing filename and version.
"""
image_files = {}
merge_bios_rw_image = self._args.merge_bios_rw_image
if bios_image:
bios_version = self._ExtractFrid(bios_image)
bios_rw_version = bios_version
shutil.copy2(bios_image, image_main)
image_files['BIOS'] = ImageFile(bios_image, bios_version)
else:
merge_bios_rw_image = False
if not bios_rw_image and create_bios_rw_image:
bios_rw_image = image_main_rw
self._CreateRwFirmware(bios_image, bios_rw_image)
merge_bios_rw_image = False
if bios_rw_image:
self._CheckRwFirmware(bios_rw_image)
bios_rw_version = self._ExtractFrid(bios_rw_image)
if merge_bios_rw_image:
self._MergeRwFirmware(image_main, bios_rw_image)
elif bios_rw_image != image_main_rw:
shutil.copy2(bios_rw_image, image_main_rw)
image_files['BIOS (RW)'] = ImageFile(bios_rw_image, bios_rw_version)
else:
merge_bios_rw_image = False
if ec_image:
ec_version = self._ExtractFrid(ec_image)
image_files['EC'] = ImageFile(ec_image, ec_version)
shutil.copy2(ec_image, image_ec)
if merge_bios_rw_image:
self._MergeRwEcFirmware(image_ec, image_main, 'ecrw')
ec_rw_version = self._ExtractFrid(image_ec, 'RW_FWID')
image_files['EC (RW)'] = ImageFile(None, ec_rw_version)
if pd_image:
pd_version = self._ExtractFrid(pd_image)
image_files['PD'] = ImageFile(pd_image, pd_version)
shutil.copy2(pd_image, image_pd)
if merge_bios_rw_image:
self._MergeRwEcFirmware(image_pd, image_main, 'pdrw')
pd_rw_version = self._ExtractFrid(image_pd, 'RW_FWID')
image_files['PD (RW)'] = ImageFile(None, pd_rw_version)
return image_files
def _WriteVersions(self, image_files):
"""Write version information for all image files into the version file.
image_files: Dict with:
key: Image type (e.g. 'BIOS').
value: ImageFile object containing filename and version.
"""
print(file=self._versions) # Blank line.
for name in sorted(image_files.keys()):
image_file = image_files[name]
self._AddVersionInfo(name, image_file.filename, image_file.version)
def _UntarFile(self, pathname, dst_dirname, suffix=''):
"""Unpack the only item in a tar file.
Read a file from a tar file. It must contain just a single member and its
filename may not include a path.
Args:
pathname: Pathname of tar file to unpack.
dst_dirname: Destination directory to place the unpacked file.
suffix: String to append to output filename.
Returns:
Pathname of unpacked file.
"""
with tarfile.open(pathname) as tar:
members = tar.getmembers()
if len(members) != 1:
raise PackError("Expected 1 member in file '%s' but found %d" %
(pathname, len(members)))
if '/' in members[0].name:
raise PackError("Tar file '%s' member '%s' should be a simple name" %
(pathname, members[0].name))
tar.extractall(self._tmpdir, members)
fname = os.path.join(dst_dirname, members[0].name + suffix)
os.rename(os.path.join(self._tmpdir, members[0].name), fname)
return fname
@staticmethod
def _CopyFile(src, dst, mode=CHMOD_ALL_READ):
"""Copy a file (to another file or into a directory) and set its mode.
Args:
src: Source filename (relative or absolute path).
dst: Destination filename or directory (relative or absolute path).
mode: File mode to OR with the existing mode.
Returns:
Full pathname of the new file.
"""
if os.path.isdir(dst):
dst = os.path.join(dst, os.path.basename(src))
shutil.copy2(src, dst)
os.chmod(dst, os.stat(dst).st_mode | mode)
return dst
def _CopyBaseFiles(self, tool_base, tools, script):
"""Copy base files that every firmware update needs.
Args:
tool_base: List of directories to check.
tools: List of tools to copy.
script: Update script being used (so that we can avoid copying the
other update scripts over).
"""
self._CopyFile(self._shflags_file, self._basedir)
for tool in tools:
tool_fname = self._FindTool(tool_base, tool)
# Most tools are dynamically linked, but if there is a statically
# linked version (denoted by a '_s' suffix) use that in preference.
# This helps to reduce run-time dependencies for firmware update,
# which is a critical process.
if os.path.exists(tool_fname + '_s'):
tool_fname += '_s'
self._CopyFile(tool_fname, self._BaseDirPath(tool), CHMOD_ALL_EXEC)
for fname in glob.glob(os.path.join(self._pack_dist, '*')):
if (self._args.remove_inactive_updaters and 'updater' in fname and
not script in fname):
continue
newfile = self._CopyFile(fname, self._basedir, CHMOD_ALL_EXEC)
if self._force_dash and fname.endswith('.sh'):
with open(newfile) as fd:
lines = fd.read().splitlines()
lines = ['#!/bin/dash'] + lines[1:]
with open(newfile, 'w') as fd:
fd.write('\n'.join(lines))
def _CopyExtraFiles(self, extras):
"""Copy extra files into the base directory.
extras: List of extra files / directories to copy. If the item is a
directory, then the files in that directory are copied into the
base directory. If the item starts with "bcs://" then this indicates a
tar file: the prefix is stripped, the file is picked up from the
image directory (--imagedir) and unpacked to obtain the (single-file)
contents.
"""
for extra in extras:
if extra.startswith('bcs://'):
src = os.path.join(self._args.imagedir, extra.replace('bcs://', ''))
fname = self._UntarFile(src, self._basedir)
print("Extra BCS file: %s: %s" % (extra, fname), file=self._versions)
elif os.path.isdir(extra):
fnames = glob.glob(os.path.join(extra, '*'))
if not fnames:
raise PackError("cannot copy extra files from folder '%s'" %
extra)
for fname in fnames:
if not os.path.isdir(fname):
self._CopyFile(fname, self._basedir)
print('Extra files from folder: %s' % extra,
file=self._versions)
else:
self._CopyFile(extra, self._basedir)
print('Extra file: %s' % extra, file=self._versions)
@staticmethod
def _GetReplaceDict(image_files, stable_main_version, stable_ec_version,
stable_pd_version):
"""Build a dictionary of string replacements for use with the update script.
Args:
image_files: Dict with:
key: Image type (e.g. 'BIOS').
value: ImageFile object containing filename and version.
stable_main_version: Version name of stable main firmware, or None.
stable_ec_version: Version name of stable EC firmware, or None.
stable_pd_version: Version name of stable PD firmware, or None.
Returns:
Dict with:
key: String to replace.
value: Value to replace with.
"""
empty = ImageFile(None, '')
bios_rw_version = image_files.get('BIOS (RW)', empty).version
if not bios_rw_version:
bios_rw_version = image_files['BIOS'].version
return {
'REPLACE_RO_FWID': image_files['BIOS'].version,
'REPLACE_FWID': bios_rw_version,
'REPLACE_ECID': image_files.get('EC', empty).version,
'REPLACE_PDID': image_files.get('PD', empty).version,
# Set platform to first field of firmware version
# (ex: Google_Link.1234 -> Google_Link).
'REPLACE_PLATFORM': image_files['BIOS'].version.split('.')[0],
'REPLACE_STABLE_FWID': stable_main_version,
'REPLACE_STABLE_ECID': stable_ec_version,
'REPLACE_STABLE_PDID': stable_pd_version,
}
@staticmethod
def _CreateFileFromTemplate(infile, outfile, replace_dict, unibuild):
"""Create a new file based on a template and some string replacements.
Args:
infile: Input file (the template).
outfile: Output file.
replace_dict: Dict with:
key: String to replace.
value: Value to replace with.
unibuild: True if this is for a unified build, in which case the UNIBUILD
variable will be set to 'yes' in outfile.
"""
with open(infile) as fd:
data = fd.read()
rep = dict((re.escape(k), v) for k, v in replace_dict.iteritems())
pattern = re.compile("|".join(rep.keys()))
data = pattern.sub(lambda m: rep[re.escape(m.group(0))], data)
if unibuild:
data = re.sub('UNIBUILD=', 'UNIBUILD="yes"', data)
with open(outfile, 'w') as fd:
fd.write(data)
os.chmod(outfile, os.stat(outfile).st_mode | 0555)
def _WriteUpdateScript(self, script, replace_dict, unibuild):
"""Create and write the update script which will run on the device.
This generates the beginnings of the output file (shellball) based on the
template. Normally all the required settings for firmware update are at the
top of the script. With unified builds only TARGET_SCRIPT is used: the
rest are unset and the model-specific setvars.sh files contain the rest
of the settings.
Args:
script: Filename of update script being used (e.g. 'updater4.sh').
replace_dict: Modified by this function to add the script. Dict with:
key: String to replace.
value: Value to replace with.
unibuild: True if this is for a unified build, in which case the UNIBUILD
variable will be set to 'yes' in the output and the other
replacements will be marked unused.
"""
if unibuild:
for key in replace_dict:
replace_dict[key] = '<unused with unified builds>'
replace_dict['REPLACE_SCRIPT'] = script
self._CreateFileFromTemplate(self._stub_file, self._args.output,
replace_dict, unibuild)
def _WriteVersionFile(self):
"""Write out the VERSION file with our collected version information."""
print(file=self._versions)
with open(self._BaseDirPath('VERSION'), 'w') as fd:
fd.write(self._versions.getvalue())
def _BuildShellball(self):
"""Build a shell-ball containing the firmware update.
Add our files to the shell-ball, and display all version information.
"""
cros_build_lib.RunCommand(
['sh', self._args.output, '--sb_repack', self._basedir],
quiet=self._args.quiet, mute_output=not self._args.quiet)
if not self._args.quiet:
for fname in glob.glob(self._BaseDirPath('VERSION*')):
with open(fname) as fd:
print(fd.read())
def _ProcessModel(self, model, bios_image, bios_rw_image, ec_image, pd_image,
create_bios_rw_image, tools, tool_base, script, extras):
"""Set up the firmware update for one model.
Args:
model: name of model to set up for (e.g. 'reef'). If this is not a
unified build then this should be ''.
bios_image: Input filename of main BIOS (main) image.
bios_rw_image: Input filename of RW BIOS image, or None if none.
ec_image: Input filename of EC (Embedded Controller) image.
pd_image: Input filename of PD (Power Delivery) image.
create_bios_rw_image: True to create a RW BIOS image from the main one
if not provided.
tools: List of tools to include in the update (e.g. ['mosys', 'ectool'].
tool_base: List of directories to look in for tools.
script: Update script to use (e.g. 'updater4.sh').
extras: List of extra files/directories to include.
Returns:
Dict containing information on the output image files:
key: Name, one of "BIOS", "BIOS (RW)", "EC", "EC (RW)", "PD", "PD (RW)".
value: ImageFile, containing filename and version.
"""
main_script = os.path.join(self._pack_dist, script)
if not os.path.exists(main_script):
raise PackError("Cannot find required file '%s'" % main_script)
for tool in tools:
self._FindTool(tool_base, tool)
if not bios_image and not ec_image and not pd_image:
raise PackError('Must assign at least one of BIOS or EC or PD image')
models_dir = MODELS_DIR if model else ''
image_files = self._CopyFirmwareFiles(
bios_image=bios_image,
bios_rw_image=bios_rw_image,
ec_image=ec_image,
pd_image=pd_image,
create_bios_rw_image=create_bios_rw_image,
image_main=os.path.join(self._basedir, models_dir, model, IMAGE_MAIN),
image_main_rw=os.path.join(self._basedir, models_dir, model,
IMAGE_MAIN_RW),
image_ec=os.path.join(self._basedir, models_dir, model, IMAGE_EC),
image_pd=os.path.join(self._basedir, models_dir, model, IMAGE_PD))
self._WriteVersions(image_files)
self._CopyBaseFiles(tool_base, tools, script)
if extras:
self._CopyExtraFiles(extras)
return image_files
def _SetUpConfig(self, fname):
"""Set up the master configuration ready for use.
Args:
fname: Filename of master configuration.
"""
self._config = libfdt.Fdt(open(fname).read())
def _GetString(self, node, prop_name):
"""Get a string from a node in the master configuration.
Args:
node: Integer offset of node to read.
prop_name: Property name within that node to read.
Returns:
Value of property as a string, or ''.
"""
prop = self._config.getprop(node, prop_name, libfdt.QUIET_NOTFOUND)
if type(prop) == int or prop[-1]:
return ''
return str(prop[:-1])
def _GetStringList(self, node, prop_name):
"""Get a string list from a node in the master configuration.
Args:
node: Integer offset of node to read.
prop_name: Property name within that node to read.
Returns:
Value of property as a list of strings, which may be empty.
"""
prop = self._config.getprop(node, prop_name, libfdt.QUIET_NOTFOUND)
if type(prop) == int or prop[-1]:
return []
return str(prop[:-1]).split('\0')
def _GetBool(self, node, prop_name):
"""Get a boolean value from a node in the master configuration.
Args:
node: Integer offset of node to read.
prop_name: Property name within that node to read.
Returns:
True if the property is True (i.e. present), else False.
"""
prop = self._config.getprop(node, prop_name, libfdt.QUIET_NOTFOUND)
if type(prop) == int:
return False
return True
def _ExtractFile(
self, model, fname_template, node, prop_name, dirname, suffix=''):
"""Obtain a file based on provided information.
This obtains the file in one of two ways:
- if we are building local firmware, it creates a filename using the
provided template, with MODEL replaced with the current model. This
is used to build a firmware update from the firmware ebuilds. The
file is only used if there is a valid property value, since that is
what indicates that the file is needed in the update.
- otherwise it finds a tar file, unpacks it and returns the filename of
the resulting unpacked file. There is only one file in the archive.
This is used to build a firmware update from the configured firmware
versions, downloaded from BCS.
Args:
model: Model name being processed (e.g. 'reef').
fname_template: Filename template to use in local model.
node: Integer offset of node to read.
prop_name: Property name in that node which contains the image path.
dirname: Output directory where the unpacked file should be placed.
suffix: String to append to output filename.
Returns:
Filename of the resulting image file.
"""
uri = self._GetString(node, prop_name)
if not uri:
return None
if self._args.local:
if not fname_template:
return None
return fname_template.replace('MODEL', model)
fname = uri.replace('bcs://', '')
return self._UntarFile(
os.path.join(self._args.imagedir, fname), dirname, suffix)
def Start(self, argv, remove_tmpdirs=True):
"""Handle the creation of a firmware shell-ball.
argv: List of arguments (excluding the program name/argv[0]).
Raises:
PackError if any error occurs.
"""
args = self._args = self.ParseArgs(argv)
self._EnsureCommand('shar', 'sharutils')
if not os.path.exists(self._stub_file):
raise PackError("Cannot find required file '%s'" % self._stub_file)
tool_base = args.tool_base.split(':')
try:
if not args.output:
raise PackError('Missing output file')
self._basedir = self._CreateTmpDir()
self._tmpdir = self._CreateTmpDir()
self._AddFlashromVersion(tool_base)
if args.models:
# Most of the arguments are meaningless with unified builds since we
# get the information from the master configuration. Add checks here
# to avoid confusion. We could possibly do some of this using
# mutual exclusivity in the argparser, but I'm not sure it is better.
image_args = [args.bios_image, args.ec_image, args.pd_image]
if any(image_args) and not args.local:
raise PackError('Cannot use -b/-p/-e with -m')
elif args.local and not all(image_args):
raise PackError('Must provide -b, -e, -p with -l')
if args.create_bios_rw_image:
raise PackError('Cannot use --create_bios_rw_image with -m')
if (args.stable_main_version or args.stable_ec_version or
args.stable_pd_version):
raise PackError('Cannot set stable versions with -m')
if not args.config:
raise PackError('Missing master configuration file (use -c)')
# This relies on an upstream pylibfdt which does not yet exist.
# See here:
# https://www.spinics.net/lists/devicetree-compiler/index.html#01166
# Series starts: [PATCH v9 0/5] Introduce Python bindings for libfdt
self._config = libfdt.Fdt(open(args.config).read())
os.mkdir(self._BaseDirPath(MODELS_DIR))
os.mkdir(os.path.join(self._tmpdir, MODELS_DIR))
for model in args.models:
node = self._config.path_offset('/chromeos/models/%s/firmware' %
model)
os.mkdir(self._BaseDirPath(os.path.join(MODELS_DIR, model)))
# Put all model files in a separate subdirectory.
dirname = os.path.join(self._tmpdir, MODELS_DIR, model)
if not os.path.exists(dirname):
os.mkdir(dirname)
bios_image = self._ExtractFile(model, args.bios_image, node,
'main-image', dirname)
bios_rw_image = self._ExtractFile(model, None, node, 'main-rw-image',
dirname)
ec_image = self._ExtractFile(model, args.ec_image, node, 'ec-image',
dirname)
pd_image = self._ExtractFile(model, args.pd_image, node, 'pd-image',
dirname)
extras = self._GetStringList(node, 'extra')
if extras:
extras = [os.path.expandvars(extra) for extra in extras]
create_bios_rw_image = self._GetBool(node, 'create-bios-rw-image')
if args.local:
create_bios_rw_image = False
tools = self._GetStringList(node, 'tools')
for tool in args.tools.split():
tools.append(tool)
image_files = self._ProcessModel(
bios_image=bios_image,
bios_rw_image=bios_rw_image,
ec_image=ec_image,
pd_image=pd_image,
create_bios_rw_image=create_bios_rw_image,
tools=tools,
tool_base=tool_base,
script=self._GetString(node, 'script'),
extras=extras,
model=model)
replace_dict = self._GetReplaceDict(
image_files,
self._GetString(node, 'stable-main-version'),
self._GetString(node, 'stable-ec-version'),
self._GetString(node, 'stable-pd-version'))
replace_dict['REPLACE_MODEL'] = model
fname = os.path.join(self._basedir, MODELS_DIR, model, 'setvars.sh')
self._CreateFileFromTemplate(self._setvars_template_file, fname,
replace_dict, True)
else:
image_files = self._ProcessModel(
model='',
bios_image=args.bios_image,
bios_rw_image=args.bios_rw_image,
ec_image=args.ec_image,
pd_image=args.pd_image,
create_bios_rw_image=args.create_bios_rw_image,
tools=args.tools.split(),
tool_base=tool_base,
script=args.script,
extras=args.extra.split(':') if args.extra else [])
replace_dict = self._GetReplaceDict(image_files, args.stable_main_version,
args.stable_ec_version,
args.stable_pd_version)
self._WriteUpdateScript(args.script, replace_dict,
unibuild=args.models is not None)
self._WriteVersionFile()
self._BuildShellball()
if not args.quiet:
print('Packed output image is: %s' % args.output)
finally:
if remove_tmpdirs:
self._RemoveTmpdirs()
# The style guide says that we cannot pass in sys.argv[0]. That makes testing
# a pain, so this is a full argv.
def main(argv):
# pylint: disable=W0603
global packer
packer = FirmwarePacker(argv[0])
packer.Start(argv[1:])
if __name__ == "__main__":
main(sys.argv)