Systèmes CI pour le développement iOS : transformation d'Intel vers ARM

Publié: 2024-02-14

Dans le paysage technologique en constante évolution, les entreprises doivent s’adapter aux vents du changement pour rester pertinentes et compétitives. L'une de ces transformations qui a pris d'assaut le monde de la technologie est la transition de l'architecture Intel x86_64 à l'architecture iOS ARM, illustrée par la puce révolutionnaire Apple M1 d'Apple. Dans ce contexte, les systèmes CI pour iOS sont devenus un élément crucial pour les entreprises qui évoluent dans cette transition, garantissant que les processus de développement et de test de logiciels restent efficaces et à jour avec les dernières normes technologiques.

Apple a annoncé ses puces M1 il y a près de trois ans, et depuis lors, il est clair que la société adopterait l'architecture ARM et abandonnerait éventuellement la prise en charge des logiciels basés sur Intel. Pour maintenir la compatibilité entre les architectures, Apple a introduit une nouvelle version de Rosetta, son framework de traduction binaire propriétaire, qui s'est avéré fiable dans le passé lors de l'importante transformation de l'architecture de PowerPC à Intel en 2006. La transformation est toujours en cours, et nous l'avons vu Xcode perd son support Rosetta dans la version 14.3.

Chez Miquido, nous avons reconnu il y a quelques années la nécessité de migrer d'Intel vers ARM. Nous avons commencé les préparatifs au milieu de l'année 2021. En tant qu'éditeur de logiciels avec plusieurs clients, applications et projets en cours simultanément, nous avons été confrontés à quelques défis que nous avons dû surmonter. Cet article peut être votre guide pratique si votre entreprise est confrontée à des situations similaires. Les situations et solutions décrites sont présentées du point de vue du développement iOS, mais vous pouvez également trouver des informations adaptées à d'autres technologies. Un élément essentiel de notre stratégie de transition consistait à garantir que notre système CI pour iOS était entièrement optimisé pour la nouvelle architecture, en soulignant l'importance de Les systèmes CI pour iOS pour maintenir des flux de travail efficaces et des résultats de haute qualité dans un contexte de changement aussi important.

Le problème : migration de l'architecture Intel vers iOS ARM

Apple a sorti sa puce M1 en 2020

Le processus de migration était divisé en deux branches principales.

1. Remplacement des ordinateurs des développeurs Intel existants par de nouveaux Macbooks M1

Ce processus était censé être relativement simple. Nous avons établi une politique de remplacement progressif de tous les Macbooks Intel du développeur sur deux ans. À l'heure actuelle, 95 % de nos employés utilisent des Macbooks basés sur ARM.

Cependant, nous avons rencontré des défis inattendus au cours de ce processus. Mi-2021, une pénurie de Mac M1 a ralenti notre processus de remplacement. Fin 2021, nous n’avions pu remplacer qu’une poignée de Macbooks sur les près de 200 en attente. Nous avons estimé qu'il faudrait environ deux ans pour remplacer complètement tous les Mac Intel de l'entreprise par des Macbooks M1, y compris les ingénieurs non iOS.

Heureusement, Apple a sorti ses nouvelles puces M1 Pro et M2. En conséquence, nous sommes passés du remplacement des Intel par des Mac M1 à leur remplacement par des puces M1 Pro et M2.

Le logiciel non prêt pour le changement a provoqué la frustration des développeurs

Les premiers ingénieurs qui ont reçu les nouveaux Macbooks M1 ont eu du mal car la plupart des logiciels n'étaient pas prêts à passer à la nouvelle architecture iOS ARM d'Apple. Les outils tiers comme Rubygems et Cocoapods, qui sont des outils de gestion des dépendances qui s'appuient sur de nombreux autres Rubygems, ont été les plus touchés. Certains de ces outils n'étaient pas compilés pour l'architecture iOS ARM à l'époque, la plupart des logiciels devaient donc être exécutés à l'aide de Rosetta, entraînant des problèmes de performances et de la frustration.

Cependant, les créateurs de logiciels se sont efforcés de résoudre la plupart de ces problèmes au fur et à mesure qu’ils se présentaient. Le moment décisif est survenu avec la sortie de Xcode 14.3, qui n’était plus pris en charge par Rosetta. C'était un signal clair à tous les développeurs de logiciels qu'Apple faisait pression pour une migration de l'architecture Intel vers iOS ARM. Cela a obligé la plupart des développeurs de logiciels tiers qui comptaient auparavant sur Rosetta à migrer leurs logiciels vers ARM. De nos jours, 99 % des logiciels tiers utilisés quotidiennement chez Miquido fonctionnent sans Rosetta.

2. Remplacement du système CI de Miquido pour iOS

Remplacer le système iOS d'intégration continue chez Miquido s'est avéré être une tâche plus compliquée que le simple échange de machines. Tout d'abord, jetez un œil à notre infrastructure à l'époque :

L'architecture CI chez Miquido. Système CI pour la transformation iOS.

