Exemple #1
0
  def process(self, etree):
    """Process the parsed AndroidManifest to extract SDK version info.

    Args:
      etree: An xml.etree.ElementTree object of the parsed XML file.

    Raises:
      ConfigurationError: Required elements were missing or incorrect.
      MissingActivityError: If the activity element isn't present.  This
        instance will be completely populated when this exception is thrown.
    """
    root = etree.getroot()

    self.package_name = root.get('package')

    sdk_element = root.find('uses-sdk')

    if sdk_element is None:
      raise common.ConfigurationError(self.path, 'uses-sdk element missing')

    min_sdk_version = AndroidManifest.__get_schema_attribute_value(
        sdk_element, 'minSdkVersion')
    target_sdk_version = AndroidManifest.__get_schema_attribute_value(
        sdk_element, 'targetSdkVersion')

    app_element = root.find('application')
    if app_element is None:
      raise common.ConfigurationError(self.path, 'application missing')

    activity_element = app_element.find('activity')
    if activity_element is not None:
      self.activity_name = AndroidManifest.__get_schema_attribute_value(
          activity_element, 'name')

    if not min_sdk_version:
      raise common.ConfigurationError(self.path, 'minSdkVersion missing')
    if not target_sdk_version:
      target_sdk_version = min_sdk_version
    if not self.package_name:
      raise common.ConfigurationError(self.path, 'package missing')

    self.min_sdk = int(min_sdk_version)
    self.target_sdk = int(target_sdk_version)

    if self.activity_name == _NATIVE_ACTIVITY:
      for metadata_element in activity_element.findall('meta-data'):
        if (AndroidManifest.__get_schema_attribute_value(
            metadata_element, 'name') == 'android.app.lib_name'):
          self.lib_name = AndroidManifest.__get_schema_attribute_value(
              metadata_element, 'value')

      if not self.lib_name:
        raise common.ConfigurationError(
            self.path, 'meta-data android.app.lib_name missing')

    elif not self.activity_name:
      raise AndroidManifest.MissingActivityError(
          self.path, 'activity android:name missing', self)
Exemple #2
0
  def process(self, etree):
    """Process the parsed build.xml to extract project info.

    Args:
      etree: An xml.etree.ElementTree object of the parsed XML file.

    Raises:
      ConfigurationError: Required elements were missing or incorrect.
    """
    project_element = etree.getroot()

    if project_element.tag != 'project':
      raise common.ConfigurationError(self.path, 'project element missing')

    self.project_name = project_element.get('name')

    if not self.project_name:
      raise common.ConfigurationError(self.path, 'project name missing')
Exemple #3
0
  def __init__(self, path):
    """Constructs the XMLFile for a specified path.

    Args:
      path: The absolute path to the manifest file.

    Raises:
      ConfigurationError: Manifest file missing.
    """
    if path and not os.path.exists(path):
      raise common.ConfigurationError(path, os.strerror(errno.ENOENT))

    self.path = path
