Sistemas CI para el desarrollo de iOS: transformación de Intel a ARM

Publicado: 2024-02-14

En el panorama tecnológico en constante evolución, las empresas deben adaptarse a los vientos del cambio para seguir siendo relevantes y competitivas. Una de esas transformaciones que ha arrasado en el mundo de la tecnología es la transición de la arquitectura Intel x86_64 a la arquitectura iOS ARM, ejemplificada por el innovador chip Apple M1 de Apple. En este contexto, los sistemas de CI para iOS se han convertido en una consideración crucial para las empresas que navegan por este cambio, asegurando que los procesos de desarrollo y prueba de software sigan siendo eficientes y actualizados con los últimos estándares tecnológicos.

Apple anunció sus chips M1 hace casi tres años y, desde entonces, ha quedado claro que la empresa adoptaría la arquitectura ARM y, finalmente, dejaría de admitir el software basado en Intel. Para mantener la compatibilidad entre arquitecturas, Apple ha introducido una nueva versión de Rosetta, su marco de traducción binaria patentado, que ha demostrado ser confiable en el pasado durante la importante transformación de la arquitectura de PowerPC a Intel en 2006. La transformación aún está en curso y hemos visto Xcode pierde su soporte para Rosetta en la versión 14.3.

En Miquido reconocimos la necesidad de migrar de Intel a ARM hace unos años. Comenzamos los preparativos a mediados de 2021. Como empresa de software con múltiples clientes, aplicaciones y proyectos en curso al mismo tiempo, enfrentamos algunos desafíos que tuvimos que superar. Este artículo puede ser su guía práctica si su empresa enfrenta situaciones similares. Las situaciones y soluciones descritas se describen desde una perspectiva de desarrollo de iOS, pero es posible que también encuentre información adecuada para otras tecnologías. Un componente crítico de nuestra estrategia de transición implicó garantizar que nuestro sistema de CI para iOS estuviera completamente optimizado para la nueva arquitectura, destacando la importancia de Los sistemas CI para iOS mantienen flujos de trabajo eficientes y resultados de alta calidad en medio de un cambio tan significativo.

El problema: migración de la arquitectura ARM de Intel a iOS

Apple lanzó su chip M1 en 2020

El proceso migratorio se dividió en dos ramas principales.

1. Reemplazar las computadoras de desarrolladores basadas en Intel existentes por nuevas Macbooks M1

Se suponía que este proceso sería relativamente simple. Establecimos una política para reemplazar gradualmente todas las Macbooks Intel del desarrollador durante dos años. En la actualidad, el 95 % de nuestros empleados utilizan Macbooks basados ​​en ARM.

Sin embargo, encontramos algunos desafíos inesperados durante este proceso. A mediados de 2021, la escasez de Mac M1 ralentizó nuestro proceso de reemplazo. A finales de 2021, solo habíamos podido reemplazar un puñado de Macbook de los casi 200 que estaban en espera. Estimamos que se necesitarían alrededor de dos años para reemplazar completamente todas las Mac Intel de la compañía con Macbook M1, incluidos los ingenieros que no son iOS.

Afortunadamente, Apple lanzó sus nuevos chips M1 Pro y M2. Como resultado, hemos cambiado nuestro enfoque de reemplazar Intel con Mac M1 a reemplazarlos con chips M1 Pro y M2.

El software que no está listo para el cambio provocó la frustración de los desarrolladores

Los primeros ingenieros que recibieron las nuevas Macbooks M1 tuvieron dificultades porque la mayor parte del software no estaba listo para cambiar a la nueva arquitectura iOS ARM de Apple. Las herramientas de terceros como Rubygems y Cocoapods, que son herramientas de gestión de dependencias que dependen de muchos otros Rubygems, fueron las más afectadas. Algunas de estas herramientas no se compilaron para la arquitectura ARM de iOS en ese momento, por lo que la mayor parte del software tuvo que ejecutarse usando Rosetta, lo que provocó problemas de rendimiento y frustración.

Sin embargo, los creadores de software trabajaron para resolver la mayoría de estos problemas a medida que surgían. El momento decisivo llegó con el lanzamiento de Xcode 14.3, que ya no era compatible con Rosetta. Esta fue una señal clara para todos los desarrolladores de software de que Apple estaba presionando para una migración de la arquitectura Intel a iOS ARM. Esto obligó a la mayoría de los desarrolladores de software externos que anteriormente habían confiado en Rosetta a migrar su software a ARM. Hoy en día, el 99 % del software de terceros que se utiliza a diario en Miquido se ejecuta sin Rosetta.

2. Reemplazo del sistema CI de Miquido para iOS

Reemplazar el sistema iOS de integración continua en Miquido resultó ser una tarea más complicada que simplemente cambiar las máquinas. Primero, eche un vistazo a nuestra infraestructura en aquel entonces:

La arquitectura CI en Miquido. Sistema CI para la transformación de iOS.

Teníamos una instancia en la nube de Gitlab y 9 Mac Minis basados ​​en Intel conectados a ella. Estas máquinas servían como ejecutores de trabajos y Gitlab era responsable de la orquestación. Siempre que un trabajo de CI estaba en cola, Gitlab lo asignaba al primer ejecutor disponible que cumplía con los requisitos del proyecto especificados en el archivo gitlab-ci.yml. Gitlab crearía un script de trabajo que contiene todos los comandos de compilación, variables, rutas, etc. Luego, este script se movió al ejecutor y se ejecutó en esa máquina.

Si bien esta configuración puede parecer sólida, enfrentamos problemas con la virtualización debido al escaso soporte de los procesadores Intel. Como resultado, decidimos no utilizar virtualización como Docker y ejecutar trabajos en las propias máquinas físicas. Intentamos configurar una solución eficiente y confiable basada en Docker, pero las limitaciones de virtualización, como la falta de aceleración de GPU, hicieron que los trabajos tardaran el doble en ejecutarse que en las máquinas físicas. Esto generó más gastos generales y colas que se llenaron rápidamente.

Debido al SLA de macOS, solo pudimos configurar dos máquinas virtuales simultáneamente. Por lo tanto, decidimos ampliar el grupo de ejecutores físicos y configurarlos para ejecutar trabajos de Gitlab directamente en su sistema operativo. Sin embargo, este enfoque también tenía algunos inconvenientes.

Desafíos en el proceso de construcción y la gestión de corredores

  1. No hay aislamiento de las compilaciones fuera del entorno limitado del directorio de compilaciones.

El ejecutor ejecuta cada compilación en una máquina física, lo que significa que las compilaciones no están aisladas del entorno limitado del directorio de compilación. Esto tiene sus ventajas y desventajas. Por un lado, podemos usar cachés del sistema para acelerar las compilaciones, ya que la mayoría de los proyectos usan el mismo conjunto de dependencias de terceros.

Por otro lado, el caché se vuelve imposible de mantener ya que los restos de un proyecto pueden afectar a todos los demás proyectos. Esto es particularmente importante para los cachés de todo el sistema, ya que se utilizan los mismos ejecutores para el desarrollo de Flutter y React Native. React Native, en particular, requiere muchas dependencias almacenadas en caché a través de NPM.

  1. Posible desorden de herramientas del sistema.

Aunque ninguno de los trabajos se había ejecutado con privilegios sudo, todavía les era posible acceder a algunas de las herramientas del sistema o del usuario, como Ruby. Esto planteaba una amenaza potencial de dañar algunas de estas herramientas, especialmente porque macOS usa Ruby para parte de su software heredado, incluidas algunas funciones heredadas de Xcode. La versión del sistema de Ruby no es algo con lo que quieras meterte.

Sin embargo, la introducción de rbenv crea otra capa de complejidad con la que lidiar. Es importante tener en cuenta que Rubygems se instalan según la versión de Ruby y algunas de estas gemas requieren versiones específicas de Ruby. Casi todas las herramientas de terceros que utilizábamos dependían de Ruby, siendo Cocoapods y Fastlane los actores principales.

  1. Gestión de identidades de firma.

Administrar múltiples identidades de firma desde varias cuentas de desarrollo de clientes puede ser un desafío cuando se trata de los llaveros del sistema en los corredores. La identidad de firma es un dato altamente confidencial , ya que nos permite codificar la aplicación, haciéndola vulnerable a posibles amenazas.

Para garantizar la seguridad, las identidades deben estar protegidas y protegidas en todos los proyectos. Sin embargo, este proceso puede convertirse en una pesadilla considerando la complejidad añadida que introduce macOS en su implementación de llavero.

  1. Desafíos en entornos multiproyectos.

No todos los proyectos se crearon utilizando las mismas herramientas, particularmente Xcode. Algunos proyectos, especialmente aquellos en fase de soporte, se mantuvieron utilizando la última versión de Xcode con la que se desarrolló el proyecto. Esto significa que si se requería algún trabajo en esos proyectos, la CI tenía que ser capaz de realizarlo. Como resultado, los corredores tenían que admitir múltiples versiones de Xcode al mismo tiempo, lo que efectivamente redujo la cantidad de corredores disponibles para un trabajo en particular.

5. Se requiere un esfuerzo adicional.

Cualquier cambio realizado en los corredores, como la instalación de software, se debe realizar en todos los corredores simultáneamente. Aunque teníamos una herramienta de automatización para esto, requirió un esfuerzo adicional para mantener los scripts de automatización.

Soluciones de infraestructura personalizadas para diversas necesidades de los clientes