Nous avions une instance cloud Gitlab et 9 Mac Mini basés sur Intel qui y étaient connectés. Ces machines servaient d'exécuteurs de tâches et Gitlab était responsable de l'orchestration. Chaque fois qu'une tâche CI était mise en file d'attente, Gitlab l'attribuait au premier exécuteur disponible qui répondait aux exigences du projet spécifiées dans le fichier gitlab-ci.yml. Gitlab créerait un script de travail contenant toutes les commandes de construction, variables, chemins, etc. Ce script était ensuite déplacé sur le coureur et exécuté sur cette machine.

Bien que cette configuration puisse paraître robuste, nous avons été confrontés à des problèmes de virtualisation dus à la mauvaise prise en charge des processeurs Intel. En conséquence, nous avons décidé de ne pas utiliser de virtualisation telle que Docker et d'exécuter les tâches sur les machines physiques elles-mêmes. Nous avons essayé de mettre en place une solution efficace et fiable basée sur Docker, mais les limitations de la virtualisation telles que le manque d'accélération GPU entraînaient que les tâches prenaient deux fois plus de temps à s'exécuter que sur les machines physiques. Cela a entraîné davantage de frais généraux et un remplissage rapide des files d'attente.

En raison du SLA macOS, nous n'avons pu configurer que deux VM simultanément. Par conséquent, nous avons décidé d'étendre le pool d'exécuteurs physiques et de les configurer pour exécuter des tâches Gitlab directement sur leur système d'exploitation. Cependant, cette approche présentait également quelques inconvénients.

Défis liés au processus de construction et à la gestion des coureurs

  1. Aucune isolation des builds en dehors du sandbox du répertoire de build.

Le programme d'exécution exécute chaque build sur une machine physique, ce qui signifie que les builds ne sont pas isolées du bac à sable du répertoire de build. Cela a ses avantages et ses inconvénients. D'une part, nous pouvons utiliser les caches système pour accélérer les builds puisque la plupart des projets utilisent le même ensemble de dépendances tierces.

D'un autre côté, le cache devient impossible à maintenir puisque les restes d'un projet peuvent affecter tous les autres projets. Ceci est particulièrement important pour les caches à l’échelle du système, car les mêmes exécuteurs sont utilisés pour le développement Flutter et React Native. React Native, en particulier, nécessite de nombreuses dépendances mises en cache via NPM.

  1. Gâchis potentiel des outils système.

Bien qu'aucune tâche n'ait été exécutée avec les privilèges sudo, il leur était toujours possible d'accéder à certains outils système ou utilisateur, tels que Ruby. Cela représentait une menace potentielle de rupture de certains de ces outils, d'autant plus que macOS utilise Ruby pour certains de ses logiciels existants, y compris certaines fonctionnalités Xcode héritées. La version système de Ruby n’est pas quelque chose avec laquelle vous voudriez jouer.

Cependant, l’introduction de rbenv crée une autre couche de complexité à gérer. Il est important de noter que les Rubygems sont installés par version de Ruby et que certaines de ces gemmes nécessitent des versions spécifiques de Ruby. Presque tous les outils tiers que nous utilisions dépendaient de Ruby, Cocoapods et Fastlane étant les principaux acteurs.

  1. Gestion des identités de signature.

La gestion de plusieurs identités de signature à partir de différents comptes de développement clients peut être un défi lorsqu'il s'agit des trousseaux système sur les coureurs. L'identité de signature est une donnée très sensible car elle nous permet de co-concevoir l'application, la rendant vulnérable aux menaces potentielles.

Pour garantir la sécurité, les identités doivent être mises en bac à sable dans tous les projets et protégées. Cependant, ce processus peut devenir un cauchemar compte tenu de la complexité supplémentaire introduite par macOS dans la mise en œuvre de son trousseau.

  1. Défis dans les environnements multi-projets.

Tous les projets n'ont pas été créés avec les mêmes outils, notamment Xcode. Certains projets, notamment ceux en phase de support, ont été maintenus en utilisant la dernière version de Xcode avec laquelle le projet a été développé. Cela signifie que si des travaux étaient nécessaires sur ces projets, l'IC devait être capable de les réaliser. En conséquence, les exécuteurs devaient prendre en charge plusieurs versions de Xcode en même temps, ce qui réduisait effectivement le nombre d'exécuteurs disponibles pour une tâche particulière.

5. Effort supplémentaire requis.

Toutes les modifications apportées aux coureurs, telles que l'installation de logiciels, doivent être effectuées simultanément sur tous les coureurs. Même si nous disposions d'un outil d'automatisation pour cela, la maintenance des scripts d'automatisation nécessitait des efforts supplémentaires.

Solutions d'infrastructure personnalisées pour les divers besoins des clients

Miquido est un éditeur de logiciels qui travaille avec plusieurs clients ayant des besoins différents. Nous personnalisons nos services pour répondre aux exigences spécifiques de chaque client. Nous hébergeons souvent la base de code et l'infrastructure nécessaire pour les petites entreprises ou les start-ups, car elles peuvent manquer de ressources ou de connaissances pour la maintenir.

