Subir automáticamente tu app a la play store

Últimamente estoy muy metido en esto del CI/CD (Integración continúa/Despliegue continuo) que no significa, ni más ni menos, que las actualizaciones a los repositorios de código generan automáticamente todos los artefactos necesarios para poder comprobar que están correctos y que pueden desplegarse en producción, e incluso desplegarlos si así lo consideramos.

En el caso de servicios en internet de cualquier tipo (webs, APIs, etc.) el método de despliegue suele ser más sencillo de automatizar (básicamente generas un contenedor con una versión nueva y lo «empujas» al sistema de producción), más sencillo incluso si utilizamos kubernetes o ansible. Pero en el caso de distribución de aplicaciones a instalar en clientes es más complicado. En el caso que traemos aquí lo que queremos hacer es subir a la tienda de google play (y ya veremos qué hacemos con apple) una nueva versión de una aplicación móvil.

En principio debería ser sencillo, ya que solo tenemos que utilizar el API de Google Play para desarrolladores. Pero antes, tenemos que conseguir ciertas «autorizaciones» de google que no son tan triviales como deberían. Para haceros la tarea un poco más sencilla, aquí os dejo un script python que hace justo lo que queremos: subir a la tienda nuestro apk recién compilado al canal alpha (upload_apks.py)

"""Uploads an apk to the alpha track."""

import argparse

from googleapiclient import discovery
import httplib2
from oauth2client.service_account import ServiceAccountCredentials
from oauth2client import client

TRACK = 'alpha'  # Can be 'alpha', beta', 'production' or 'rollout'
SERVICE_ACCOUNT_EMAIL = (
    'ENTER_YOUR_SERVICE_ACCOUNT_EMAIL_HERE@developer.gserviceaccount.com')

# Declare command-line flags.
argparser = argparse.ArgumentParser(add_help=False)
argparser.add_argument('package_name',
                       help='The package name. Example: com.android.sample')
argparser.add_argument('apk_file',
                       nargs='?',
                       default='test.apk',
                       help='The path to the APK file to upload.')
argparser.add_argument('key_file',
                       nargs='?',
                       default='key.json',
                       help='key in json format for service account')
argparser.add_argument('version',
                       nargs='?',
                       default='New version',
                       help='Version name')

def main():
  # Process flags and read their values.
  flags = argparser.parse_args()

  package_name = flags.package_name
  apk_file = flags.apk_file
  key_file = flags.key_file
  version = flags.version
  
  credentials = ServiceAccountCredentials.from_json_keyfile_name(
      key_file,
      scopes='https://www.googleapis.com/auth/androidpublisher')
  http = httplib2.Http()
  http = credentials.authorize(http)

  service = discovery.build('androidpublisher', 'v3', http=http)

  try:
    edit_request = service.edits().insert(body={}, packageName=package_name)
    result = edit_request.execute()
    edit_id = result['id']

    apk_response = service.edits().apks().upload(
        editId=edit_id,
        packageName=package_name,
        media_body=apk_file).execute()

    print ('Version code {} has been uploaded'.format(apk_response['versionCode']))

    track_response = service.edits().tracks().update(
        editId=edit_id,
        track=TRACK,
        packageName=package_name,
        body={u'releases': [{
            u'name': version,
            u'versionCodes': [apk_response['versionCode']],
            u'status': u'completed',
        }]}).execute()

    print ('Track {} is set for release(s) {}' .format (
        track_response['track'], str(track_response['releases'])))

    commit_request = service.edits().commit(
        editId=edit_id, packageName=package_name).execute()

    print ('Edit "{}" has been committed'.format(commit_request['id']))

  except client.AccessTokenRefreshError:
    print ('The credentials have been revoked or expired, please re-run the '
           'application to re-authorize')

if __name__ == '__main__':
  main()

Antes de poder ejecutar este código deberás instalar algunas librerías (o ponerlas en el Dockerfile)

apt-get update -y
apt-get install python3 -y
apt-get install python3-pip -y
pip3 install google-api-python-client
pip3 install --upgrade oauth2client

Y después crear un archivo pc-api.json con este contenido (que luego os explico como conseguir):

{
  "type": "service_account",
  "project_id": "<project-id>",
  "private_key_id": "<private-key-id>",
  "private_key": "-----BEGIN PRIVATE KEY-----...\n-----END PRIVATE KEY-----\n",
  "client_email": "<service-account>",
  "client_id": "<client-id>",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/<service-account>"
}