Miquido es una empresa de software que trabaja con múltiples clientes con diferentes necesidades. Personalizamos nuestros servicios para satisfacer los requisitos específicos de cada cliente. A menudo alojamos el código base y la infraestructura necesaria para pequeñas empresas o nuevas empresas, ya que pueden carecer de los recursos o conocimientos para mantenerlo.

Los clientes empresariales suelen tener su propia infraestructura para alojar sus proyectos. Sin embargo, algunos no tienen capacidad para hacerlo o están obligados por regulaciones de la industria a utilizar su infraestructura. También prefieren no utilizar ningún servicio SaaS de terceros como Xcode Cloud o Codemagic. En cambio, quieren una solución que se ajuste a su arquitectura existente.

Para dar cabida a estos clientes, a menudo alojamos los proyectos en nuestra infraestructura o configuramos la misma configuración de iOS de integración continua en su infraestructura. Sin embargo, tenemos especial cuidado al tratar con información y archivos confidenciales, como la firma de identidades.

Aprovechando Fastlane para una gestión eficiente de la construcción

Aquí Fastlane se presenta como una herramienta útil. Consta de varios módulos llamados acciones que ayudan a agilizar el proceso y separarlo entre diferentes clientes. Una de estas acciones, denominada coincidencia, ayuda a mantener las identidades de firma de desarrollo y producción, así como los perfiles de aprovisionamiento. También funciona a nivel del sistema operativo para separar esas identidades en llaveros separados durante el tiempo de compilación y realiza una limpieza después de la compilación, lo cual es muy útil porque ejecutamos todas nuestras compilaciones en máquinas físicas.

Fastlane: herramienta de automatización del desarrollo
Créditos de imagen: Fastlane

Inicialmente recurrimos a Fastlane por una razón específica, pero descubrimos que tenía características adicionales que podrían sernos útiles.

  1. Carga de compilación en Testflight

En el pasado, la API AppStoreConnect no estaba disponible públicamente para los desarrolladores. Esto significaba que la única forma de cargar una compilación en Testflight era a través de Xcode o mediante Fastlane. Fastlane era una herramienta que básicamente eliminaba la API de ASC y la convertía en una acción llamada piloto . Sin embargo, este método a menudo fallaba con la siguiente actualización de Xcode. Si un desarrollador quería cargar su compilación en Testflight usando la línea de comando, Fastlane era la mejor opción disponible.

  1. Fácil cambio entre versiones de Xcode

Al tener más de una instancia de Xcode en una sola máquina, era necesario seleccionar qué Xcode usar para la compilación. Desafortunadamente, Apple hizo que fuera inconveniente cambiar entre versiones de Xcode; debe usar 'xcode-select' para hacerlo, lo que además requiere privilegios sudo. Fastlane también cubre eso.

  1. Utilidades adicionales para desarrolladores

Fastlane proporciona muchas otras utilidades útiles, incluido el control de versiones y la capacidad de enviar resultados de compilación a webhooks.

Las desventajas de Fastlane

Adaptar Fastlane a nuestros proyectos fue sólido y sólido, así que fuimos en esa dirección. Lo utilizamos con éxito durante varios años. Sin embargo, a lo largo de estos años, identificamos algunos problemas:

  1. Fastlane requiere conocimientos de Ruby.

Fastlane es una herramienta escrita en Ruby y requiere un buen conocimiento de Ruby para utilizarla de forma eficaz. Cuando hay errores en la configuración de Fastlane o en la propia herramienta, depurarlos usando irb o pry puede ser todo un desafío.

  1. Dependencia de numerosas gemas.

El propio Fastlane se basa en aproximadamente 70 gemas. Para mitigar los riesgos de romper el sistema Ruby, los proyectos utilizaban gemas de paquete local. Buscar todas estas gemas generó una gran cantidad de tiempo adicional.

  1. Problemas con el sistema Ruby y Rubygems.

Como resultado, todos los problemas con el sistema Ruby y rubygems mencionados anteriormente también son aplicables aquí.

  1. Redundancia para proyectos de Flutter.

Los proyectos de Flutter también se vieron obligados a utilizar fastlane match solo para preservar la compatibilidad con proyectos de iOS y proteger los llaveros de los corredores. Eso fue absurdamente innecesario, ya que Flutter tiene su propio sistema de compilación integrado y la sobrecarga mencionada anteriormente se introdujo solo para administrar identidades de firma y perfiles de aprovisionamiento.

La mayoría de estos problemas se solucionaron en el camino, pero necesitábamos una solución más sólida y confiable.

La idea: adaptar herramientas de integración continua nuevas y más sólidas para iOS

La buena noticia es que Apple ha obtenido control total sobre la arquitectura de su chip y ha desarrollado un nuevo marco de virtualización para macOS. Este marco permite a los usuarios crear, configurar y ejecutar máquinas virtuales Linux o macOS que se inician rápidamente y se caracterizan por un rendimiento nativo, y realmente me refiero a nativo.