Les entreprises clientes disposent généralement de leur propre infrastructure pour héberger leurs projets. Cependant, certains n’ont pas la capacité de le faire ou sont obligés par la réglementation industrielle d’utiliser leurs infrastructures. Ils préfèrent également ne pas utiliser de services SaaS tiers comme Xcode Cloud ou Codemagic. Ils souhaitent plutôt une solution adaptée à leur architecture existante.

Pour accueillir ces clients, nous hébergeons souvent les projets sur notre infrastructure ou mettons en place la même configuration iOS d'intégration continue sur leur infrastructure. Cependant, nous prenons des précautions particulières lorsque nous traitons des informations et des fichiers sensibles, comme la signature d'identités.

Tirer parti de Fastlane pour une gestion efficace des builds

Ici, Fastlane se présente comme un outil pratique. Il se compose de divers modules appelés actions qui aident à rationaliser le processus et à le séparer entre les différents clients. L'une de ces actions, appelée correspondance, permet de conserver les identités de signature de développement et de production, ainsi que les profils d'approvisionnement. Il fonctionne également au niveau du système d'exploitation pour séparer ces identités dans des trousseaux distincts pendant la période de construction et effectue un nettoyage après la construction, ce qui est très utile car nous exécutons toutes nos versions sur des machines physiques.

Fastlane : outil d'automatisation du développement
Crédits image : Voie rapide

Nous nous sommes initialement tournés vers Fastlane pour une raison précise, mais avons découvert qu'il disposait de fonctionnalités supplémentaires qui pourraient nous être utiles.

  1. Créer un téléchargement vers Testflight

Dans le passé, l'API AppStoreConnect n'était pas accessible au public pour les développeurs. Cela signifiait que le seul moyen de télécharger une version sur Testflight était via Xcode ou en utilisant Fastlane. Fastlane était un outil qui supprimait essentiellement l'API ASC et la transformait en une action appelée pilot . Cependant, cette méthode était souvent interrompue avec la prochaine mise à jour de Xcode. Si un développeur souhaitait télécharger sa version sur Testflight à l'aide de la ligne de commande, Fastlane était la meilleure option disponible.

  1. Basculement facile entre les versions de Xcode

Ayant plus d'une instance Xcode sur une seule machine, il était nécessaire de sélectionner quel Xcode utiliser pour la construction. Malheureusement, Apple a rendu difficile le basculement entre les versions de Xcode – vous devez utiliser « xcode-select » pour le faire, ce qui nécessite en outre les privilèges sudo. Fastlane couvre également cela.

  1. Utilitaires supplémentaires pour les développeurs

Fastlane fournit de nombreux autres utilitaires utiles, notamment la gestion des versions et la possibilité de soumettre les résultats de build aux webhooks.

Les inconvénients de Fastlane

Adapter Fastlane à nos projets était judicieux et solide, nous sommes donc allés dans cette direction. Nous l'avons utilisé avec succès pendant plusieurs années. Cependant, au fil de ces années, nous avons identifié quelques problèmes :

  1. Fastlane nécessite des connaissances Ruby.

Fastlane est un outil écrit en Ruby et nécessite une bonne connaissance de Ruby pour l'utiliser efficacement. Lorsqu'il y a des bugs dans votre configuration Fastlane ou dans l'outil lui-même, les déboguer à l'aide d'irb ou de pry peut être assez difficile.

  1. Dépendance à de nombreuses pépites.

Fastlane lui-même s'appuie sur environ 70 gemmes. Pour atténuer les risques de rupture du système Ruby, les projets utilisaient des gemmes de bundle locales. La récupération de toutes ces gemmes créait beaucoup de temps.

  1. Problèmes de système Ruby et Rubygems.

En conséquence, tous les problèmes liés au système Ruby et Rubygems mentionnés précédemment sont également applicables ici.

  1. Redondance pour les projets Flutter.

Les projets Flutter ont également été contraints d'utiliser Fastlane Match uniquement pour préserver la compatibilité avec les projets iOS et protéger les trousseaux de coureurs. C'était absurdement inutile, car Flutter dispose de son propre système de construction intégré et la surcharge mentionnée précédemment n'a été introduite que pour gérer les identités de signature et les profils d'approvisionnement.

La plupart de ces problèmes ont été résolus en cours de route, mais nous avions besoin d'une solution plus robuste et plus fiable.

L'idée : adapter de nouveaux outils d'intégration continue plus robustes pour iOS

La bonne nouvelle est qu'Apple a pris le contrôle total de l'architecture de sa puce et a développé un nouveau cadre de virtualisation pour macOS. Ce framework permet aux utilisateurs de créer, configurer et exécuter des machines virtuelles Linux ou macOS qui démarrent rapidement et se caractérisent par des performances de type natif – et je veux vraiment dire de type natif.