Exemple #4
0
  def _sign_apk(self, source, target):
    """This function signs an Android APK, optionally generating a key.

    This function signs an APK using a keystore and password as configured
    in the build configuration. If none are configured, it generates an
    ephemeral key good for 60 days.

    Args:
      source: Absolute path to source APK to sign.
      target: Target path to write signed APK to.

    Raises:
      SubCommandError: Jarsigner invocation failed or returned an error.
      ToolPathError: Jarsigner or keygen location not found in $PATH.
      ConfigurationError: User specified some but not all signing parameters.
      IOError: An error occurred copying the APK.
    """
    # Debug targets are automatically signed and aligned by ant.
    if self.ant_target is 'debug':
      return

    keystore = self.apk_keystore
    passfile = self.apk_passfile
    alias = self.apk_keyalias

    # If any of keystore, passwdfile, or alias are None we will create a
    # temporary keystore with a random password and alias and remove it after
    # signing. This facilitates testing release builds when the release
    # keystore is not available (such as in a continuous testing environment).
    ephemeral = False

    # Exit and don't sign if the source file is older than the target.
    if os.path.exists(target):
      if os.path.getmtime(source) < os.path.getmtime(target):
        return

    # If a key / cert pair is specified, generate a temporary key store to sign
    # the APK.
    temp_directory = ''
    if self.apk_keycertpair:
      key = tempfile.NamedTemporaryFile()
      self.run_subprocess(('openssl', 'pkcs8', '-inform', 'DER', '-nocrypt',
                           '-in', self.apk_keycertpair[0], '-out', key.name))

      p12 = tempfile.NamedTemporaryFile()
      password_file = tempfile.NamedTemporaryFile()
      password = BuildEnvironment.generate_password()
      passfile = password_file.name
      password_file.write(password)
      password_file.flush()

      alias = BuildEnvironment.generate_password()
      self.run_subprocess(('openssl', 'pkcs12', '-export', '-in',
                           self.apk_keycertpair[1], '-inkey', key.name,
                           '-out', p12.name, '-password', 'pass:'******'-name', alias))
      key.close()

      temp_directory = tempfile.mkdtemp()
      keystore = os.path.join(temp_directory, 'temp.keystore')
      self.run_subprocess(('keytool', '-importkeystore', '-deststorepass',
                           password, '-destkeystore', keystore,
                           '-srckeystore', p12.name, '-srcstoretype', 'PKCS12',
                           '-srcstorepass', password))
      p12.close()

    try:
      if not keystore or not passfile or not alias:
        # If the user specifies any of these, they need to specify them all,
        # otherwise we may overwrite one of them.
        if keystore:
          raise common.ConfigurationError(keystore,
                                          ('Must specify all of keystore, '
                                           'password file, and alias'))
        if passfile:
          raise common.ConfigurationError(passfile,
                                          ('Must specify all of keystore, '
                                           'password file, and alias'))
        if alias:
          raise common.ConfigurationError(alias,
                                          ('Must specify all of keystore, '
                                           'password file, and alias'))
        ephemeral = True
        keystore = source + '.keystore'
        passfile = source + '.password'
        if self.verbose:
          print ('Creating ephemeral keystore file %s and password file %s' %
                 (keystore, passfile))

        password = BuildEnvironment.generate_password()
        with open(passfile, 'w') as pf:
          if self._posix:
            os.fchmod(pf.fileno(), stat.S_IRUSR | stat.S_IWUSR)
          pf.write(password)

        alias = os.path.basename(source).split('.')[0]

        # NOTE: The password is passed via the command line for compatibility
        # with JDK 6.  Move to use -storepass:file and -keypass:file when
        # JDK 7 is a requirement for Android development.
        acmd = [self._find_binary(BuildEnvironment.KEYTOOL), '-genkeypair',
                '-v', '-dname', 'cn=, ou=%s, o=fpl' % alias, '-storepass',
                password, '-keypass', password, '-keystore', keystore,
                '-alias', alias, '-keyalg', 'RSA', '-keysize', '2048',
                '-validity', '60']
        self.run_subprocess(acmd)

      tmpapk = target + '.tmp'

      if self.verbose:
        print 'Copying APK %s for signing as %s' % (source, tmpapk)

      shutil.copy2(source, tmpapk)

      with open(passfile, 'r') as pf:
        password = pf.read()

      # NOTE: The password is passed via stdin for compatibility with JDK 6
      # which - unlike the use of keytool above - ensures the password is
      # not visible when displaying the command lines of processes of *nix
      # operating systems like Linux and OSX.
      # Move to use -storepass:file and -keypass:file when JDK 7 is a
      # requirement for Android development.
      password_stdin = os.linesep.join(
          [password, password,  # Store password and confirmation.
           password, password])  # Key password and confirmation.
      acmd = [self._find_binary(BuildEnvironment.JARSIGNER),
              '-verbose', '-sigalg', 'SHA1withRSA', '-digestalg',
              'SHA1', '-keystore', keystore, tmpapk, alias]
      self.run_subprocess(acmd, stdin=password_stdin)

      # We want to align the APK for more efficient access on the device.
      # See:
      # http://developer.android.com/tools/help/zipalign.html
      acmd = [self._find_binary(BuildEnvironment.ZIPALIGN), '-f']
      if self.verbose:
        acmd.append('-v')
      acmd += ['4', tmpapk, target]  # alignment == 4
      self.run_subprocess(acmd)

    finally:
      if temp_directory and os.path.exists(temp_directory):
        shutil.rmtree(temp_directory)
      if ephemeral:
        if self.verbose:
          print 'Removing ephemeral keystore and password files'
        if os.path.exists(keystore):
          os.unlink(keystore)
        if os.path.exists(passfile):
          os.unlink(passfile)