Parecía prometedor y podría ser la piedra angular de nuestras nuevas herramientas de integración continua para iOS. Sin embargo, era sólo una parte de una solución completa. Al tener una herramienta de administración de VM, también necesitábamos algo que pudiera usar ese marco en coordinación con nuestros ejecutores de Gitlab.

Teniendo eso en cuenta, la mayoría de nuestros problemas relacionados con el bajo rendimiento de la virtualización quedarían obsoletos. También nos permitiría resolver automáticamente la mayoría de los problemas que pretendíamos resolver con Fastlane.

Desarrollar una solución personalizada para la gestión de identidades de firma bajo demanda

Teníamos un último problema que resolver: la gestión de la identidad de firma. No queríamos utilizar Fastlane para esto porque parecía excesivo para nuestras necesidades. En cambio, buscábamos una solución que se adaptara mejor a nuestras necesidades. Nuestras necesidades eran sencillas: el proceso de gestión de identidades tenía que realizarse bajo demanda, exclusivamente durante el tiempo de construcción, sin identidades preinstaladas en el llavero y ser compatible con cualquier máquina en la que se ejecutara.

El problema de distribución y la falta de una API AppstoreConnect estable quedaron obsoletos cuando Apple lanzó su "altool", que permitía la comunicación entre los usuarios y ASC.

Entonces teníamos una idea y teníamos que encontrar una manera de conectar esos tres aspectos:

  1. Encontrar una manera de utilizar el marco de virtualización de Apple.
  2. Hacerlo funcionar con corredores de Gitlab.
  3. Encontrar una solución para la gestión de identidades de firma en múltiples proyectos y ejecutores.

La solución: un vistazo a nuestro enfoque (herramientas incluidas)

Comenzamos a buscar soluciones para abordar todos los problemas mencionados anteriormente.

  1. Utilizando el marco de virtualización de Apple.

Para el primer obstáculo, encontramos una solución bastante rápido: nos topamos con la herramienta tarta de Cirrus Labs. Desde el primer momento supimos que esta iba a ser nuestra elección.

Las ventajas más importantes de utilizar la herramienta para tartas que ofrece Cirrus Lab son:

  • La posibilidad de crear máquinas virtuales a partir de imágenes .ipsw sin formato.
  • La posibilidad de crear máquinas virtuales utilizando plantillas preempaquetadas (con algunas herramientas de utilidad instaladas, como Brew o Xcode), disponible en la página de GitHub de Cirrus Labs.
  • La herramienta Tart utiliza el empaquetador para soportar la creación dinámica de imágenes.
  • La herramienta Tart admite imágenes de Linux y MacOS.
  • La herramienta utiliza una característica sobresaliente del sistema de archivos APFS que permite la duplicación de archivos sin reservar espacio en disco para ellos. De esta manera, no necesita asignar espacio en disco para 3 veces el tamaño de la imagen original. Solo necesita suficiente espacio en disco para la imagen original, mientras que el clon ocupa solo el espacio que es una diferencia entre este y la imagen original. Esto es increíblemente útil, especialmente porque las imágenes de macOS tienden a ser bastante grandes.

Por ejemplo, una imagen operativa de macOS Ventura con Xcode y otras utilidades instaladas requiere un mínimo de 60 GB de espacio en disco. En circunstancias normales, una imagen y dos de sus clones ocuparían hasta 180 GB de espacio en disco, lo cual es una cantidad significativa. Y esto es sólo el comienzo, ya que es posible que quieras tener más de una imagen original o instalar varias versiones de Xcode en una sola VM, lo que aumentaría aún más el tamaño.

  • La herramienta permite la gestión de direcciones IP para máquinas virtuales originales y clonadas, lo que permite el acceso SSH a las máquinas virtuales.
  • La capacidad de montar directorios de forma cruzada entre la máquina host y las máquinas virtuales.
  • La herramienta es fácil de usar y tiene una CLI muy sencilla.

No hay casi nada que le falte a esta herramienta en términos de utilizarla para la gestión de VM. Casi nada, excepto por una cosa: aunque prometedor, el complemento del paquete para crear imágenes sobre la marcha consumía demasiado tiempo, por lo que decidimos no usarlo.

Probamos la tarta y funcionó fantásticamente. Su rendimiento era similar al nativo y la gestión era sencilla.

Después de haber integrado tarta con éxito con resultados impresionantes, a continuación nos centramos en abordar otros desafíos.

  1. Encontrar una manera de combinar tarta con corredores de Gitlab.

Después de resolver el primer problema, nos enfrentamos a la cuestión de cómo combinar tart con corredores de Gitlab.