Cela semblait prometteur et pourrait constituer la pierre angulaire de nos nouveaux outils d’intégration continue pour iOS. Cependant, ce n’était qu’une partie d’une solution complète. Ayant un outil de gestion de VM, nous avions également besoin de quelque chose qui puisse utiliser ce framework en coordination avec nos exécuteurs Gitlab.

Ainsi, la plupart de nos problèmes liés aux mauvaises performances de virtualisation deviendraient obsolètes. Cela nous permettrait également de résoudre automatiquement la plupart des problèmes que nous avions l'intention de résoudre avec Fastlane.

Développer une solution sur mesure pour la gestion des identités de signature à la demande

Nous avions un dernier problème à résoudre : la gestion des identités de signature. Nous ne voulions pas utiliser Fastlane pour cela car cela semblait excessif pour nos besoins. Nous recherchions plutôt une solution mieux adaptée à nos besoins. Nos besoins étaient simples : le processus de gestion des identités devait être effectué à la demande, exclusivement pendant la durée de la construction, sans aucune identité préinstallée sur le trousseau, et être compatible avec n'importe quelle machine sur laquelle il fonctionnerait.

Le problème de distribution et le manque d'API AppstoreConnect stable sont devenus obsolètes lorsque Apple a publié son « altool », qui permettait la communication entre les utilisateurs et ASC.

Nous avons donc eu une idée et avons dû trouver un moyen de relier ces trois aspects entre eux :

  1. Trouver un moyen d'utiliser le cadre de virtualisation d'Apple.
  2. Le faire fonctionner avec les coureurs Gitlab.
  3. Trouver une solution pour la gestion des identités de signature sur plusieurs projets et exécuteurs.

La solution : un aperçu de notre approche (outils inclus)

Nous avons commencé à chercher des solutions pour résoudre tous les problèmes mentionnés précédemment.

  1. Utilisation du framework de virtualisation d'Apple.

Pour le premier obstacle, nous avons trouvé une solution assez rapidement : nous sommes tombés sur l'outil tarte de Cirrus Labs. Dès le premier instant, nous savions que ce serait notre choix.

Les avantages les plus importants de l'utilisation de l'outil à tarte proposé par Cirrus Lab sont :

  • La possibilité de créer des vms à partir d’images brutes .ipsw.
  • La possibilité de créer des vms à l'aide de modèles pré-emballés (avec certains outils utilitaires installés, comme Brew ou Xcode), disponibles sur la page GitHub de Cirrus Labs.
  • L'outil Tart utilise un packer pour la prise en charge de la création d'images dynamiques.
  • L'outil Tart prend en charge les images Linux et MacOS.
  • L'outil utilise une fonctionnalité exceptionnelle du système de fichiers APFS qui permet la duplication de fichiers sans réellement leur réserver d'espace disque. De cette façon, vous n'avez pas besoin d'allouer un espace disque équivalant à 3 fois la taille de l'image d'origine. Vous n'avez besoin que de suffisamment d'espace disque pour l'image d'origine, tandis que le clone occupe uniquement l'espace qui constitue une différence entre lui et l'image d'origine. Ceci est incroyablement utile, d’autant plus que les images macOS ont tendance à être assez volumineuses.

Par exemple, une image macOS Ventura opérationnelle avec Xcode et d'autres utilitaires installés nécessite un minimum de 60 Go d'espace disque. Dans des circonstances normales, une image et deux de ses clones occuperaient jusqu'à 180 Go d'espace disque, ce qui représente une quantité importante. Et ce n'est que le début, car vous souhaiterez peut-être avoir plus d'une image originale ou installer plusieurs versions de Xcode sur une seule VM, ce qui augmenterait encore la taille.

  • L'outil permet la gestion des adresses IP pour les VM originales et clonées, permettant l'accès SSH aux VM.
  • La possibilité de monter des répertoires entre la machine hôte et les machines virtuelles.
  • L'outil est convivial et dispose d'une CLI très simple.

Il ne manque pratiquement rien à cet outil en termes d’utilisation pour la gestion des VM. Presque rien, à part une chose : bien que prometteur, le plugin packer pour construire des images à la volée prenait trop de temps, nous avons donc décidé de ne pas l'utiliser.

Nous avons essayé la tarte et cela a fonctionné à merveille. Ses performances étaient natives et sa gestion simple.

Après avoir intégré tart avec succès avec des résultats impressionnants, nous nous sommes ensuite concentrés sur d’autres défis.

  1. Trouver un moyen de combiner tarte avec les coureurs Gitlab.

Après avoir résolu le premier problème, nous avons été confrontés à la question de savoir comment combiner tarte avec les coureurs Gitlab.

Commençons par décrire ce que font réellement les runners Gitlab :

Le schéma simplifié de la délégation de tâches Gitlab. Système CI pour iOS

Nous devions inclure un casse-tête supplémentaire au diagramme, qui impliquait l'attribution de tâches de l'hôte d'exécution à la VM. Le travail GitLab est un script shell qui contient des variables cruciales, des entrées PATH et des commandes.

