Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit eef3944e authored by Doug Zongker's avatar Doug Zongker Committed by The Android Open Source Project
Browse files

AI 144270: am: CL 144269 Relocate the new (google-indepedent) tools for signing and

  building images & OTA packages out of vendor/google.
  No device code is touched by this change.
  Original author: dougz
  Merged from: //branches/cupcake/...

Automated import of CL 144270
parent 8269d354
Loading
Loading
Loading
Loading
+273 −0
Original line number Diff line number Diff line
# Copyright (C) 2008 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import getopt
import getpass
import os
import re
import shutil
import subprocess
import sys
import tempfile

# missing in Python 2.4 and before
if not hasattr(os, "SEEK_SET"):
  os.SEEK_SET = 0

class Options(object): pass
OPTIONS = Options()
OPTIONS.signapk_jar = "out/host/linux-x86/framework/signapk.jar"
OPTIONS.max_image_size = {}
OPTIONS.verbose = False
OPTIONS.tempfiles = []


class ExternalError(RuntimeError): pass


def Run(args, **kwargs):
  """Create and return a subprocess.Popen object, printing the command
  line on the terminal if -v was specified."""
  if OPTIONS.verbose:
    print "  running: ", " ".join(args)
  return subprocess.Popen(args, **kwargs)


def LoadBoardConfig(fn):
  """Parse a board_config.mk file looking for lines that specify the
  maximum size of various images, and parse them into the
  OPTIONS.max_image_size dict."""
  OPTIONS.max_image_size = {}
  for line in open(fn):
    line = line.strip()
    m = re.match(r"BOARD_(BOOT|RECOVERY|SYSTEM|USERDATA)IMAGE_MAX_SIZE"
                 r"\s*:=\s*(\d+)", line)
    if not m: continue

    OPTIONS.max_image_size[m.group(1).lower() + ".img"] = int(m.group(2))


def BuildAndAddBootableImage(sourcedir, targetname, output_zip):
  """Take a kernel, cmdline, and ramdisk directory from the input (in
  'sourcedir'), and turn them into a boot image.  Put the boot image
  into the output zip file under the name 'targetname'."""

  print "creating %s..." % (targetname,)

  img = BuildBootableImage(sourcedir)

  CheckSize(img, targetname)
  output_zip.writestr(targetname, img)

def BuildBootableImage(sourcedir):
  """Take a kernel, cmdline, and ramdisk directory from the input (in
  'sourcedir'), and turn them into a boot image.  Return the image data."""

  ramdisk_img = tempfile.NamedTemporaryFile()
  img = tempfile.NamedTemporaryFile()

  p1 = Run(["mkbootfs", os.path.join(sourcedir, "RAMDISK")],
           stdout=subprocess.PIPE)
  p2 = Run(["gzip", "-n"], stdin=p1.stdout, stdout=ramdisk_img.file.fileno())

  p2.wait()
  p1.wait()
  assert p1.returncode == 0, "mkbootfs of %s ramdisk failed" % (targetname,)
  assert p2.returncode == 0, "gzip of %s ramdisk failed" % (targetname,)

  cmdline = open(os.path.join(sourcedir, "cmdline")).read().rstrip("\n")
  p = Run(["mkbootimg",
           "--kernel", os.path.join(sourcedir, "kernel"),
           "--cmdline", cmdline,
           "--ramdisk", ramdisk_img.name,
           "--output", img.name],
          stdout=subprocess.PIPE)
  p.communicate()
  assert p.returncode == 0, "mkbootimg of %s image failed" % (targetname,)

  img.seek(os.SEEK_SET, 0)
  data = img.read()

  ramdisk_img.close()
  img.close()

  return data


def AddRecovery(output_zip):
  BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "RECOVERY"),
                           "recovery.img", output_zip)

def AddBoot(output_zip):
  BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "BOOT"),
                           "boot.img", output_zip)

def UnzipTemp(filename):
  """Unzip the given archive into a temporary directory and return the name."""

  tmp = tempfile.mkdtemp(prefix="targetfiles-")
  OPTIONS.tempfiles.append(tmp)
  p = Run(["unzip", "-q", filename, "-d", tmp], stdout=subprocess.PIPE)
  p.communicate()
  if p.returncode != 0:
    raise ExternalError("failed to unzip input target-files \"%s\"" %
                        (filename,))
  return tmp