Comencemos describiendo lo que realmente hacen los corredores de Gitlab:

El diagrama simplificado de la delegación de trabajos de Gitlab. Sistema CI para iOS

Necesitábamos incluir un rompecabezas adicional en el diagrama, que implicaba asignar tareas desde el host del ejecutor a la VM. El trabajo de GitLab es un script de shell que contiene variables, entradas de RUTA y comandos cruciales.

Nuestro objetivo era transferir este script a la VM y ejecutarlo.

Sin embargo, esta tarea resultó ser más desafiante de lo que pensábamos inicialmente.

El corredor

Los ejecutores de ejecución estándar de Gitlab, como Docker o SSH, son fáciles de configurar y requieren poca o ninguna configuración. Sin embargo, necesitábamos un mayor control sobre la configuración, lo que nos llevó a explorar ejecutores personalizados proporcionados por GitLab.

Los ejecutores personalizados son una excelente opción para configuraciones no estándar, ya que cada paso del ejecutor (preparación, ejecución, limpieza) se describe en forma de script de shell. Lo único que faltaba era una herramienta de línea de comandos que pudiera realizar las tareas que necesitábamos y ejecutarse en scripts de configuración del ejecutor.

Actualmente, hay un par de herramientas disponibles que hacen exactamente eso, por ejemplo, el ejecutor de tartas Gitlab de CirrusLabs. Esta herramienta es precisamente lo que buscábamos en ese momento. Sin embargo, aún no existía y después de realizar investigaciones no encontramos ninguna herramienta que pudiera ayudarnos a realizar nuestra tarea.

Escribiendo la propia solución

Como no pudimos encontrar una solución perfecta, escribimos una nosotros mismos. ¡Somos ingenieros, después de todo! La idea parecía sólida y teníamos todas las herramientas necesarias, así que procedimos con el desarrollo.

Hemos elegido utilizar Swift y un par de bibliotecas de código abierto proporcionadas por Apple: Swift Argument Parser para manejar la ejecución de la línea de comandos y Swift NIO para manejar la conexión SSH con las máquinas virtuales. Comenzamos el desarrollo y, en un par de días, obtuvimos el primer prototipo funcional de una herramienta que finalmente evolucionó hasta convertirse en MQVMRunner.

Infraestructura de CI de iOS: MQVMRunner

En un nivel alto, la herramienta funciona de la siguiente manera:

  1. (Preparar paso)
    1. Lea las variables proporcionadas en gitlab-ci.yml (nombre de la imagen y variables adicionales).
    2. Elija la base de VM solicitada
    3. Clonar la base de VM solicitada.
    4. Configure un directorio de montaje cruzado y copie el script del trabajo de Gitlab, estableciendo los permisos necesarios para ello.
    5. Ejecute el clon y verifique la conexión SSH.
    6. Configure las dependencias necesarias (como la versión de Xcode), si es necesario.
  2. (Ejecutar paso)
    1. Ejecute el trabajo de Gitlab ejecutando un script desde un directorio montado en forma cruzada en un clon de VM preparado a través de SSH.
  3. (Paso de limpieza)
    1. Eliminar imagen clonada.

Desafíos en el desarrollo

Durante el desarrollo, encontramos varios problemas que hicieron que no fuera tan bien como nos hubiera gustado.

  1. Gestión de direcciones IP.

La gestión de direcciones IP es una tarea crucial que debe realizarse con cuidado. En el prototipo, el manejo de SSH se implementó mediante comandos de shell SSH directos y codificados. Sin embargo, en el caso de shells no interactivos, se recomienda la autenticación con clave. Además, es recomendable agregar el host al archivo conocido_hosts para evitar interrupciones. Sin embargo, debido a la gestión dinámica de las direcciones IP de las máquinas virtuales, existe la posibilidad de duplicar la entrada para una determinada IP, lo que genera errores. Por lo tanto, debemos asignar los hosts_conocidos dinámicamente para un trabajo en particular para evitar este tipo de problemas.

  1. Solución pura Swift.

Teniendo en cuenta eso, y el hecho de que los comandos de shell codificados en el código Swift no son realmente elegantes, pensamos que sería bueno usar una biblioteca Swift dedicada y decidimos optar por Swift NIO. Resolvimos algunos problemas pero, al mismo tiempo, introdujimos un par de nuevos como, por ejemplo, a veces los registros colocados en la salida estándar se transfirieron *después* de que se finalizó el canal SSH debido a que el comando finalizó la ejecución y, como nos estábamos basando en ese resultado en el trabajo posterior, la ejecución fallaba aleatoriamente.

  1. Selección de versión de Xcode.