Exemple #5
0
  def _find_best_android_sdk(self, android, minsdk, target):
    """Finds the best installed Android SDK for a project.

    Based on the passed in min and target SDK levels, find the highest SDK
    level installed that is greater than the specified minsdk, up to the
    target sdk level. Return it as an API level string.

    Otherwise, if the minimum installed SDK is greater than the
    targetSdkVersion, return the maximum installed SDK version, or raise a
    ConfigurationError if no installed SDK meets the min SDK.

    Args:
      android: Path to android tool binary.
      minsdk: Integer minimum SDK level.
      target: Integer target SDK level.

    Returns:
      Highest installed Android SDK API level in the range, as a string.

    Raises:
      SubCommandError: NDK toolchain invocation failed or returned an error.
      ToolPathError: Android NDK or SDK location not found in configured build
          environment or $PATH, or ant not found.
      ConfigurationError: Required build configuration file missing or broken
          in an unrecoverable way.
    """
    acmd = [android, 'list', 'target', '--compact']
    (stdout, unused_stderr) = self.run_subprocess(acmd, capture=True)

    if self.verbose:
      print 'android list target returned: {%s}' % (stdout)
    # Find the highest installed SDK <= targetSdkVersion, if possible.
    #
    # 'android list target --compact' will output lines like:
    #
    # android-1
    # android-2
    #
    # for installed SDK targets, along with other info not starting with
    # android-.
    installed = 0
    for line in stdout.splitlines():
      l = line.strip()
      if l.startswith('android-'):
        nstr = l.split('-')[1]
        # Ignore preview SDK revisions (e.g "L").
        if not nstr.isdigit():
          continue
        n = int(nstr)
        if n > installed:
          if self.verbose:
            print 'sdk api level %d found' % (n)
          installed = n
        if installed == target:
          break

    if installed < minsdk:
      raise common.ConfigurationError(self.sdk_home,
                                      ('Project requires Android SDK %d, '
                                       'but only found up to %d' %
                                       (minsdk, installed)))

    apitarget = 'android-%d' % (installed)
    return apitarget
Exemple #6
0
  def install_android_apk(self, path='.', adb_device=None, force_install=True):
    """Install an android apk on the given device.

    This function will attempt to install an unsigned APK if a signed APK is
    not available which will *only* work on rooted devices.

    Args:
      path: Relative path from project directory to project to run.
      adb_device: The device to run the apk on. If none it will use the only
        device connected.
      force_install: Whether to install the package if it's older than the
        package on the target device.

    Raises:
      ConfigurationError: If no APKs are found.
      AdbError: If it's not possible to install the APK.
    """
    adb_path = self._find_binary(BuildEnvironment.ADB)
    device = self.check_adb_devices(adb_device=adb_device)
    adb_device_arg = self.get_adb_device_argument(adb_device=device.serial)
    try:
      manifest = self.parse_manifest(path=path)
    except AndroidManifest.MissingActivityError as e:
      print >>sys.stderr, str(e)
      return
    buildxml = self.create_update_build_xml(manifest, path=path)
    apks = [f for f in self.get_apk_filenames(buildxml.project_name,
                                              path=path) if os.path.exists(f)]
    if not apks:
      raise common.ConfigurationError(
          'Unable to find an APK for the project in %s' % (
              self.get_project_directory(path=path)))

    print 'Installing %s on %s' % (apks[0], self.get_adb_device_name(device))

    # If the project is installed and it's older than the current APK,
    # uninstall it.
    if manifest.package_name in self.list_installed_packages(
        adb_device=adb_device):

      if not force_install:
        # Get the modification time of the package on the device.
        get_package_modification_date_args = [adb_path]
        if adb_device_arg:
          get_package_modification_date_args.extend(adb_device_arg.split())
        get_package_modification_date_args.extend([
            'shell',
            r'f=( $(ls -l $( IFS=":"; p=( $(pm path %s) ); echo ${p[1]} )) ); '
            r'echo "${f[4]} ${f[5]}"' % manifest.package_name])
        out, _ = self.run_subprocess(get_package_modification_date_args,
                                     capture=True)
        if out:
          remote_modification_time = int(time.mktime(
              datetime.datetime.strptime(out.splitlines()[0],
                                         '%Y-%m-%d %H:%M').timetuple()))
          local_modification_time = os.stat(apks[0]).st_mtime
          if local_modification_time < remote_modification_time:
            print 'Not installing %s, already up to date (%d vs. %d)' % (
                manifest.package_name, local_modification_time,
                remote_modification_time)
            return

      self.run_subprocess('%s %s uninstall %s' % (
          adb_path, adb_device_arg, manifest.package_name), shell=True)
    # Install the APK.
    self.run_subprocess('%s %s install %s' % (
        adb_path, adb_device_arg, apks[0]), shell=True)
Exemple #7
0
 def _parse(self, xmlfile):
   try:
     etree = xml.etree.ElementTree.parse(xmlfile)
     self.process(etree)
   except xml.etree.ElementTree.ParseError as pe:
     raise common.ConfigurationError(self.path, 'XML parse error: ' + str(pe))