def GetKeyPasswords(keylist):
  """Given a list of keys, prompt the user to enter passwords for
  those which require them.  Return a {key: password} dict.  password
  will be None if the key has no password."""

  key_passwords = {}
  devnull = open("/dev/null", "w+b")
  for k in sorted(keylist):
    p = subprocess.Popen(["openssl", "pkcs8", "-in", k+".pk8",
                          "-inform", "DER", "-nocrypt"],
                         stdin=devnull.fileno(),
                         stdout=devnull.fileno(),
                         stderr=subprocess.STDOUT)
    p.communicate()
    if p.returncode == 0:
      print "%s.pk8 does not require a password" % (k,)
      key_passwords[k] = None
    else:
      key_passwords[k] = getpass.getpass("Enter password for %s.pk8> " % (k,))
  devnull.close()
  print
  return key_passwords


def SignFile(input_name, output_name, key, password, align=None):
  """Sign the input_name zip/jar/apk, producing output_name.  Use the
  given key and password (the latter may be None if the key does not
  have a password.

  If align is an integer > 1, zipalign is run to align stored files in
  the output zip on 'align'-byte boundaries.
  """
  if align == 0 or align == 1:
    align = None

  if align:
    temp = tempfile.NamedTemporaryFile()
    sign_name = temp.name
  else:
    sign_name = output_name

  p = subprocess.Popen(["java", "-jar", OPTIONS.signapk_jar,
                        key + ".x509.pem",
                        key + ".pk8",
                        input_name, sign_name],
                       stdin=subprocess.PIPE,
                       stdout=subprocess.PIPE)
  if password is not None:
    password += "\n"
  p.communicate(password)
  if p.returncode != 0:
    raise ExternalError("signapk.jar failed: return code %s" % (p.returncode,))

  if align:
    p = subprocess.Popen(["zipalign", "-f", str(align), sign_name, output_name])
    p.communicate()
    if p.returncode != 0:
      raise ExternalError("zipalign failed: return code %s" % (p.returncode,))
    temp.close()


def CheckSize(data, target):
  """Check the data string passed against the max size limit, if
  any, for the given target.  Raise exception if the data is too big.
  Print a warning if the data is nearing the maximum size."""
  limit = OPTIONS.max_image_size.get(target, None)
  if limit is None: return

  size = len(data)
  pct = float(size) * 100.0 / limit
  msg = "%s size (%d) is %.2f%% of limit (%d)" % (target, size, pct, limit)
  if pct >= 99.0:
    raise ExternalError(msg)
  elif pct >= 95.0:
    print
    print "  WARNING: ", msg
    print
  elif OPTIONS.verbose:
    print "  ", msg


COMMON_DOCSTRING = """
  -p  (--path)  <dir>
      Prepend <dir> to the list of places to search for binaries run
      by this script.

  -v  (--verbose)
      Show command lines being executed.

  -h  (--help)
      Display this usage message and exit.
"""

def Usage(docstring):
  print docstring.rstrip("\n")
  print COMMON_DOCSTRING


def ParseOptions(argv,
                 docstring,
                 extra_opts="", extra_long_opts=(),
                 extra_option_handler=None):
  """Parse the options in argv and return any arguments that aren't
  flags.  docstring is the calling module's docstring, to be displayed
  for errors and -h.  extra_opts and extra_long_opts are for flags
  defined by the caller, which are processed by passing them to
  extra_option_handler."""

  try:
    opts, args = getopt.getopt(
        argv, "hvp:" + extra_opts,
        ["help", "verbose", "path="] + list(extra_long_opts))
  except getopt.GetoptError, err:
    Usage(docstring)
    print "**", str(err), "**"
    sys.exit(2)

  path_specified = False

  for o, a in opts:
    if o in ("-h", "--help"):
      Usage(docstring)
      sys.exit()
    elif o in ("-v", "--verbose"):
      OPTIONS.verbose = True
    elif o in ("-p", "--path"):
      os.environ["PATH"] = a + os.pathsep + os.environ["PATH"]
      path_specified = True
    else:
      if extra_option_handler is None or not extra_option_handler(o, a):
        assert False, "unknown option \"%s\"" % (o,)

  if not path_specified:
    os.environ["PATH"] = ("out/host/linux-x86/bin" + os.pathsep +
                          os.environ["PATH"])

  return args


def Cleanup():
  for i in OPTIONS.tempfiles:
    if os.path.isdir(i):
      shutil.rmtree(i)
    else:
      os.remove(i)
+157 −0
Original line number Diff line number Diff line
#!/usr/bin/env python
#
# Copyright (C) 2008 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Given a target-files zipfile, produces an image zipfile suitable for
use with 'fastboot update'.

Usage:  img_from_target_files [flags] input_target_files output_image_zip

  -b  (--board_config)  <file>
      Specifies a BoardConfig.mk file containing image max sizes
      against which the generated image files are checked.