Debido a que el complemento Packer no era una opción para la creación de imágenes dinámicas debido al consumo de tiempo, decidimos utilizar una única base de VM con múltiples versiones de Xcode preinstaladas. Tuvimos que encontrar una manera para que los desarrolladores especificaran la versión de Xcode que necesitan en su gitlab-ci.yml, y hemos creado variables personalizadas disponibles para usar en cualquier proyecto. MQVMRunner luego ejecutará `xcode-select` en una VM clonada para configurar la versión de Xcode correspondiente.

Y muchos muchos mas

Optimización de la migración de proyectos y la integración continua del flujo de trabajo de iOS con Mac Studios

Lo configuramos en dos nuevos Mac Studios y comenzamos a migrar los proyectos. Queríamos que el proceso de migración para nuestros desarrolladores fuera lo más transparente posible. No pudimos hacerlo completamente fluido, pero finalmente llegamos al punto en el que solo tenían que hacer un par de cosas en gitlab-ci.yml:

  • Las etiquetas de los corredores: utilizar Mac Studios en lugar de Intel.
  • El nombre de la imagen: parámetro opcional, introducido para futura compatibilidad en caso de que necesitemos más de una VM base. En este momento, siempre utiliza de forma predeterminada la VM base única que tenemos.
  • La versión de Xcode: parámetro opcional; si no se proporciona, se utilizará la versión más reciente disponible.

La herramienta obtuvo muy buenos comentarios iniciales, por lo que hemos decidido hacerla de código abierto. Hemos agregado un script de instalación para configurar Gitlab Custom Runner y todas las acciones y variables requeridas. Con nuestra herramienta, puede configurar su propio ejecutor GitLab en cuestión de minutos; lo único que necesita es la máquina virtual inicial y base en la que se ejecutarán los trabajos.

La estructura final de integración continua para iOS tiene el siguiente aspecto:

La infraestructura de CI final: MQVMRunner

3. Solución para una gestión de identidad eficiente

Hemos estado luchando por encontrar una solución eficiente para gestionar las identidades de firma de nuestros clientes. Esto fue particularmente desafiante, ya que la identidad de firma es información altamente confidencial que no debe almacenarse en un lugar no seguro por más tiempo del necesario.

Además, queríamos cargar estas identidades solo durante el tiempo de compilación, sin ninguna solución entre proyectos. Esto significaba que no se debería poder acceder a la identidad fuera del entorno limitado de la aplicación (o compilación). Ya hemos abordado este último problema mediante la transición a máquinas virtuales. Sin embargo, todavía necesitábamos encontrar una manera de almacenar y cargar la identidad de firma en la VM solo durante el tiempo de compilación.

Problemas con Fastlane Match

En ese momento, todavía estábamos usando Fastlane Match, que almacena identidades y provisiones cifradas en un repositorio separado, las carga durante el proceso de compilación en una instancia de llavero separada y elimina esa instancia después de la compilación.

Este enfoque parece conveniente, pero tiene algunos problemas:

  • Requiere toda la configuración de Fastlane para funcionar.

Fastlane es rubygem y todos los problemas enumerados en el primer capítulo se aplican aquí.

  • Pago del repositorio en el momento de la compilación.

Mantuvimos nuestras identidades en un repositorio separado que se revisó durante el proceso de compilación en lugar del proceso de configuración. Esto significó que tuvimos que establecer un acceso separado al repositorio de identidades, no solo para Gitlab, sino también para los corredores específicos, de manera similar a cómo manejaríamos las dependencias privadas de terceros.

  • Difícil de gestionar fuera de Match.

Si utiliza Match para gestionar identidades o aprovisionamiento, hay poca o ninguna necesidad de intervención manual. Editar, descifrar y cifrar perfiles manualmente para que las coincidencias puedan seguir funcionando con ellos más adelante es tedioso y requiere mucho tiempo. El uso de Fastlane para llevar a cabo este proceso generalmente resulta en la eliminación completa de la configuración de aprovisionamiento de la aplicación y la creación de una nueva.

  • Un poco difícil de depurar.

En caso de cualquier problema de firma de código, puede resultarle difícil determinar la identidad y la coincidencia de aprovisionamiento que se acaba de instalar, ya que primero deberá decodificarlos.

  • Preocupaciones de seguridad.

Haga coincidir las cuentas de desarrollador a las que se accedió utilizando las credenciales proporcionadas para realizar cambios en su nombre. A pesar de que Fastlane es de código abierto, algunos clientes lo rechazaron por motivos de seguridad.

  • Por último, pero no menos importante, deshacernos de Match eliminaría el mayor obstáculo en nuestro camino para deshacernos de Fastlane por completo.

Nuestros requisitos iniciales fueron los siguientes:

  • La carga requiere firmar la identidad desde un lugar seguro, preferiblemente en formato no plano, y colocarla en el llavero.
  • Xcode debería poder acceder a esa identidad.
  • Preferiblemente, las variables de contraseña de identidad, nombre de llavero y contraseña de llavero deben poder configurarse para fines de depuración.

