blob: b25f1082c7a8b985b99d0689c9554a3f0e878224 [file] [log] [blame]
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 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/sfx?.sh as the self extraction (and installation) script
"""
from __future__ import print_function
from collections import namedtuple, OrderedDict
import glob
import hashlib
import os
import re
import shutil
import struct
import sys
import tarfile
import tempfile
from six.moves import StringIO
# pylint: disable=import-error
from cros_config_host import libcros_config_host
# pylint: enable=import-error
from chromite.lib import commandline
from chromite.lib import cros_build_lib
from chromite.lib import osutils
MAIN, MAIN_RW = 'BIOS', 'BIOS (RW)'
EC = 'EC'
PD = 'PD'
MODELS_DIR = 'models'
IMG_DIR = 'images'
Section = namedtuple('Section', ['offset', 'size'])
ImageFile = namedtuple('ImageFile',
['filename', 'build_target', 'firmware_ids'])
FirmwareIds = namedtuple('FirmwareIds', ['ro_id', 'rw_id'])
# Source locations for all firmware images.
FirmwareSource = namedtuple('FirmwareSource', ['bios', 'bios_rw', 'ec', 'pd'])
# image_files: Dict containing information on the output image files for this
# model:
# key: Image name (e.g. 'BIOS (RW)' or 'EC').
# value: ImageFile, containing filename and version.
# key_id: key ID to use to sign the image for this model
ModelDetails = namedtuple('ModelDetails', ['image_files', 'key_id'])
# File execution permissions. We could use state.S_... but that's confusing.
CHMOD_ALL_READ = 0o444
CHMOD_ALL_EXEC = 0o555
# For testing
packer = None
# Variables that we replace in pack/setvars_template.
REPLACE_VARS = [
'TARGET_RO_FWID',
'TARGET_FWID',
'TARGET_ECID',
'TARGET_PDID',
'TARGET_PLATFORM',
]
class PackError(Exception):
"""Exception returned by FirmwarePacker when something goes wrong"""
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.
_script_base: Base directory with useful files (src/platform/firmware).
_sfx_file: Path to the SFX program (pack/sfx?.sh).
_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.
"""
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._sfx_file = os.path.join(self._script_base, 'pack', 'sfx2.sh')
self._setvars_template_file = os.path.join(
self._script_base, 'pack', 'setvars_template')
self._basedir = None
self._tmpdir = None
self._tmp_dirs = []
self._versions = StringIO()
self._force_dash = False
@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 = commandline.ArgumentParser(description=__doc__)
parser.add_argument('-L', '--legacy', action='store_true',
help='Legacy mode for use with boards not using '
'unified builds')
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='path',
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-BUILD_TARGET.bin"')
parser.add_argument('-i', '--imagedir', type='path', default='.',
help='Default locations for source images')
parser.add_argument('-b', '--bios_image', type='path',
help='Path of input AP (BIOS) firmware image')
parser.add_argument('-w', '--bios_rw_image', type='path',
help='Path of input BIOS RW firmware image')
parser.add_argument('-e', '--ec_image', type='path',
help='Path of input Embedded Controller firmware image')
parser.add_argument('-p', '--pd_image', type='path',
help='Path of input Power Delivery firmware image')
parser.add_argument('-o', '--output', type='path',
help='Path of output filename')
parser.add_argument('-t', '--testing', action='store_true',
help='Testing mode for mocked unit tests.')
parser.add_argument('-q', '--quiet', action='store_true',
help='Avoid output except for warnings/errors')
opts = parser.parse_args(argv)
opts.Freeze()
return opts
@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.
"""
if not osutils.Which(cmd):
raise PackError("You need '%s' (package '%s')" % (cmd, package))
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 _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:
digest = hashlib.md5()
digest.update(osutils.ReadFile(fname, mode='rb'))
# 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
# any build and temp directies since they are 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'))
if self._tmpdir and short_fname.startswith(self._tmpdir):
short_fname = short_fname[len(self._tmpdir):]
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._args.testing and os.path.exists(fname):
os.remove(fname)
cros_build_lib.run(['futility', 'dump_fmap', '-x', image_file],
quiet=True, cwd=self._tmpdir, check=False)
if os.path.exists(fname):
return osutils.ReadFile(fname, mode='rb').replace(
b'\x00', b'').strip().decode('utf-8')
return ''
def _ExtractFirmwareIds(self, image_file, rw_image_file=None):
"""Extracts the RO/RW firmware IDs from image files.
Args:
image_file: File to process.
rw_image_file: RW image file, if separate.
Returns:
FirmwareIds object containing RO and RW firmware IDs.
If RW ID not found, RO ID is used in its place.
"""
ro_id = self._ExtractFrid(image_file)
rw_id = (self._ExtractFrid(rw_image_file)
if rw_image_file else self._ExtractFrid(image_file, 'RW_FWID'))
return FirmwareIds(ro_id, rw_id or ro_id)
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 _TmpDirPath(self, basename):
"""Build a filename in the temporary directory.
Args:
basename: Leafname (with no directory) of file to build.
Returns:
New filename within the self._tmpdir directory.
"""
return os.path.join(self._tmpdir, basename)
@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 _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.run(
['futility', 'dump_fmap', '-p', fname],
quiet=True, cwd=self._tmpdir, encoding='utf-8')
sections = {}
for line in result.output.splitlines():
name, offset, size = line.split()
sections[name] = Section(int(offset), int(size))
return sections
@staticmethod
def _MergeFile(large_path, small_path, large_offset, small_offset=0,
size=None):
"""Merges file in small_path to large_path in given offset.
Args:
large_path: A string for path of the file to merged to.
small_path: A string for path of the file to merge from.
large_offset: The offset to write in large_path.
small_offset: The offset to read in small_path.
size: Count of bytes to read/write. None to read whole small_path.
"""
with open(large_path, 'rb+') as output:
output.seek(large_offset)
with open(small_path, 'rb') as source:
source.seek(small_offset)
output.write(source.read() if size is None else source.read(size))
def _CloneFirmwareSection(self, dst, src, section, optional=False):
"""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.
optional: Ignore if the section does not exist.
"""
src_section = self._GetFMAP(src).get(section, None)
dst_section = self._GetFMAP(dst).get(section, None)
if src_section is None and dst_section is None and optional:
if not self._args.quiet:
print("Ignored nonexistent optional section '%s' in RW firmware image" %
section)
return
if src_section is None or dst_section is None:
raise PackError("Firmware section '%s' does not exist" % 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)
self._MergeFile(dst, src, dst_section.offset, src_section.offset,
src_section.size)
def _MergeRwMainFirmware(self, ro_fname, rw_fname):
"""Merge RW sections from main RW firmware to main RO firmware.
The RO image is cloned to a temporary directory before the RW image
is merged.
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._CloneFirmwareSection(ro_fname, rw_fname, 'RW_LEGACY', optional=True)
self._CloneFirmwareSection(ro_fname, rw_fname, 'RW_MISC', optional=True)
self._CopyTimestamp(rw_fname, ro_fname)
def _ExtractEcRwUsingFMAP(self, fname, fmap_area_name, ecrw_fname):
"""Use the FMAP to extract fmap_area_name section containing an EC binary.
Args:
fname: Filename of firmware image (relative or absolute path).
fmap_area_name: Name of the FMAP area which contains the EC binary.
ecrw_fname: Filename to put EC binary into (relative or absolute path).
"""
cros_build_lib.run(
['futility', 'dump_fmap', '-x', fname, fmap_area_name],
quiet=True, cwd=self._tmpdir)
ec_main_a = os.path.join(self._tmpdir, fmap_area_name)
with open(ec_main_a, mode='rb') as fd:
count, offset, size = struct.unpack('<III', fd.read(12))
if count != 1 or offset != 12:
raise PackError('Unexpected %s (%d, %d). Cannot merge EC RW' %
(fmap_area_name, count, offset))
# To make sure files to be merged are both prepared, _MergeFile will
# only accept existing files, so we have to create ecrw now.
osutils.Touch(ecrw_fname)
self._MergeFile(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.run(
['cbfstool', fname, 'extract', '-n', cbfs_name, '-f', ecrw_fname, '-r',
'FW_MAIN_A'], quiet=True, cwd=self._tmpdir)
def _ExtractEcRw(self, fname, fmap_area_name, cbfs_name, ecrw_fname):
"""Obtain the RW EC binary.
Args:
fname: Filename of firmware image (relative or absolute path).
fmap_area_name: Name of the section in FMAP which contains the EC
binary, will fall-back to CBFS if not present.
cbfs_name: Name of file in CBFS which contains the EC binary, if the
image does not have an fmap_area_name section.
ecrw_fname: Filename to put EC binary into (relative or absolute path).
Raises:
PackError or RunCommandError if an error occurs.
"""
if fmap_area_name in self._GetFMAP(fname):
self._ExtractEcRwUsingFMAP(fname, fmap_area_name, ecrw_fname)
else:
self._ExtractEcRwUsingCBFS(fname, cbfs_name, ecrw_fname)
def _MergeRwEcFirmware(self, ec_fname, rw_fname, fmap_area_name, cbfs_name):
"""Merge EC firmware from an image into a copy of 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).
fmap_area_name: Name of the section in FMAP which contains the EC
binary, will fall-back to CBFS if not present.
cbfs_name: Name of file in CBFS which contains the EC binary, if the
image does not have an fmap_area_name section.
"""
ecrw_fname = os.path.join(self._tmpdir, cbfs_name)
self._ExtractEcRw(rw_fname, fmap_area_name, 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' % (ecrw_size, section.size))
self._MergeFile(ec_fname, ecrw_fname, section.offset)
self._CopyTimestamp(rw_fname, ec_fname)
def _MergeRwFirmware(self, fw_source, merge_dir):
"""Merge all RW firmware into corresponding RO images.
RO images are cloned to a given directory before being modified.
Args:
fw_source: Locations of original images.
merge_dir: Directory in which to perform the merge.
Returns:
FirmwareSource with locations of merged firmware.
"""
if not fw_source.bios:
raise PackError('Need main (RO) firmware image to merge RW image.')
bios, bios_rw, ec, pd = [
fw and self._CopyFile(fw, merge_dir, preserve_path=True)
for fw in fw_source]
self._MergeRwMainFirmware(bios, bios_rw)
if ec:
self._MergeRwEcFirmware(ec, bios, 'EC_MAIN_A', 'ecrw')
if pd:
self._MergeRwEcFirmware(pd, bios, 'PD_MAIN_A', 'pdrw')
return FirmwareSource(bios, bios_rw, ec, pd)
@staticmethod
def _ExtractEcVersion(fw_id):
"""Extracts EC version from a firmware ID, swapping dots for dashes.
Returns empty string if fw_id is empty or malformed.
Letting V denote [0-9], the expected version regex is "V+.V+.V+"
Expected format is "<board>_vV.V.VVVV-<hash>"
For example, "reef_v1.1.5857-77f6ed7" becomes "1-1-5857"
Args:
fw_id: The firmware ID providing version information.
Returns:
The version number.
"""
matched_version = re.search(r'((\d+\.){2}\d+)(-\S+)', fw_id)
if not matched_version:
raise PackError('Malformed EC firmware ID: %s' % fw_id)
return matched_version.group(1).replace('.', '-')
@staticmethod
def _ExtractMainVersion(fw_id):
"""Extracts main version from a firmware ID.
Note this function also swaps periods for dashes, and removes
timestamps from local builds. Returns empty string if fw_id is empty
or malformed.
Letting V denote [0-9], the expected version regex is...
"V+.V+" for local builds
"V+.V+.V+" for official builds
Expected FW ID format is one of...
"Google_<board>.<version>.yyyy_mm_dd_tttt" for local builds
"Google_<board>.<version>" for official builds
For example...
"Google_Reef.9264.0.2017_02_09_1240" becomes "9264-0"
"Google_Reef.9264.0.1" becomes "9264-0-1"
Args:
fw_id: The firmware ID providing version information.
Returns:
The version number.
"""
matched_version = re.search(r'(\d+\.\d+)(\.\d+$|\.\d{4}_)', fw_id)
if not matched_version:
raise PackError('Malformed coreboot firmware ID: %s' % fw_id)
version = matched_version.group(1)
if '_' not in matched_version.group(2):
version += matched_version.group(2)
return version.replace('.', '-')
def _FirmwareImageOutput(self, tag, build_target=None, fw_ids=None,
extract_version=None, target_dir=None):
"""Creates output file name for some firmware image.
Filename will have the form...
"<target_dir>/<tag>.<ro_version>.<rw_version>.bin",
If versions or model are not provided, returns empty string.
If legacy or testing is set, returns a generic name of the form
<target_dir>/<tag>.bin.
Args:
tag: Type of image (e.g, "bios").
build_target: The build target for this image.
fw_ids: The firmware IDs for this image.
extract_version: Function extracting image version from a firmware ID.
target_dir: Target directory to be prepended to filename.
Returns:
Filename of the output image.
"""
use_generic = self._args.legacy or self._args.testing
has_required_fields = fw_ids and all([build_target, fw_ids.ro_id,
fw_ids.rw_id, extract_version])
if not use_generic and not has_required_fields:
return ''
fname = ('%s.bin' % tag if use_generic else
'%s-%s.ro-%s.rw-%s.bin' % (tag, build_target,
extract_version(fw_ids.ro_id),
extract_version(fw_ids.rw_id)))
return os.path.join(target_dir, fname) if target_dir else fname
def _PdImageOutput(self, image_file=None, target_dir=None):
"""Creates output file name for given PD image.
Args:
image_file: The input firmware image (ImageFile object).
target_dir: Target directory to be prepended to filename.
Returns:
Filename of the output image.
"""
return self._FirmwareImageOutput(
tag='pd',
build_target=image_file.build_target,
fw_ids=image_file.firmware_ids,
extract_version=self._ExtractEcVersion,
target_dir=target_dir)
def _EcImageOutput(self, image_file=None, target_dir=None):
"""Creates output file name for given EC image.
Args:
image_file: The input firmware image (ImageFile object).
target_dir: Target directory to be prepended to filename.
Returns:
Filename of the output image.
"""
return self._FirmwareImageOutput(
tag='ec',
build_target=image_file.build_target,
fw_ids=image_file.firmware_ids,
extract_version=self._ExtractEcVersion,
target_dir=target_dir)
def _MainImageOutput(self, image_file=None, target_dir=None):
"""Creates output file name for given main image.
Args:
image_file: The input firmware image (ImageFile object).
target_dir: Target directory to be prepended to filename.
Returns:
Filename of the output image.
"""
return self._FirmwareImageOutput(
tag='bios',
build_target=image_file.build_target,
fw_ids=image_file.firmware_ids,
extract_version=self._ExtractMainVersion,
target_dir=target_dir)
def _CopyFirmwareFiles(self, image_files, target_dir):
"""Process firmware files and copy them into the working directory.
Args:
image_files: Dict:
key: Type of image (e.g. 'BIOS')
value: Corresponding ImageFile object.
target_dir: Target directory for output images.
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.
"""
bios, ec, pd = [image_files.get(label, None) for label in [MAIN, EC, PD]]
if bios:
shutil.copy2(bios.filename, self._MainImageOutput(bios, target_dir))
if ec:
shutil.copy2(ec.filename, self._EcImageOutput(ec, target_dir))
if pd:
shutil.copy2(pd.filename, self._PdImageOutput(pd, target_dir))
def _WriteVersions(self, model, image_files):
"""Write version information for all image files into the version file.
Args:
model: Name of model these image files are for (or '' if none).
image_files: Dict with:
key: Image type (e.g. 'BIOS').
value: ImageFile object containing filename and version.
"""
print(file=self._versions) # Blank line.
if model:
print('Model: %s' % model, file=self._versions)
for name in sorted(image_files.keys()):
filename = image_files[name].filename
ro_id, rw_id = image_files[name].firmware_ids
self._AddVersionInfo(name, filename, ro_id)
# Only write the RW version if it differs from RO. Main has separate
# RO/RW inputs, so check to avoid writing RW version twice.
if ro_id != rw_id and name != MAIN:
self._AddVersionInfo(name + ' (RW)', None, rw_id)
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, preserve_path=False):
"""Copy a file (to another file or into a directory) and set its mode.
Fails if src/dst do not exist.
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.
preserve_path: Whether or not to preserve src's path inside dst.
Returns:
Full pathname of the new file.
"""
if os.path.isdir(dst):
dst = os.path.join(
dst, src.strip('/') if preserve_path else os.path.basename(src))
osutils.SafeMakedirs(dst if os.path.isdir(dst) else os.path.dirname(dst))
shutil.copy2(src, dst)
os.chmod(dst, os.stat(dst).st_mode | mode)
return dst
def _GetReplaceDict(self, image_files):
"""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.
Returns:
Dict with:
key: String to replace.
value: Value to replace with.
"""
empty = ImageFile('', '', FirmwareIds('', ''))
bios, ec, pd = [image_files.get(label, empty) for label in [MAIN, EC, PD]]
return {
'REPLACE_TARGET_IMAGE_RO_MAIN': self._MainImageOutput(bios, IMG_DIR),
'REPLACE_TARGET_IMAGE_EC': self._EcImageOutput(ec, IMG_DIR),
'REPLACE_TARGET_IMAGE_PD': self._PdImageOutput(pd, IMG_DIR),
}
@staticmethod
def _CreateFileFromTemplate(infile, outfile, replace_dict):
"""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.
"""
# The template file is going to be ASCII everywhere, so we don't need to
# jump through binary mode hoops here.
data = osutils.ReadFile(infile)
rep = dict((re.escape(k), v) for k, v in replace_dict.items())
pattern = re.compile('|'.join(rep.keys()))
data = pattern.sub(lambda m: rep[re.escape(m.group(0))], data)
osutils.WriteFile(outfile, data)
os.chmod(outfile, os.stat(outfile).st_mode | 0o555)
def _WriteVersionFile(self):
"""Write out the VERSION file with our collected version information."""
print(file=self._versions)
osutils.WriteFile(self._BaseDirPath('VERSION'), self._versions.getvalue())
def _BuildShellball(self):
"""Build a shell-ball containing the firmware update.
Create a new shell-ball by copying from SFX file, add our files to the
shell-ball, and display all version information.
"""
self._CopyFile(self._sfx_file, self._args.output, mode=CHMOD_ALL_EXEC)
cros_build_lib.run(
['sh', self._args.output, '--repack', self._basedir],
quiet=self._args.quiet, capture_output=self._args.quiet)
if not self._args.quiet:
for fname in glob.glob(self._BaseDirPath('VERSION*')):
print(osutils.ReadFile(fname))
def _AddImageFile(self, image_files, key, build_target, ro_image,
rw_image=None):
"""Adds an ImageFile to the given dict if possible.
Does nothing in ro_image not provided.
Args:
image_files: Dictionary in which to insert ImageFile.
key: Dictionary key for the new ImageFile.
build_target: The firmware build target.
ro_image: Path to the RO image.
rw_image: Path to the RW image, if any.
"""
if not ro_image:
return
image_files[key] = ImageFile(ro_image, build_target,
self._ExtractFirmwareIds(ro_image, rw_image))
def _ProcessFirmware(self, fw_source, target_dir, main_target=None,
ec_target=None, fw_name=None):
"""Prepares firmware to be copied into shellball, then copies it.
In particular, merges the RW firmware if it is provided.
Args:
fw_source: Locations of original images.
target_dir: Output location for firmware images.
main_target: The build target for main firmware, if available.
ec_target: The build target for ec firmware, if available.
fw_name: Name for this firmware.
Returns:
Dict:
key: Type of firmware (e.g. 'BIOS')
value: Corresponding ImageFile object.
"""
if not fw_source.bios and not fw_source.ec and not fw_source.pd:
raise PackError("Target '%s': Must assign at least one of BIOS or EC "
'or PD image' % fw_name)
if fw_source.bios_rw:
merge_dir = self._TmpDirPath('%s-merged' % (fw_name or 'images'))
# We use os.mkdir instead of osutils.SafeMakedirs because merge_dir
# should not exist. If it does, something is broken and we want to know.
# This prevents FirmwarePacker from silently reusing dirty images.
os.mkdir(merge_dir)
fw_source = self._MergeRwFirmware(fw_source, merge_dir)
image_files = {}
self._AddImageFile(image_files, MAIN, main_target, fw_source.bios,
fw_source.bios_rw)
self._AddImageFile(image_files, MAIN_RW, main_target, fw_source.bios_rw)
self._AddImageFile(image_files, EC, ec_target, fw_source.ec)
self._AddImageFile(image_files, PD, ec_target, fw_source.pd)
self._CopyFirmwareFiles(image_files, target_dir)
return image_files
def _ExtractFile(
self, build_target, fname_template, uri, 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:
build_target: The name of the build target uses, driving the file output.
fname_template: Filename template to use in local model.
uri: URI containing the file
dirname: Output directory where the unpacked file should be placed.
suffix: String to append to output filename.
Returns:
Filename of the resulting image file or None if the file doesn't exist.
"""
if self._args.local:
if not fname_template:
return None
# TODO(shapiroc): Remove MODEL after cros-firmware.eclass is updated
real_path = fname_template.replace('MODEL', build_target).replace(
'BUILD_TARGET', build_target)
if not os.path.exists(real_path):
real_path = None
return real_path
if not uri:
return None
fname = uri.replace('bcs://', '')
return self._UntarFile(
os.path.join(self._args.imagedir, fname), dirname, suffix)
def _ExtractFirmware(self, firmware, dirname):
"""Extract all firmware images to a temporary directory.
Args:
firmware: FirmwareInfo object, describing what firmware to extract.
dirname: Destination for extracted firmware.
Returns:
FirmwareSource object containing filenames of extracted images.
"""
bios = self._ExtractFile(firmware.bios_build_target, self._args.bios_image,
firmware.main_image_uri, dirname)
bios_rw = self._ExtractFile(firmware.bios_build_target, None,
firmware.main_rw_image_uri, dirname, 'rw')
ec = self._ExtractFile(firmware.ec_build_target, self._args.ec_image,
firmware.ec_image_uri, dirname)
pd = self._ExtractFile(firmware.ec_build_target, self._args.pd_image,
firmware.pd_image_uri, dirname)
return FirmwareSource(bios, bios_rw, ec, pd)
def _WriteFirmwareImages(self, firmware_info, devices_fw_target):
"""Extract and build all firmware images, then copy them to the shellball.
Args:
firmware_info: Dict:
key: Model name.
value: FirmwareInfo object for that model.
devices_fw_target: Dict:
key: Device name.
value: Firmware target name.
Returns:
Dict:
key: Target name.
value: Firmware files for that model (ImageFiles object).
"""
images = {}
for device, fw_target in devices_fw_target.items():
firmware = firmware_info[device]
if fw_target in images:
continue
unpack_dir = self._TmpDirPath(fw_target)
osutils.SafeMakedirs(unpack_dir)
fw_source = self._ExtractFirmware(firmware, unpack_dir)
if not fw_source.bios and not fw_source.ec:
continue
images[fw_target] = self._ProcessFirmware(
fw_source, self._BaseDirPath(IMG_DIR), firmware.bios_build_target,
firmware.ec_build_target, fw_target)
return images
def _GenerateSetVars(self, firmware, image_files):
"""Generate the setvars.sh script for a single model.
Args:
firmware: Firmware information to process (FirmwareInfo object)
image_files: Dict:
key: Image type (e.g. BIOS).
value: ImageFile object for that image.
"""
setvars_dirname = self._BaseDirPath(os.path.join(MODELS_DIR,
firmware.model))
osutils.SafeMakedirs(setvars_dirname)
setvars_dict = self._GetReplaceDict(image_files)
setvars_dict['REPLACE_MODEL'] = firmware.model
# For zero-touch whitelabel devices tell the firmware updater to get its
# signature ID from the VPD.
# TODO(sjg): Update this to say that it comes from mosys / cros_config.
setvars_dict['REPLACE_SIGNATURE_ID'] = firmware.sig_id
setvars_fname = os.path.join(setvars_dirname, 'setvars.sh')
self._CreateFileFromTemplate(self._setvars_template_file, setvars_fname,
setvars_dict)
def _WriteSignerInstructions(self, model_details):
"""Write the signer instructions file.
This file tells the signer the mapping between models and their images and
key IDs. The signer uses this to work out which images to sign, the key to
use to sign each image and the model name to use in the vblock filename.
Note that a change with the format can break the code which has already
been released. Update all the consumers first so that they can handle
old and new formats, then push the change here.
Args:
model_details: OrderedDict:
key: Model name (in the order that information is wanted in the
instruction file).
value: ModelDetails object for that model.
"""
with open(self._BaseDirPath('signer_config.csv'), 'w') as fd:
print('model_name,firmware_image,key_id,ec_image', file=fd)
for model, details in model_details.items():
if not details.key_id:
# Some models will not have a key. At present this is normally
# just the zero-touch whitelabel devices.
continue
image_fname = self._MainImageOutput(details.image_files[MAIN], IMG_DIR)
if EC in details.image_files:
ec_fname = self._EcImageOutput(details.image_files[EC], IMG_DIR)
else:
ec_fname = ''
print(','.join((model, image_fname, details.key_id, ec_fname)), file=fd)
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('zip', 'zip')
if not os.path.exists(self._sfx_file):
raise PackError("Cannot find required file '%s'" % self._sfx_file)
try:
if not args.output:
raise PackError('Missing output file')
self._basedir = self._CreateTmpDir()
self._tmpdir = self._CreateTmpDir()
model_details = OrderedDict()
if not args.legacy:
# 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')
if args.local and not any(image_args):
raise PackError('Must provide one of -b, -e, -p with -l')
if not args.config:
raise PackError('Missing master configuration file (use -c)')
conf = libcros_config_host.CrosConfig(args.config)
osutils.SafeMakedirs(self._BaseDirPath(MODELS_DIR))
osutils.SafeMakedirs(self._BaseDirPath(IMG_DIR))
devices_fw_target = conf.GetFirmwareConfigsByDevice()
# TODO(yshaul): delete this once the api migration is complete.
firmware_info = conf.GetFirmwareInfo()
# TODO: Ignore the passed-in model list and use the list from
# libcros_config_host always. For now the passed-in list is used for
# testing.
if args.models:
# tests depend on exact ordering
devices_fw_target = OrderedDict(
(model, devices_fw_target[model]) for model in args.models
)
images = self._WriteFirmwareImages(firmware_info, devices_fw_target)
if not images:
if not args.quiet:
print('No image created\n')
return
for device, fw_target in devices_fw_target.items():
fw = firmware_info[device]
if fw_target not in images:
# It is possible that some devices have images and others do not.
# Do sanity checks and continue past this device.
assert not args.local, (
'Every target must have an image if args.local is set.'
)
assert not any(
(fw.main_image_uri, fw.main_rw_image_uri,
fw.ec_image_uri, fw.pd_image_uri)
), ('Expected an image for %s if any image_uri is set.' % device)
continue
image_files = images[fw_target]
model_details[device] = ModelDetails(image_files, fw.key_id)
if not fw.have_image:
continue
self._WriteVersions(device, image_files)
self._GenerateSetVars(fw, image_files)
else:
image_files = self._ProcessFirmware(
FirmwareSource(args.bios_image, args.bios_rw_image, args.ec_image,
args.pd_image),
self._basedir)
self._WriteVersions('', image_files)
self._WriteVersionFile()
if model_details:
self._WriteSignerInstructions(model_details)
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)