Y ya, por último, solo nos quedaría ejecutar el comando con estos parámetros:

upload_apks.py com.paquete.app apk/app-release.apk res/pc-api.json ${VERSION}

Y ahora la parte más pesada… Los datos a rellenar en pc-api.json, para ello deberás configurar un proyecto API en la consola cloud de Google y crear una cuenta de servicio… Este no es un proceso inmediato, pero hay buenas instrucciones en la página de google.

Luego hay que ir a la sección de la Google Play Console al apartado Acceso a API, donde deberás vincular el proyecto que acabas de crear y crear las cuentas de servicio necesarias.

Para después en la sección de Usuarios y permisos dar permisos a la cuenta de servicio que acabamos de crear, al menos estos:

El tema de extraer la clave privada y eso ya os lo dejo para después por si alguien tiene curiosidad…

ACTUALIZACIÓN 4/01/2022

Como ahora Google solo deja subir bundles (.aab) dejo aquí el código modificado para poder hacerlo (según el tipo de archivo que elijamos):

#!/usr/bin/env python3
#
# Copyright 2014 Marta Rodriguez.
# Modified by Jose Antonio Espinosa (2021-2022)
#
# 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.

"""Uploads an apk/aab to the alpha track."""

import argparse

from googleapiclient import discovery
import httplib2
from oauth2client.service_account import ServiceAccountCredentials
from oauth2client import client
import os.path
import mimetypes

mimetypes.add_type("application/octet-stream", ".apk")
mimetypes.add_type("application/octet-stream", ".aab")

TRACK = 'alpha'  # Can be 'alpha', beta', 'production' or 'rollout'
SERVICE_ACCOUNT_EMAIL = (
    'ENTER_YOUR_SERVICE_ACCOUNT_EMAIL_HERE@developer.gserviceaccount.com')

# Declare command-line flags.
argparser = argparse.ArgumentParser(add_help=False)
argparser.add_argument('package_name',
                       help='The package name. Example: com.android.sample')
argparser.add_argument('apk_file',
                       nargs='?',
                       default='test.apk',
                       help='The path to the APK file to upload.')
argparser.add_argument('key_file',
                       nargs='?',
                       default='key.json',
                       help='key in json format for service account')
argparser.add_argument('version',
                       nargs='?',
                       default='New version',
                       help='Version name')

def main():
  # Process flags and read their values.
  flags = argparser.parse_args()

  package_name = flags.package_name
  apk_file = flags.apk_file
  key_file = flags.key_file
  version = flags.version
  extension = os.path.splitext(apk_file)[1]
  
  # Create an httplib2.Http object to handle our HTTP requests and authorize it
  # with the Credentials. Note that the first parameter, service_account_name,
  # is the Email address created for the Service account. It must be the email
  # address associated with the key that was created.
  credentials = ServiceAccountCredentials.from_json_keyfile_name(
      key_file,
      scopes='https://www.googleapis.com/auth/androidpublisher')
  http = httplib2.Http()
  http = credentials.authorize(http)

  service = discovery.build('androidpublisher', 'v3', http=http)

  try:
    edit_request = service.edits().insert(body={}, packageName=package_name)
    result = edit_request.execute()
    edit_id = result['id']

    if extension == '.apk':
        apk_response = service.edits().apks().upload(
            editId=edit_id,
            packageName=package_name,
            media_body=apk_file).execute()
    else:
        apk_response = service.edits().bundles().upload(
            editId=edit_id,
            packageName=package_name,
            media_body=apk_file).execute()

    print ('Version code {} has been uploaded'.format(apk_response['versionCode']))

    track_response = service.edits().tracks().update(
        editId=edit_id,
        track=TRACK,
        packageName=package_name,
        body={u'releases': [{
            u'name': version,
            u'versionCodes': [apk_response['versionCode']],
            u'status': u'completed',
        }]}).execute()

    print ('Track {} is set for release(s) {}' .format (
        track_response['track'], str(track_response['releases'])))

    commit_request = service.edits().commit(
        editId=edit_id, packageName=package_name).execute()

    print ('Edit "{}" has been committed'.format(commit_request['id']))

  except client.AccessTokenRefreshError:
    print ('The credentials have been revoked or expired, please re-run the '
           'application to re-authorize')

if __name__ == '__main__':
  main()

Dale nueva vida a tu viejo macbook pro