Match tenía todo lo que necesitábamos, pero implementar Fastlane solo para usar Match parecía una exageración, especialmente para soluciones multiplataforma con su propio sistema de compilación. Queríamos algo similar a Match, pero sin la pesada carga de Ruby que llevaba.

Creando la propia solución

Entonces pensamos: ¡escribámoslo nosotros mismos! Lo hicimos con MQVMRunner, por lo que también podríamos hacerlo aquí. También elegimos Swift para hacerlo, principalmente porque podíamos obtener muchas API necesarias de forma gratuita utilizando el marco de seguridad de Apple.

Por supuesto, tampoco todo salió tan bien como se esperaba.

  • Marco de seguridad implementado.

La estrategia más sencilla fue llamar a los comandos bash como lo hace Fastlane. Sin embargo, al tener el marco de seguridad disponible, pensamos que sería más elegante usarlo para el desarrollo.

  • Falta de experiencia.

No teníamos mucha experiencia con el marco de seguridad para macOS y resultó que difería significativamente de lo que estábamos acostumbrados en iOS. Esto nos ha salido por la culata en muchos casos en los que no éramos conscientes de las limitaciones de macOS o asumimos que funciona igual que en iOS; la mayoría de esas suposiciones eran erróneas.

  • Pésima documentación.

La documentación del marco de seguridad de Apple es, por decirlo suavemente, humilde. Es una API muy antigua que se remonta a las primeras versiones de OSX y, en ocasiones, teníamos la impresión de que no se había actualizado desde entonces. Una gran parte del código no está documentada, pero anticipamos cómo funciona leyendo el código fuente. Afortunadamente para nosotros, es de código abierto.

  • Depreciaciones sin reemplazos.

Una buena parte de este marco está en desuso; Apple está intentando alejarse del típico llavero “estilo macOS” (múltiples llaveros accesibles mediante contraseña) e implementar el llavero “estilo iOS” (llavero único, sincronizado a través de iCloud). Así que lo dejaron obsoleto en macOS Yosemite en 2014, pero no encontraron ningún reemplazo en los últimos nueve años, por lo que la única API disponible para nosotros, por ahora, está obsoleta porque todavía no hay ninguna nueva.

Supusimos que las identidades de firma se pueden almacenar como cadenas codificadas en base64 en variables de Gitlab por proyecto. Es seguro, se basa en cada proyecto y, si se configura como una variable enmascarada, se puede leer y mostrar en los registros de compilación como texto no plano.

Entonces, teníamos los datos de identidad. Sólo nos faltaba meterlo en el llavero. Uso de la API de seguridad Después de varios intentos y una pelea revisando la documentación del marco de seguridad, preparamos un prototipo de algo que luego se convirtió en MQSwiftSign.

Aprendiendo el sistema de seguridad macOS, pero por las malas

Tuvimos que adquirir un conocimiento profundo de cómo funciona el llavero de macOS para desarrollar nuestra herramienta. Esto implicó investigar cómo el llavero gestiona los elementos, su acceso y permisos, y la estructura de los datos del llavero. Por ejemplo, descubrimos que el llavero es el único archivo de macOS cuyo sistema operativo ignora el conjunto de ACL. Además, aprendimos que las ACL en elementos específicos del llavero son una lista de texto sin formato guardada en un archivo de llavero. Enfrentamos varios desafíos a lo largo del camino, pero también aprendimos mucho.

Un desafío importante que encontramos fueron las indicaciones. Nuestra herramienta fue diseñada principalmente para ejecutarse en sistemas CI iOS, lo que significaba que no tenía que ser interactiva. No pudimos pedir a los usuarios que confirmaran una contraseña en el CI.

Sin embargo, el sistema de seguridad de macOS está bien diseñado, lo que hace imposible editar o leer información confidencial, incluida la identidad de firma, sin el permiso explícito del usuario. Para acceder a un recurso sin confirmación, el programa que accede debe estar incluido en la Lista de control de acceso del recurso. Este es un requisito estricto que ningún programa puede infringir, ni siquiera los programas de Apple que vienen con el sistema. Si algún programa necesita leer o editar una entrada de llavero, el usuario debe proporcionar una contraseña de llavero para desbloquearla y, opcionalmente, agregarla a la ACL de la entrada.

Superar los desafíos de permisos de usuario

Entonces, tuvimos que encontrar una manera para que Xcode accediera a una identidad configurada por nuestro llavero sin pedir permiso al usuario mediante la solicitud de contraseña. Para hacerlo, podemos cambiar la lista de control de acceso de un elemento, pero eso también requiere permiso del usuario y, por supuesto, lo necesita. De lo contrario, socavaría el objetivo de tener la ACL. Hemos estado tratando de eludir esa protección; intentamos lograr el mismo efecto que con el comando `security set-key-partition-list`.