"""

import sys

if sys.hexversion < 0x02040000:
  print >> sys.stderr, "Python 2.4 or newer is required."
  sys.exit(1)

import os
import re
import shutil
import subprocess
import tempfile
import zipfile

# missing in Python 2.4 and before
if not hasattr(os, "SEEK_SET"):
  os.SEEK_SET = 0

import common

OPTIONS = common.OPTIONS


def AddUserdata(output_zip):
  """Create an empty userdata image and store it in output_zip."""

  print "creating userdata.img..."

  # The name of the directory it is making an image out of matters to
  # mkyaffs2image.  So we create a temp dir, and within it we create an
  # empty dir named "data", and build the image from that.
  temp_dir = tempfile.mkdtemp()
  user_dir = os.path.join(temp_dir, "data")
  os.mkdir(user_dir)
  img = tempfile.NamedTemporaryFile()

  p = common.Run(["mkyaffs2image", "-f", user_dir, img.name])
  p.communicate()
  assert p.returncode == 0, "mkyaffs2image of userdata.img image failed"

  common.CheckSize(img.name, "userdata.img")
  output_zip.write(img.name, "userdata.img")
  img.close()
  os.rmdir(user_dir)
  os.rmdir(temp_dir)


def AddSystem(output_zip):
  """Turn the contents of SYSTEM into a system image and store it in
  output_zip."""

  print "creating system.img..."

  img = tempfile.NamedTemporaryFile()

  # The name of the directory it is making an image out of matters to
  # mkyaffs2image.  It wants "system" but we have a directory named
  # "SYSTEM", so create a symlink.
  os.symlink(os.path.join(OPTIONS.input_tmp, "SYSTEM"),
             os.path.join(OPTIONS.input_tmp, "system"))

  p = common.Run(["mkyaffs2image", "-f",
                  os.path.join(OPTIONS.input_tmp, "system"), img.name])
  p.communicate()
  assert p.returncode == 0, "mkyaffs2image of system.img image failed"

  img.seek(os.SEEK_SET, 0)
  data = img.read()
  img.close()

  common.CheckSize(data, "system.img")
  output_zip.writestr("system.img", data)


def CopyInfo(output_zip):
  """Copy the android-info.txt file from the input to the output."""
  output_zip.write(os.path.join(OPTIONS.input_tmp, "OTA", "android-info.txt"),
                   "android-info.txt")


def main(argv):

  def option_handler(o, a):
    if o in ("-b", "--board_config"):
      common.LoadBoardConfig(a)
      return True
    else:
      return False

  args = common.ParseOptions(argv, __doc__,
                             extra_opts="b:",
                             extra_long_opts=["board_config="],
                             extra_option_handler=option_handler)

  if len(args) != 2:
    common.Usage(__doc__)
    sys.exit(1)

  if not OPTIONS.max_image_size:
    print
    print "  WARNING:  No board config specified; will not check image"
    print "  sizes against limits.  Use -b to make sure the generated"
    print "  images don't exceed partition sizes."
    print

  OPTIONS.input_tmp = common.UnzipTemp(args[0])

  output_zip = zipfile.ZipFile(args[1], "w", compression=zipfile.ZIP_DEFLATED)

  common.AddBoot(output_zip)
  common.AddRecovery(output_zip)
  AddSystem(output_zip)
  AddUserdata(output_zip)
  CopyInfo(output_zip)

  print "cleaning up..."
  output_zip.close()
  shutil.rmtree(OPTIONS.input_tmp)

  print "done."


if __name__ == '__main__':
  try:
    main(sys.argv[1:])
  except common.ExternalError, e:
    print
    print "   ERROR: %s" % (e,)
    print
    sys.exit(1)
+670 −0

File added.

Preview size limit exceeded, changes collapsed.

+193 −0
Original line number Diff line number Diff line
#!/usr/bin/env python
#
# Copyright (C) 2008 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Signs all the APK files in a target-files zipfile, producing a new
target-files zip.

Usage:  sign_target_files_apks [flags] input_target_files output_target_files

  -s  (--signapk_jar)  <path>
      Path of the signapks.jar file used to sign an individual APK
      file.

  -e  (--extra_apks)  <name,name,...=key>
      Add extra APK name/key pairs as though they appeared in
      apkcerts.zip.  Option may be repeated to give multiple extra
      packages.

  -k  (--key_mapping)  <src_key=dest_key>
      Add a mapping from the key name as specified in apkcerts.txt (the
      src_key) to the real key you wish to sign the package with
      (dest_key).  Option may be repeated to give multiple key
      mappings.

  -d  (--default_key_mappings)  <dir>
      Set up the following key mappings:

        build/target/product/security/testkey   ==>  $dir/releasekey
        build/target/product/security/media     ==>  $dir/media
        build/target/product/security/shared    ==>  $dir/shared
        build/target/product/security/platform  ==>  $dir/platform

      -d and -k options are added to the set of mappings in the order
      in which they appear on the command line.
"""