Aunque yo no soy muy fan de apple en nada (como podréis ver si miráis un poco en este blog), el hecho es que tuve que gastarme una cantidad ingente de dinero en comprarme un macbook pro en 2011 para tener la oportunidad de empezar a programar aplicaciones para iOS. Entonces no había otra manera, ahora, por suerte, si que la hay como podéis ver en este mismo blog.

Es la mayor inversión que he hecho nunca en un ordenador y, aunque mac os no es que sea el mejor sistema del mundo, el hardware si que estaba muy cuidado, la carcasa en aluminio y el teclado retroiluminado y amplio (además de una pantalla muy brillante) me hicieron tenerlo como portatil principal durante muchos años. Pero la obsolescencia programa de apple terminaría por llamar a la puerta. Primero fue el chip de la GPU que, aparentemente por exceso de calor, terminó por romperse y me obligaron a comprarme toda una nueva placa madre (coste superior a un portatil completo que no fuese apple), pero es que no contentos con esto dejaron de actualizar el sistema operativo en High Sierra y ya me he perdido 3 grandes actualizaciones. Después de que la «nueva» GPU terminase por romperse de nuevo decidí dejar de usarlo por un tiempo… Hasta ahora que he decidido ponerle un arranque dual y tener ubuntu y os x (lo que me permita apple) en el mismo ordenador (ahora mismo estoy escribiendo esto desde ubuntu en el macbook pro). Os comento en este post algunas cosas a hacer para resucitar un viejo macbook pro 2011 con la GPU radeon rota.

Lo primero es poder arrancar una vez que la tarjeta gráfica se ha estropeado, para ello lo que hay que hacer es arrancar en modo superusuario (arrancar con cmd+s pulsado) y en esa terminal escribir:

nvram fa4ce28d-b62f-4c99-9cc3-6815686e30f9:gpu-power-prefs=%01%00%00%00
reboot

Una vez reiniciado ya con la pantalla normal (esto es temporal) habría que hacerlo un poco más permanente, para ello he creado un script que podríais lanzar desde cualquier distribución live (yo uso ubuntu) y que de hecho yo tengo almacenada en el usb arrancable (en la particion writable). El script es este:

echo "Ejecutar como root este script"
printf "\x07\x00\x00\x00\x01\x00\x00\x00" > /sys/firmware/efi/efivars/gpu-power-prefs-fa4ce28d-b62f-4c99-9cc3-6815686e30f9
chattr +i "/sys/firmware/efi/efivars/gpu-power-prefs-fa4ce28d-b62f-4c99-9cc3-6815686e30f9"
cd /
umount /sys/firmware/efi/efivars/
echo "Ahora deberías reiniciar la máquina para que tenga efecto"

Arrancar con la live (tener pulsado la tecla alt mientras se arranca y luego seleccionar el disco arrancable de ubuntu) y ejecutar como root el script. Esto hace que el cambio sea permanente (hasta que se actualice de nuevo la información de la EFI, pero eso solo pasa con las instalaciones grandes del sistema operativo).

Lo siguiente ya es más entretenido… Actualizar el hardware (yo le he puesto dos ssd y estoy pensando en actualizar la memoria), instalar linux, intentar actualizar high sierra a otro sistema (yo he conseguido instalarle mojave), limpiarle por dentro y cambiar la pasta térmica (esto no es tan complicado pero hay que lidiar con la forma de integrar componentes que tiene mac). En fin, entretenimiento para rato.

Por suerte, parece que todo funciona (menos la maldita GPU de AMD) y puedo volver a usar el trasto cuando quiera.

Instalar windows en un HP 15s-fq2000

Hay pocas cosas en informática que no me apetezcan hacer, pero una de ellas es tener que instalar windows en un ordenador. Como profesional que soy utilizo las herramientas que mejor vienen a mi trabajo y esas, desde hace más de 14 años viene siendo Linux, pero, en fín, si algún cliente tiene el capricho o, como en este caso si alguno de mis hijos se emperra por tener lo mismo que sus colegas, pues bueno, hay que ponerse.

El caso es que tras adquirir un portatil HP 15S-fq2005ns Intel Core i5-1135G7/8GB/512GB SSD/15.6″ en una tienda en la que dije que no iba a comprar nunca más descubro que el ordenador no trae sistema operativo (demasiado bien de precio, ya me parecía), así que desempolvo mi viejo usb instalador de windows y la licencia que tengo como partner de microsoft que es mi empresa y me pongo al tajo…

Primer paso, bien, segundo paso, bien, meto el número de licencia y luego… Me sale algo como esto:

¡Vaya por Dios! que no tengo disco… Salgo a la bios, encuentro que el disco es un NVMe 2.0 de 500Gb y que está ahí tan pancho. Y entonces es cuando pierdo un montón de horas en buscar en todos los sitios posibles una solución a esto… Os voy a ahorrar el sufrimiento y paso directamente a lo que tenéis que hacer si se os da el caso. El problema, aparentemente, es que teniendo un procesador de 11ª generación y un NVMe pues windows no te lo iba a reconocer si no le poníamos los drivers antes, así que lo que hice fue lo siguiente:

  1. Descargar los drivers de Intel Rapid Storage Technology (IRST), por ejemplo, del propio intel. El archivo se llama f6flpy-x64.zip
  2. Descomprimir el contenido del zip en un usb (o en el mismo usb de instalación dentro de un directorio)
  3. En la pantalla en la que windows no nos encuentra el disco pulsar en «Cargar controlador» y luego en «examinar»
  4. Seleccionar la unidad y el directorio donde hemos extraído los archivos y seleccionar el driver que pone Intel RST VMD Controller 9A08 (TGL)
  5. Cuando se completa la instalación ya veréis, por fin, vuestro disco y podréis continuar con la instalación

Algunas notas:

  • Estos drivers solo funcionan con Windows 10 20H2 y más modernos., Yo me tiré un par de horas intentando que funcionase en mi usb instalador de una versión anterior y no hubo manera.
  • En la página de HP hay otros drivers que podemos utilizar (sp108979.exe), pero generan varios directorios de drivers y no estoy muy seguro de cuantos de ellos hay que cargar
  • Ya se que esto es obvio, pero Ubuntu 20.04 se instaló a la primera detectando correctamente disco y todo…

Espero no tener que repetir la experiencia próximamente (esa y la hora y media de actualizaciones de drivers que siguieron a la instalación principal), pero bueno, para que luego no se diga que no uso windows.

Hasta la próxima!

Big Sur + iphone en mi ubuntu

Continuando con lo que escribí en la anterior entrada, el siguiente objetivo es múltiple: conseguir la última versión de mac os (Big Sur) y poder utilizar un teléfono físico con la máquina virtual para poder generar la versión final de las aplicaciones para ios.

Utilizar sosumi en ubuntu es muy sencillo y relativamente poco complicado, pero al intentar actualizar a BigSur siempre daba el mismo problema… Se descargaba la actualización, comanzaba a ejecutarla y tras el primer reinicio… nada… Seguimos en Catalina. Tras varios intentos infructuosos decidí seguir otro camino. Decidí seguir esta entrada y crear un usb bootable para instalar Big Sur desde cero. Eso lo conseguí añadiendo el dispositivo usb al archivo launch con esta línea:

-device usb-host,vendorid=0x0951,productid=0x16ae

El vendorid y productid lo podemos sacar con un comando lsusb:

Bus 003 Device 072: ID 0951:16ae Kingston Technology DT microDuo 3C

Con esta línea en el archivo launch conseguimos que la máquina macos reconozca el usb cuando lo pinchemos y nos permita formatearlo e instalarle el instalador de BigSur. Arrancando la máquina con este usb pinchado nos permitiría ejecutar una instalación limpia de Big Sur (en teoría)… Pero la teoría y la práctica en la práctica no coinciden y no conseguí que funcionase la instalación desde el usb con sosumi, así que, guardando mi dispositivo USB por si las moscas (spoiler alert, si que lo vamos a necesitar) busqué otras formas de ejecutar macos en mi ubuntu. El siguiente en la lista y que me ha dado buenos resultados está en esta url:

https://github.com/kholia/OSX-KVM

Siguiendo las instrucciones de instalación se consigue más o menos de manera sencilla lo mismo que con sosumi, resumiendo, esto es lo que hay que hacer:

echo 1 | sudo tee /sys/module/kvm/parameters/ignore_msrs
sudo apt-get install qemu uml-utilities virt-manager git \
    wget libguestfs-tools p7zip-full -y
git clone --depth 1 https://github.com/kholia/OSX-KVM.git
cd OSX-KVM

Llegados a este punto deberíamos poder bajarnos de la web de apple el instalador del sistema que queremos, siguiendo las instrucciones a mi no me ha funcionado y terminaba instalándose catalina cada vez… Así que recuperando mi USB de instalación de BigSur decidí arriesgarme y utilizar la imagen que creé para arrancar, para ello copié el archivo BaseSystem.dmg que se encuentra en el directorio BaseSystem del USB creado al directorio y lo convertí con el comando:

qemu-img convert BaseSystem.dmg -O raw BaseSystem.img

Lo siguiente es crear un disco duro con el tamaño que nos interese (yo lo creé de 128G para no quedarme corto) con:

qemu-img create -f qcow2 mac_hdd_ng.img 128G

Y ya podemos arrancar el sistema y hacer lo mismo que hicimos con sosumi, entrar en el gestor de discos, formatear el disco inicial y luego seleccionar la opción de reinstalación del sistema operativo… Va a tardar un rato largo pero, al final, tendrás un Big sur operativo ejecutando el comando:

./OpenCore-Boot.sh

Y como veo que este post me está quedando un poco largo, dejaremos el tema de pinchar un iphone a nuestra máquina virtual para un post posterior… Porque la cosa tiene miga (no, no es tan sencillo como lo de la memoria USB de antes).

Por ahora podéis experimentar con el sistema y veréis que tenéis algo más de control que con sosumi a cambio de tener un poco más de cuidado con lo que hacéis… Ah! y como bonus, que sepas que puedes hacer un backup de la máquina virtual simplemente copiando el directorio OSX-KVM y podrás restaurar la máquina a ese mismo estado (incluso llevártela a otro ordenador) sin ningún problema.

Mac os en mi Ubuntu

Llevo varios años desarrollando aplicaciones móviles, la última nomorepass, y me encuentro siempre en la tesitura de tener que compilar la aplicación en un mac nativo. Supongo que eso es un peaje que Apple pide por «dejarte» usar ios, pero es que gastarse unos cuantos miles de euros solo para compilar una aplicación es bastante aberrante.

CREATOR: gd-jpeg v1.0 (using IJG JPEG v80), quality = 82

Hace unos años me compré un macbook pro de 15″ que me ha dado buen servicio hasta hace cosa de tres años en que falló el chip gráfico de nvidia y el servicio técnico me cambió la placa entera (700 Euros) y me querían cobrar otros 400 si se me ocurría reclamar mi placa vieja… Negociazo redondo para apple cuando cambie un chip y le endose la misma placa a otro ingenuo… En fin, que se me han quitado las ganas de comprar un apple y el que tenemos en la oficina es un poco «lento» y tarda una eternidad en compilar un simple proyecto (más con cada actualización del sistema operativo). Por esto he estado intentando de todas las maneras posibles poder hacer esa compilación en una máquina profesional de verdad que corriese linux.. He intentado hackintosh, he intentado virtualbox, etc… Hasta que hace poco encontré que se puede instalar una versión de mac en qemu… Una versión genuina, sin modificar… Y dicho y hecho…

Forma sencilla

La forma más sencilla de instalar mac os (Catalina) en un Ubuntu es instalar el paquete snap sosumi. Os recomiendo mucho que echéis un vistazo a este video, ya que explica todo con cierta profundidad. Básicamente esto es lo que necesité hacer:

sudo snap install sosumi

Una vez instalado (es rápido), se puede ejecutar incluso desde el lanzador de aplicaciones buscando sosumi. La primera vez que se lanza te mostrará la pantalla de recuperación y deberás abrir el programa Disk Utility para dar formato al disco virtual (inicialmente le da una capacidad limitada, pero puedes ampliarlo antes de hacer este paso):

En Disk Utility selecciona el primer disco y dale formato con la opción Erase… Ponle el nombre que quieras (por ejemplo MacHD) y debería quedar algo así:

Luego, cierra la aplicación y ves a la opción de Reinstalar macOS… Y listo para instalar

Aceptas la licencia y seleccionas el disco para instalar y eso es todo… Tendrás una máquina con macOS catalina lista para ejecutar.

Hay algunas cosillas interesantes a hacer como cambiar la resolución de la pantalla, para eso os recomiendo que sigáis este procedimiento, o aumentar la memoria o los cores que se hace editando el archivo ~/snap/sosumi/common/launch y teniendo cuidado en no poner cosas disparatadas.

El mayor problema que me he encontrado con este método es que no he conseguido actualizarlo a la nueva versión Big Sur, por lo que su utilidad queda un poco limitada. Sin embargo, he encontrado un método (un poco rebuscado, eso si) para instalar BigSur en qemu y poder utilizar mi máquina linux para compilar con xcode… Pero eso si, os lo contaré cuando lo tenga un poco más pulido