Notre objectif était de transférer ce script sur la VM et de l'exécuter.

Cependant, cette tâche s’est avérée plus ardue que nous le pensions initialement.

Le coureur

Les exécuteurs d'exécution Gitlab standard tels que Docker ou SSH sont simples à configurer et nécessitent peu ou pas de configuration. Cependant, nous avions besoin d'un plus grand contrôle sur la configuration, ce qui nous a amené à explorer les exécuteurs personnalisés fournis par GitLab.

Les exécuteurs personnalisés sont une excellente option pour les configurations non standard, car chaque étape du programme d'exécution (préparation, exécution, nettoyage) est décrite sous la forme d'un script shell. La seule chose qui manquait était un outil de ligne de commande capable d'effectuer les tâches dont nous avions besoin et d'être exécuté dans des scripts de configuration du coureur.

Actuellement, il existe quelques outils disponibles qui font exactement cela – par exemple, l’exécuteur de tarte CirrusLabs Gitlab. Cet outil est précisément ce que nous recherchions à l’époque. Cependant, cela n’existait pas encore et après avoir mené des recherches, nous n’avons trouvé aucun outil susceptible de nous aider à accomplir notre tâche.

Écrire sa propre solution

Comme nous n’avons pas trouvé de solution parfaite, nous en avons rédigé une nous-mêmes. Nous sommes des ingénieurs, après tout ! L’idée semblait solide et nous avions tous les outils nécessaires, nous avons donc procédé au développement.

Nous avons choisi d'utiliser Swift et quelques bibliothèques open source fournies par Apple : Swift Argument Parser pour gérer l'exécution en ligne de commande et Swift NIO pour gérer la connexion SSH avec les VM. Nous avons commencé le développement et, en quelques jours, nous avons obtenu le premier prototype fonctionnel d'un outil qui a finalement évolué vers MQVMRunner.

Infrastructure CI iOS : MQVMRunner