import sys

if sys.hexversion < 0x02040000:
  print >> sys.stderr, "Python 2.4 or newer is required."
  sys.exit(1)

import os
import re
import subprocess
import tempfile
import zipfile

import common

OPTIONS = common.OPTIONS

OPTIONS.extra_apks = {}
OPTIONS.key_map = {}


def GetApkCerts(tf_zip):
  certmap = OPTIONS.extra_apks.copy()
  for line in tf_zip.read("META/apkcerts.txt").split("\n"):
    line = line.strip()
    if not line: continue
    m = re.match(r'^name="(.*)"\s+certificate="(.*)\.x509\.pem"\s+'
                 r'private_key="\2\.pk8"$', line)
    if not m:
      raise SigningError("failed to parse line from apkcerts.txt:\n" + line)
    certmap[m.group(1)] = OPTIONS.key_map.get(m.group(2), m.group(2))
  return certmap


def SignApk(data, keyname, pw):
  unsigned = tempfile.NamedTemporaryFile()
  unsigned.write(data)
  unsigned.flush()

  signed = tempfile.NamedTemporaryFile()

  common.SignFile(unsigned.name, signed.name, keyname, pw, align=4)

  data = signed.read()
  unsigned.close()
  signed.close()

  return data


def SignApks(input_tf_zip, output_tf_zip):
  apk_key_map = GetApkCerts(input_tf_zip)

  key_passwords = common.GetKeyPasswords(set(apk_key_map.values()))

  maxsize = max([len(os.path.basename(i.filename))
                 for i in input_tf_zip.infolist()
                 if i.filename.endswith('.apk')])

  for info in input_tf_zip.infolist():
    data = input_tf_zip.read(info.filename)
    if info.filename.endswith(".apk"):
      name = os.path.basename(info.filename)
      key = apk_key_map.get(name, None)
      if key is not None:
        print "signing: %-*s (%s)" % (maxsize, name, key)
        signed_data = SignApk(data, key, key_passwords[key])
        output_tf_zip.writestr(info, signed_data)
      else:
        # an APK we're not supposed to sign.
        print "skipping: %s" % (name,)
        output_tf_zip.writestr(info, data)
    elif info.filename == "SYSTEM/build.prop":
      # Change build fingerprint to reflect the fact that apps are signed.
      m = re.search(r"ro\.build\.fingerprint=.*\b(test-keys)\b.*", data)
      if not m:
        print 'WARNING: ro.build.fingerprint does not contain "test-keys"'
      else:
        data = data[:m.start(1)] + "release-keys" + data[m.end(1):]
      m = re.search(r"ro\.build\.description=.*\b(test-keys)\b.*", data)
      if not m:
        print 'WARNING: ro.build.description does not contain "test-keys"'
      else:
        data = data[:m.start(1)] + "release-keys" + data[m.end(1):]
      output_tf_zip.writestr(info, data)
    else:
      # a non-APK file; copy it verbatim
      output_tf_zip.writestr(info, data)


def main(argv):

  def option_handler(o, a):
    if o in ("-s", "--signapk_jar"):
      OPTIONS.signapk_jar = a
    elif o in ("-e", "--extra_apks"):
      names, key = a.split("=")
      names = names.split(",")
      for n in names:
        OPTIONS.extra_apks[n] = key
    elif o in ("-d", "--default_key_mappings"):
      OPTIONS.key_map.update({
          "build/target/product/security/testkey": "%s/releasekey" % (a,),
          "build/target/product/security/media": "%s/media" % (a,),
          "build/target/product/security/shared": "%s/shared" % (a,),
          "build/target/product/security/platform": "%s/platform" % (a,),
          })
    elif o in ("-k", "--key_mapping"):
      s, d = a.split("=")
      OPTIONS.key_map[s] = d
    else:
      return False
    return True

  args = common.ParseOptions(argv, __doc__,
                             extra_opts="s:e:d:k:",
                             extra_long_opts=["signapk_jar=",
                                              "extra_apks=",
                                              "default_key_mappings=",
                                              "key_mapping="],
                             extra_option_handler=option_handler)

  if len(args) != 2:
    common.Usage(__doc__)
    sys.exit(1)

  input_zip = zipfile.ZipFile(args[0], "r")
  output_zip = zipfile.ZipFile(args[1], "w")

  SignApks(input_zip, output_zip)

  input_zip.close()
  output_zip.close()

  print "done."


if __name__ == '__main__':
  try:
    main(sys.argv[1:])
  except common.ExternalError, e:
    print
    print "   ERROR: %s" % (e,)
    print
    sys.exit(1)