Después de profundizar en la documentación del marco, no hemos encontrado ninguna API que permita editar la ACL sin solicitar al usuario que proporcione una contraseña. Lo más parecido que encontramos es `SecKeychainItemSetAccess`, que activa un mensaje de interfaz de usuario cada vez. Luego nos sumergimos de nuevo, pero esta vez en la mejor documentación, que es el código fuente mismo. ¿Cómo lo implementó Apple?

Resultó que, como era de esperar, estaban utilizando una API privada. Un método llamado `SecKeychainItemSetAccessWithPassword` hace básicamente lo mismo que `SecKeychainItemSetAccess`, pero en lugar de solicitar una contraseña al usuario, la contraseña se proporciona como argumento para una función. Por supuesto, como API privada, no aparece en la documentación, pero Apple carece de documentación para dichas API como si no pudieran pensar en crear una aplicación para uso personal o empresarial. Como la herramienta estaba destinada únicamente a uso interno, no dudamos en utilizar la API privada. Lo único que había que hacer era unir el método C con Swift.

Superar los desafíos de permisos de usuario

Así, el flujo de trabajo final del prototipo fue el siguiente:

  1. Cree el llavero desbloqueado temporal con el bloqueo automático desactivado.
  2. Obtenga y decodifique los datos de identidad de firma codificados en base64 a partir de variables ambientales (pasadas por Gitlab).
  3. Importe la identidad al llavero creado.
  4. Configure las opciones de acceso adecuadas para la identidad importada para que Xcode y otras herramientas puedan leerla para el diseño del código.

Actualizaciones adicionales

El prototipo funcionaba bien, por lo que identificamos algunas características adicionales que nos gustaría agregar a la herramienta. Nuestro objetivo era eventualmente reemplazar fastlane; Ya hemos implementado la acción "partido". Sin embargo, fastlane todavía ofrecía dos características valiosas que aún no teníamos: instalación de perfiles de aprovisionamiento y creación de export.plist.

Instalación del perfil de aprovisionamiento

La instalación del perfil de aprovisionamiento es bastante sencilla: se divide en extraer el UUID del perfil y copiar el archivo a `~/Library/MobileDevice/Provisioning Profiles/` con UUID como nombre de archivo, y eso es suficiente para que Xcode lo vea correctamente. No es una ciencia espacial agregar a nuestra herramienta un complemento simple para recorrer el directorio proporcionado y hacerlo para cada archivo .mobileprovision que encuentre dentro.

Creación de Export.plist

La creación de export.plist, sin embargo, es un poco más complicada. Para generar un archivo IPA adecuado, Xcode requiere que los usuarios proporcionen un archivo plist con información específica recopilada de varias fuentes: el archivo del proyecto, plist de derechos, configuración del espacio de trabajo, etc. La razón por la cual Xcode solo puede recopilar esos datos a través del asistente de distribución, pero no a través de la CLI me resulta desconocido. Sin embargo, debíamos recopilarlos utilizando las API de Swift, teniendo solo referencias de proyectos/espacios de trabajo y una pequeña dosis de conocimiento sobre cómo se construye el archivo de proyecto Xcode.

El resultado fue mejor de lo esperado, por lo que decidimos agregarlo como un complemento más a nuestra herramienta. También lo lanzamos como un proyecto de código abierto para una audiencia más amplia. En este momento, MQSwiftSign es una herramienta multipropósito que se puede usar con éxito como reemplazo de las acciones rápidas básicas necesarias para crear y distribuir su aplicación iOS y la usamos en todos nuestros proyectos en Miquido.

Pensamientos finales: el éxito

Cambiar de la arquitectura Intel a iOS ARM fue una tarea desafiante. Nos enfrentamos a numerosos obstáculos y dedicamos mucho tiempo a desarrollar herramientas debido a la falta de documentación. Sin embargo, finalmente establecimos un sistema sólido:

  • Dos corredores para gestionar en lugar de nueve;
  • Ejecutar software que está completamente bajo nuestro control, sin una gran cantidad de gastos generales en forma de rubygems: pudimos deshacernos de fastlane o cualquier software de terceros en nuestras configuraciones de compilación;
  • MUCHO conocimiento y comprensión de cosas a las que normalmente no prestamos atención, como la seguridad del sistema macOS y el marco de seguridad en sí, una estructura de proyecto Xcode real y muchos, muchos más.

Con mucho gusto lo aliento: si tiene dificultades para configurar su ejecutor GitLab para compilaciones de iOS, pruebe nuestro MQVMRunner. Si necesita ayuda para crear y distribuir su aplicación utilizando una única herramienta y no quiere depender de Rubygems, pruebe MQSwiftSign. ¡Funciona para mí, también puede funcionar para ti!