À un niveau élevé, l'outil fonctionne comme suit :

  1. (Étape de préparation)
    1. Lisez les variables fournies dans gitlab-ci.yml (nom de l'image et variables supplémentaires).
    2. Choisissez la base de VM demandée
    3. Clonez la base de VM demandée.
    4. Configurez un répertoire monté de manière croisée et copiez le script de travail Gitlab, en définissant les autorisations nécessaires pour celui-ci.
    5. Exécutez le clone et vérifiez la connexion SSH.
    6. Configurez toutes les dépendances requises (comme la version Xcode), si nécessaire.
  2. (Exécuter l'étape)
    1. Exécutez la tâche Gitlab en exécutant un script à partir d'un répertoire monté de manière croisée sur un clone de VM préparé via SSH.
  3. (Étape de nettoyage)
    1. Supprimez l'image clonée.

Les défis du développement

Au cours du développement, nous avons rencontré plusieurs problèmes, ce qui a empêché le déroulement du jeu aussi bien que nous l'aurions souhaité.

  1. Gestion des adresses IP.

La gestion des adresses IP est une tâche cruciale qui doit être traitée avec précaution. Dans le prototype, la gestion SSH a été implémentée à l'aide de commandes shell SSH directes et codées en dur. Cependant, dans le cas de shells non interactifs, une authentification par clé est recommandée. De plus, il est conseillé d'ajouter l'hôte au fichier known_hosts pour éviter les interruptions. Néanmoins, en raison de la gestion dynamique des adresses IP des machines virtuelles, il existe une possibilité de doubler l'entrée pour une IP particulière, entraînant des erreurs. Par conséquent, nous devons attribuer dynamiquement les hôtes_connus pour une tâche particulière afin d'éviter de tels problèmes.

  1. Solution Swift pure.

Compte tenu de cela et du fait que les commandes shell codées en dur dans le code Swift ne sont pas vraiment élégantes, nous avons pensé qu'il serait bien d'utiliser une bibliothèque Swift dédiée et avons décidé d'opter pour Swift NIO. Nous avons résolu certains problèmes mais en même temps, nous en avons introduit quelques nouveaux comme – par exemple – parfois les journaux placés sur stdout étaient transférés *après* la fermeture du canal SSH en raison de la fin de l'exécution de la commande – et, comme nous nous basions sur cette sortie dans les travaux ultérieurs, l'exécution échouait de manière aléatoire.

  1. Sélection de la version Xcode.

Étant donné que le plugin Packer n'était pas une option pour la création d'images dynamiques en raison de la consommation de temps, nous avons décidé d'opter pour une seule base de VM avec plusieurs versions de Xcode préinstallées. Nous avons dû trouver un moyen pour les développeurs de spécifier la version de Xcode dont ils ont besoin dans leur gitlab-ci.yml – et nous avons mis au point des variables personnalisées disponibles pour être utilisées dans n'importe quel projet. MQVMRunner exécutera ensuite « xcode-select » sur une VM clonée pour configurer la version Xcode correspondante.

Et bien d'autres encore

Rationalisation de la migration des projets et intégration continue pour le workflow iOS avec Mac Studios

Nous l'avions configuré sur deux nouveaux Mac Studios et avons commencé à migrer les projets. Nous voulions rendre le processus de migration pour nos développeurs aussi transparent que possible. Nous n'avons pas pu le rendre entièrement transparent, mais finalement, nous sommes arrivés au point où ils n'ont dû faire que quelques choses dans gitlab-ci.yml :

  • Les tags des coureurs : utiliser Mac Studios à la place des Intels.
  • Le nom de l'image : paramètre facultatif, introduit pour une compatibilité future au cas où nous aurions besoin de plus d'une VM de base. À l’heure actuelle, la valeur par défaut est toujours la VM de base unique dont nous disposons.
  • La version de Xcode : paramètre facultatif ; s'il n'est pas fourni, la version la plus récente disponible sera utilisée.

L'outil a reçu de très bons retours initiaux, nous avons donc décidé de le rendre open-source. Nous avons ajouté un script d'installation pour configurer Gitlab Custom Runner et toutes les actions et variables requises. Grâce à notre outil, vous pouvez configurer votre propre runner GitLab en quelques minutes – la seule chose dont vous avez besoin est de démarrer et de baser la VM sur laquelle les tâches seront exécutées.

La structure finale d’intégration continue pour iOS se présente comme suit :

L'infrastructure CI finale : MQVMRunner

3. Solution pour une gestion efficace des identités

Nous avons eu du mal à trouver une solution efficace pour gérer les identités de signature de nos clients. Cela était particulièrement difficile, car l'identité de signature est une donnée hautement confidentielle qui ne doit pas être stockée dans un endroit non sécurisé plus longtemps que nécessaire.

De plus, nous souhaitions charger ces identités uniquement pendant le temps de construction, sans aucune solution inter-projets. Cela signifiait que l’identité ne devait pas être accessible en dehors du bac à sable de l’application (ou de la build). Nous avons déjà résolu ce dernier problème en passant aux VM. Cependant, nous devions encore trouver un moyen de stocker et de charger l'identité de signature dans la VM uniquement pendant la durée de construction.

Problèmes avec Fastlane Match

À l'époque, nous utilisions encore Fastlane Match, qui stocke les identités et les provisions chiffrées dans un référentiel distinct, les charge pendant le processus de construction dans une instance de trousseau distincte et supprime cette instance après la construction.

Cette approche semble pratique, mais elle pose quelques problèmes :

  • Nécessite toute la configuration Fastlane pour fonctionner.

Fastlane est un joyau rubygem, et tous les problèmes répertoriés dans le premier chapitre s'appliquent ici.

  • Extraction du référentiel au moment de la construction.

Nous avons conservé nos identités dans un référentiel distinct qui a été extrait pendant le processus de construction plutôt que lors du processus d'installation. Cela signifiait que nous devions établir un accès séparé au référentiel d'identités, non seulement pour Gitlab, mais pour les exécuteurs spécifiques, de la même manière que nous gérerions les dépendances privées de tiers.

  • Difficile à gérer en dehors de Match.

Si vous utilisez Match pour gérer les identités ou le provisionnement, aucune intervention manuelle n’est nécessaire, voire aucune. Modifier, déchiffrer et chiffrer manuellement les profils afin que les correspondances puissent toujours fonctionner avec eux plus tard est fastidieux et prend du temps. L'utilisation de Fastlane pour effectuer ce processus entraîne généralement l'effacement complet de la configuration de provisionnement de l'application et la création d'une nouvelle.

  • Un peu difficile à déboguer.

En cas de problème de signature de code, vous aurez peut-être du mal à déterminer la correspondance d'identité et d'approvisionnement qui vient d'être installée, car vous devrez d'abord les décoder.

  • Problèmes de sécurité.

Faites correspondre les comptes de développeur consultés à l'aide des informations d'identification fournies pour apporter des modifications en leur nom. Bien que Fastlane soit open source, certains clients l'ont refusé pour des raisons de sécurité.

  • Enfin et surtout, se débarrasser de Match éliminerait le plus gros obstacle sur notre chemin pour nous débarrasser complètement de Fastlane.

Nos exigences initiales étaient les suivantes :

  • Load nécessite de signer l'identité à partir d'un endroit sécurisé, de préférence sous une forme non-texte clair, et de la placer dans le trousseau.
  • Cette identité devrait être accessible par Xcode.
  • De préférence, les variables de mot de passe d'identité, de nom de trousseau et de mot de passe de trousseau doivent être définissables à des fins de débogage.

Match avait tout ce dont nous avions besoin, mais implémenter Fastlane uniquement pour utiliser Match semblait exagéré, en particulier pour les solutions multiplateformes avec leur propre système de construction. Nous voulions quelque chose de similaire à Match, mais sans le lourd fardeau de Ruby qu'il portait.

Créer sa propre solution

Alors nous avons pensé : écrivons cela nous-mêmes ! Nous l'avons fait avec MQVMRunner, nous pourrions donc également le faire ici. Nous avons également choisi Swift pour le faire, principalement parce que nous pouvions obtenir gratuitement de nombreuses API nécessaires en utilisant le framework de sécurité Apple.

Bien sûr, cela ne s’est pas non plus déroulé aussi bien que prévu.

  • Cadre de sécurité en place.

La stratégie la plus simple consistait à appeler les commandes bash comme le fait Fastlane. Cependant, ayant le framework de sécurité disponible, nous avons pensé qu'il serait plus élégant de l'utiliser pour le développement.

  • Manque d'expérience.

Nous n'avions pas beaucoup d'expérience avec le framework de sécurité pour macOS, et il s'est avéré qu'il différait considérablement de ce à quoi nous étions habitués sur iOS. Cela s'est retourné contre nous dans de nombreux cas où nous n'étions pas conscients des limitations de macOS ou supposions qu'il fonctionnait de la même manière que sur iOS – la plupart de ces hypothèses étaient fausses.

  • Documentation épouvantable.

La documentation du framework de sécurité Apple est, pour le moins, humble. Il s'agit d'une API très ancienne qui remonte aux premières versions d'OSX, et parfois, on avait l'impression qu'elle n'avait pas été mise à jour depuis. Une grande partie du code n'est pas documentée, mais nous avons anticipé son fonctionnement en lisant le code source. Heureusement pour nous, c'est open-source.

  • Dépréciations sans remplacements.

Une bonne partie de ce framework est obsolète ; Apple essaie de s'éloigner du trousseau typique « style macOS » (plusieurs trousseaux accessibles par mot de passe) et de mettre en œuvre le trousseau « style iOS » (porte-clés unique, synchronisé via iCloud). Ils l'ont donc rendu obsolète dans macOS Yosemite en 2014, mais n'ont trouvé aucun remplacement au cours des neuf dernières années. Ainsi, la seule API disponible pour nous, pour l'instant, est obsolète car il n'y en a pas encore de nouvelle.

Nous avons supposé que les identités de signature pouvaient être stockées sous forme de chaînes codées en base64 dans les variables Gitlab par projet. Il est sûr, basé sur chaque projet, et s'il est défini comme variable masquée, il peut être lu et affiché dans les journaux de construction sous forme de texte non brut.

Nous avions donc les données d'identité. Il nous suffisait de le mettre dans le porte-clés. Utilisation de l'API de sécurité Après quelques tentatives et une lecture acharnée de la documentation du framework de sécurité, nous avons préparé un prototype de quelque chose qui est devenu plus tard MQSwiftSign.

Apprendre le système de sécurité macOS, mais à la dure

Nous avons dû acquérir une compréhension approfondie du fonctionnement du trousseau macOS pour développer notre outil. Cela impliquait de rechercher comment le trousseau gère les éléments, leur accès et leurs autorisations, ainsi que la structure des données du trousseau. Par exemple, nous avons découvert que le trousseau est le seul fichier macOS pour lequel le système d'exploitation ignore l'ensemble d'ACL. De plus, nous avons appris que les ACL sur des éléments de trousseau spécifiques sont une liste de texte brut enregistrée dans un fichier de trousseau. Nous avons été confrontés à plusieurs défis en cours de route, mais nous avons également beaucoup appris.

Un défi important que nous avons rencontré était celui des invites. Notre outil était principalement conçu pour fonctionner sur les systèmes CI iOS, ce qui signifiait qu'il devait être non interactif. Nous ne pouvions pas demander aux utilisateurs de confirmer un mot de passe sur le CI.

Cependant, le système de sécurité macOS est bien conçu, ce qui rend impossible la modification ou la lecture d'informations confidentielles, y compris l'identité de signature, sans l'autorisation explicite de l'utilisateur. Pour accéder à une ressource sans confirmation, le programme qui accède doit être inclus dans la liste de contrôle d'accès de la ressource. Il s'agit d'une exigence stricte qu'aucun programme ne peut briser, même les programmes Apple fournis avec le système. Si un programme doit lire ou modifier une entrée de trousseau, l'utilisateur doit fournir un mot de passe de trousseau pour le déverrouiller et, éventuellement, l'ajouter à l'ACL de l'entrée.

Surmonter les problèmes d'autorisation des utilisateurs

Nous avons donc dû trouver un moyen pour Xcode d'accéder à une identité définie par notre trousseau sans demander l'autorisation à un utilisateur à l'aide de l'invite de mot de passe. Pour ce faire, nous pouvons modifier la liste de contrôle d’accès d’un élément, mais cela nécessite également l’autorisation de l’utilisateur – et, bien sûr, c’est le cas. Sinon, cela nuirait à l’intérêt même de l’ACL. Nous avons essayé de contourner cette protection – nous avons essayé d'obtenir le même effet qu'avec la commande « security set-key-partition-list ».

Après une analyse approfondie de la documentation du framework, nous n'avons trouvé aucune API permettant de modifier l'ACL sans demander à l'utilisateur de fournir un mot de passe. La chose la plus proche que nous avons trouvée est « SecKeychainItemSetAccess », qui déclenche une invite d'interface utilisateur à chaque fois. Ensuite, nous avons plongé à nouveau, mais cette fois, dans la meilleure documentation, qui est le code source lui-même. Comment Apple l’a-t-il mis en œuvre ?

Il s’est avéré que – comme on pouvait s’y attendre – ils utilisaient une API privée. Une méthode appelée « SecKeychainItemSetAccessWithPassword » fait fondamentalement la même chose que « SecKeychainItemSetAccess », mais au lieu de demander à l'utilisateur un mot de passe, le mot de passe est fourni comme argument à une fonction. Bien sûr, en tant qu'API privée, elle n'est pas répertoriée dans la documentation, mais Apple ne dispose pas de documentation pour de telles API, comme s'ils ne pouvaient pas penser à créer une application pour un usage personnel ou professionnel. L’outil étant destiné à un usage interne uniquement, nous n’avons pas hésité à utiliser l’API privée. La seule chose à faire était de relier la méthode C à Swift.

Surmonter les problèmes d'autorisation des utilisateurs

Ainsi, le flux de travail final du prototype était le suivant :

  1. Créez le trousseau déverrouillé temporaire avec le verrouillage automatique désactivé.
  2. Obtenez et décodez les données d'identité de signature codées en base64 à partir de variables environnementales (transmises par Gitlab).
  3. Importez l'identité dans le trousseau créé.
  4. Définissez les options d'accès appropriées pour l'identité importée afin que Xcode et d'autres outils puissent la lire pour la conception de code.

Autres mises à niveau

Le prototype fonctionnait bien, nous avons donc identifié quelques fonctionnalités supplémentaires que nous aimerions ajouter à l'outil. Notre objectif était de remplacer Fastlane à terme ; nous avons déjà implémenté l'action `match`. Cependant, Fastlane offrait toujours deux fonctionnalités précieuses que nous n'avions pas encore : l'installation de profils de provisionnement et la création d'export.plist.

Installation du profil de provisionnement

L'installation du profil d'approvisionnement est assez simple – elle se résume à extraire l'UUID du profil et à copier le fichier dans `~/Library/MobileDevice/Provisioning Profiles/` avec l'UUID comme nom de fichier – et c'est suffisant pour que Xcode le voie correctement. Ce n'est pas compliqué d'ajouter à notre outil un simple plugin pour parcourir le répertoire fourni et faire cela pour chaque fichier .mobileprovision qu'il trouve à l'intérieur.

Création d'export.plist

La création de export.plist est cependant légèrement plus délicate. Pour générer un fichier IPA approprié, Xcode demande aux utilisateurs de fournir un fichier plist avec des informations spécifiques collectées à partir de diverses sources : le fichier de projet, la liste de droits, les paramètres de l'espace de travail, etc. La raison pour laquelle Xcode ne peut collecter ces données que via l'assistant de distribution, mais pas via la CLI m'est inconnu. Cependant, nous devions les collecter à l'aide des API Swift, n'ayant que des références de projet/espace de travail et une petite dose de connaissances sur la façon dont le fichier de projet Xcode est construit.

Le résultat était meilleur que prévu, nous avons donc décidé de l'ajouter comme autre plugin à notre outil. Nous l'avons également publié en tant que projet open source destiné à un public plus large. À l'heure actuelle, MQSwiftSign est un outil polyvalent qui peut être utilisé avec succès en remplacement des actions rapides de base requises pour créer et distribuer votre application iOS et nous l'utilisons dans tous nos projets dans Miquido.

Réflexions finales : le succès

Passer de l'architecture Intel à l'architecture iOS ARM était une tâche difficile. Nous avons été confrontés à de nombreux obstacles et avons passé beaucoup de temps à développer des outils en raison du manque de documentation. Cependant, nous avons finalement mis en place un système robuste :

  • Deux coureurs à gérer au lieu de neuf ;
  • Exécuter un logiciel entièrement sous notre contrôle, sans une tonne de frais généraux sous forme de rubygems – nous avons pu nous débarrasser de fastlane ou de tout logiciel tiers dans nos configurations de build ;
  • BEAUCOUP de connaissances et de compréhension de choses auxquelles nous ne prêtons généralement pas attention, comme la sécurité du système macOS et le cadre de sécurité lui-même, une structure de projet Xcode réelle et bien d'autres encore.

Je vous encouragerais volontiers – Si vous avez du mal à configurer votre exécuteur GitLab pour les versions iOS, essayez notre MQVMRunner. Si vous avez besoin d'aide pour créer et distribuer votre application à l'aide d'un seul outil et que vous ne souhaitez pas vous fier aux rubygems, essayez MQSwiftSign. Fonctionne pour moi, peut aussi fonctionner pour vous !