CI-Systeme für die iOS-Entwicklung: Transformation von Intel zu ARM

Veröffentlicht: 2024-02-14

In der sich ständig weiterentwickelnden Technologielandschaft müssen sich Unternehmen an den Wind des Wandels anpassen, um relevant und wettbewerbsfähig zu bleiben. Eine solche Transformation, die die Technologiewelt im Sturm erobert hat, ist der Übergang von der Intel x86_64-Architektur zur iOS ARM-Architektur, beispielhaft dargestellt am bahnbrechenden Apple M1-Chip von Apple. In diesem Zusammenhang sind CI-Systeme für iOS zu einer entscheidenden Überlegung für Unternehmen geworden, die diesen Wandel bewältigen und sicherstellen, dass Softwareentwicklungs- und Testprozesse effizient und auf dem neuesten Stand der Technik bleiben.

Apple hat seine M1-Chips vor fast drei Jahren angekündigt und seitdem ist klar, dass das Unternehmen die ARM-Architektur übernehmen und schließlich die Unterstützung für Intel-basierte Software einstellen würde. Um die Kompatibilität zwischen Architekturen aufrechtzuerhalten, hat Apple eine neue Version von Rosetta eingeführt, seinem proprietären Binärübersetzungs-Framework, das sich in der Vergangenheit während der bedeutenden Architekturtransformation von PowerPC zu Intel im Jahr 2006 als zuverlässig erwiesen hat. Die Transformation ist noch im Gange, und wir haben gesehen Xcode verliert in Version 14.3 seine Rosetta-Unterstützung.

Bei Miquido haben wir vor einigen Jahren die Notwendigkeit erkannt, von Intel auf ARM zu migrieren. Wir haben Mitte 2021 mit den Vorbereitungen begonnen. Als Softwarehaus mit mehreren Kunden, Apps und Projekten, die gleichzeitig laufen, standen wir vor einigen Herausforderungen, die wir meistern mussten. Dieser Artikel kann Ihnen als Leitfaden dienen, wenn Ihr Unternehmen mit ähnlichen Situationen konfrontiert ist. Beschriebene Situationen und Lösungen werden aus der Perspektive der iOS-Entwicklung dargestellt – aber möglicherweise finden Sie auch Erkenntnisse, die für andere Technologien geeignet sind. Ein wichtiger Bestandteil unserer Übergangsstrategie bestand darin, sicherzustellen, dass unser CI-System für iOS vollständig für die neue Architektur optimiert wurde, was die Bedeutung von hervorhebt CI-Systeme für iOS bei der Aufrechterhaltung effizienter Arbeitsabläufe und qualitativ hochwertiger Ergebnisse inmitten solch bedeutender Veränderungen.

Das Problem: Migration der Intel-zu-iOS-ARM-Architektur

Apple hat seinen M1-Chip im Jahr 2020 veröffentlicht

Der Migrationsprozess war in zwei Hauptzweige unterteilt.

1. Ersetzen der vorhandenen Intel-basierten Entwicklercomputer durch neue M1-Macbooks

Dieser Vorgang sollte relativ einfach sein. Wir haben eine Richtlinie festgelegt, um innerhalb von zwei Jahren schrittweise alle Intel-Macbooks des Entwicklers zu ersetzen. Derzeit nutzen 95 % unserer Mitarbeiter ARM-basierte MacBooks.

Allerdings stießen wir während dieses Prozesses auf einige unerwartete Herausforderungen. Mitte 2021 verlangsamte ein Mangel an M1-Macs unseren Ersatzprozess. Bis Ende 2021 konnten wir von den fast 200 wartenden MacBooks nur eine Handvoll ersetzen. Wir haben geschätzt, dass es etwa zwei Jahre dauern würde, alle Intel-Macs des Unternehmens vollständig durch M1-Macbooks zu ersetzen, einschließlich Nicht-iOS-Ingenieuren.

Glücklicherweise hat Apple seine neuen M1 Pro- und M2-Chips veröffentlicht. Aus diesem Grund haben wir unseren Fokus vom Ersetzen von Intels durch M1-Macs auf den Ersatz durch M1 Pro- und M2-Chips verlagert.

Software, die nicht für den Wechsel bereit war, sorgte für Frust bei den Entwicklern

Die ersten Ingenieure, die die neuen M1-Macbooks erhielten, hatten eine schwierige Zeit, da der Großteil der Software noch nicht für die Umstellung auf Apples neue iOS-ARM-Architektur bereit war. Am stärksten betroffen waren Tools von Drittanbietern wie Rubygems und Cocoapods, bei denen es sich um Abhängigkeitsmanagement-Tools handelt, die auf vielen anderen Rubygems basieren. Einige dieser Tools wurden damals nicht für die iOS-ARM-Architektur kompiliert, sodass der Großteil der Software mit Rosetta ausgeführt werden musste, was zu Leistungsproblemen und Frustration führte.

Die Softwareentwickler arbeiteten jedoch daran, die meisten dieser Probleme zu lösen, sobald sie auftraten. Der Durchbruch kam mit der Veröffentlichung von Xcode 14.3, das keine Rosetta-Unterstützung mehr hatte. Dies war ein klares Signal an alle Softwareentwickler, dass Apple auf eine Migration der ARM-Architektur von Intel auf iOS drängt. Dies zwang die meisten Softwareentwickler von Drittanbietern, die sich zuvor auf Rosetta verlassen hatten, dazu, ihre Software auf ARM zu migrieren. Heutzutage laufen 99 % der bei Miquido täglich genutzten Drittanbietersoftware ohne Rosetta.

2. Ersetzen des CI-Systems von Miquido für iOS

Der Austausch des Continuous-Integration-iOS-Systems bei Miquido erwies sich als kompliziertere Aufgabe als nur der Austausch von Maschinen. Werfen Sie zunächst einen Blick auf unsere damalige Infrastruktur:

Die CI-Architektur bei Miquido. CI-System für die iOS-Transformation.

Wir hatten eine Gitlab-Cloud-Instanz und 9 damit verbundene Intel-basierte Mac Minis. Diese Maschinen dienten als Job-Runner und Gitlab war für die Orchestrierung verantwortlich. Immer wenn ein CI-Job in die Warteschlange gestellt wurde, wies Gitlab ihn dem ersten verfügbaren Runner zu, der die in der Datei gitlab-ci.yml angegebenen Projektanforderungen erfüllte. Gitlab würde ein Job-Skript erstellen, das alle Build-Befehle, Variablen, Pfade usw. enthält. Dieses Skript wurde dann auf den Runner verschoben und auf diesem Computer ausgeführt.

Obwohl dieses Setup robust erscheinen mag, hatten wir aufgrund der schlechten Unterstützung von Intel-Prozessoren Probleme mit der Virtualisierung. Aus diesem Grund haben wir uns entschieden, keine Virtualisierung wie Docker zu verwenden und Jobs nicht auf den physischen Maschinen selbst auszuführen. Wir haben versucht, eine effiziente und zuverlässige Lösung auf Basis von Docker einzurichten, doch Virtualisierungseinschränkungen wie fehlende GPU-Beschleunigung führten dazu, dass die Ausführung von Jobs doppelt so lange dauerte wie auf den physischen Maschinen. Dies führte zu einem höheren Overhead und einer raschen Befüllung der Warteschlangen.

Aufgrund des macOS SLA konnten wir nur zwei VMs gleichzeitig einrichten. Daher haben wir uns entschieden, den Pool der physischen Läufer zu erweitern und sie so einzurichten, dass sie Gitlab-Jobs direkt auf ihrem Betriebssystem ausführen. Allerdings hatte dieser Ansatz auch einige Nachteile.

Herausforderungen im Build-Prozess und im Runner-Management

  1. Keine Isolierung der Builds außerhalb der Build-Verzeichnis-Sandbox.

Der Runner führt jeden Build auf einer physischen Maschine aus, was bedeutet, dass die Builds nicht von der Build-Verzeichnis-Sandbox isoliert sind. Das hat seine Vor- und Nachteile. Einerseits können wir Systemcaches verwenden, um Builds zu beschleunigen, da die meisten Projekte dieselben Abhängigkeiten von Drittanbietern verwenden.

Andererseits wird der Cache nicht mehr wartbar, da sich Reste eines Projekts auf jedes andere Projekt auswirken können. Dies ist besonders wichtig für systemweite Caches, da sowohl für die Flutter- als auch für die React Native-Entwicklung dieselben Runner verwendet werden. Insbesondere React Native erfordert viele über NPM zwischengespeicherte Abhängigkeiten.

  1. Mögliches Systemtool-Chaos.

Obwohl keiner der Jobs mit Sudo-Berechtigungen ausgeführt wurde, war es ihnen dennoch möglich, auf einige System- oder Benutzertools wie Ruby zuzugreifen. Dies stellte die potenzielle Gefahr dar, dass einige dieser Tools beschädigt werden könnten, insbesondere da macOS Ruby für einige seiner Legacy-Software verwendet, einschließlich einiger Legacy-Xcode-Funktionen. Mit der Systemversion von Ruby sollten Sie sich nicht herumschlagen.

Die Einführung von rbenv führt jedoch zu einer weiteren Komplexitätsebene, die es zu bewältigen gilt. Es ist wichtig zu beachten, dass Rubygems pro Ruby-Version installiert werden und einige dieser Gems bestimmte Ruby-Versionen erfordern. Fast alle von uns verwendeten Tools von Drittanbietern waren Ruby-abhängig, wobei Cocoapods und Fastlane die Hauptakteure waren.

  1. Signaturidentitäten verwalten.

Die Verwaltung mehrerer Signaturidentitäten von verschiedenen Client-Entwicklungskonten kann eine Herausforderung sein, wenn es um die System-Schlüsselanhänger auf den Runnern geht. Die Signaturidentität ist ein hochsensibles Datenelement, da sie es uns ermöglicht, die Anwendung mitzugestalten und sie so anfällig für potenzielle Bedrohungen zu machen.

Um die Sicherheit zu gewährleisten, sollten die Identitäten projektübergreifend in einer Sandbox gespeichert und geschützt werden. Allerdings kann dieser Prozess zu einem Albtraum werden, wenn man die zusätzliche Komplexität bedenkt, die macOS bei der Schlüsselbundimplementierung mit sich bringt.

  1. Herausforderungen in Multiprojektumgebungen.

Nicht alle Projekte wurden mit denselben Tools erstellt, insbesondere mit Xcode. Einige Projekte, insbesondere diejenigen in der Supportphase, wurden mit der letzten Version von Xcode gepflegt, mit der das Projekt entwickelt wurde. Das heißt, wenn für diese Projekte Arbeiten erforderlich waren, musste das CI in der Lage sein, diese zu erstellen. Infolgedessen mussten Runner mehrere Versionen von Xcode gleichzeitig unterstützen, was die Anzahl der für einen bestimmten Job verfügbaren Runner effektiv einschränkte.

5. Zusätzlicher Aufwand erforderlich.

Alle auf den Läufern vorgenommenen Änderungen, wie z. B. die Softwareinstallation, müssen auf allen Läufern gleichzeitig durchgeführt werden. Obwohl wir hierfür ein Automatisierungstool hatten, erforderte die Pflege von Automatisierungsskripten zusätzlichen Aufwand.

Maßgeschneiderte Infrastrukturlösungen für unterschiedliche Kundenbedürfnisse

Miquido ist ein Softwarehaus, das mit mehreren Kunden mit unterschiedlichen Anforderungen zusammenarbeitet. Wir passen unsere Dienstleistungen an die spezifischen Anforderungen jedes Kunden an. Wir hosten häufig die Codebasis und die notwendige Infrastruktur für kleine Unternehmen oder Start-ups, da ihnen möglicherweise die Ressourcen oder das Wissen für die Wartung fehlen.

Unternehmenskunden verfügen in der Regel über eine eigene Infrastruktur zum Hosten ihrer Projekte. Einige verfügen jedoch nicht über die entsprechenden Kapazitäten oder sind aufgrund von Branchenvorschriften dazu verpflichtet, ihre Infrastruktur zu nutzen. Sie ziehen es auch vor, keine SaaS-Dienste von Drittanbietern wie Xcode Cloud oder Codemagic zu nutzen. Stattdessen wünschen sie sich eine Lösung, die zu ihrer bestehenden Architektur passt.

Um diesen Kunden gerecht zu werden, hosten wir die Projekte häufig auf unserer Infrastruktur oder richten auf ihrer Infrastruktur dieselbe iOS-Konfiguration für die kontinuierliche Integration ein. Beim Umgang mit sensiblen Informationen und Dateien, etwa beim Signieren von Identitäten, lassen wir jedoch besondere Sorgfalt walten.

Nutzen Sie Fastlane für ein effizientes Build-Management

Hier kommt Fastlane als praktisches Tool zum Einsatz. Es besteht aus verschiedenen Modulen, sogenannten Aktionen, die dabei helfen, den Prozess zu rationalisieren und auf verschiedene Clients aufzuteilen. Eine dieser Aktionen namens „Match“ hilft dabei, Entwicklungs- und Produktionssignaturidentitäten sowie Bereitstellungsprofile beizubehalten. Es funktioniert auch auf Betriebssystemebene, um diese Identitäten für die Build-Zeit in separate Schlüsselbunde zu trennen und nach dem Build eine Bereinigung durchzuführen, was besonders hilfreich ist, da wir alle unsere Builds auf physischen Maschinen ausführen.

Fastlane: Entwicklungsautomatisierungstool
Bildnachweis: Fastlane

Wir haben uns ursprünglich aus einem bestimmten Grund für Fastlane entschieden, aber dann festgestellt, dass es zusätzliche Funktionen bietet, die für uns nützlich sein könnten.

  1. Build-Upload auf Testflight

In der Vergangenheit war die AppStoreConnect-API für Entwickler nicht öffentlich verfügbar. Dies bedeutete, dass die einzige Möglichkeit, einen Build auf Testflight hochzuladen, über Xcode oder die Verwendung von Fastlane war. Fastlane war ein Tool, das im Wesentlichen die ASC-API entfernte und sie in eine Aktion namens pilot umwandelte. Diese Methode brach jedoch häufig mit dem nächsten Xcode-Update ab. Wenn ein Entwickler seinen Build über die Befehlszeile auf Testflight hochladen wollte, war Fastlane die beste verfügbare Option.

  1. Einfacher Wechsel zwischen Xcode-Versionen

Da auf einem einzelnen Computer mehr als eine Xcode-Instanz vorhanden ist, musste ausgewählt werden, welcher Xcode für den Build verwendet werden soll. Leider hat Apple den Wechsel zwischen Xcode-Versionen umständlich gemacht – Sie müssen dazu „xcode-select“ verwenden, was zusätzlich sudo-Berechtigungen erfordert. Auch das deckt Fastlane ab.

  1. Zusätzliche Dienstprogramme für Entwickler

Fastlane bietet viele weitere nützliche Dienstprogramme, darunter Versionierung und die Möglichkeit, Build-Ergebnisse an Webhooks zu senden.

Die Nachteile von Fastlane

Die Anpassung von Fastlane an unsere Projekte war solide und solide, also gingen wir in diese Richtung. Wir haben es mehrere Jahre lang erfolgreich eingesetzt. Im Laufe dieser Jahre haben wir jedoch einige Probleme festgestellt:

  1. Fastlane erfordert Ruby-Kenntnisse.

Fastlane ist ein in Ruby geschriebenes Tool und erfordert gute Ruby-Kenntnisse, um es effektiv nutzen zu können. Wenn es Fehler in Ihrer Fastlane-Konfiguration oder im Tool selbst gibt, kann das Debuggen mit irb oder pry eine große Herausforderung sein.

  1. Abhängigkeit von zahlreichen Edelsteinen.

Fastlane selbst setzt auf etwa 70 Edelsteine. Um das Risiko eines Systemabsturzes von Ruby zu mindern, verwendeten Projekte lokale Bundler-Gems. Das Abrufen all dieser Juwelen verursachte viel Zeitaufwand.

  1. Probleme mit System Ruby und Rubygems.

Daher gelten alle zuvor erwähnten Probleme mit dem System Ruby und Rubygems auch hier.

  1. Redundanz für Flutter-Projekte.

Flutter-Projekte waren auch gezwungen, Fastlane Match zu verwenden, nur um die Kompatibilität mit iOS-Projekten zu wahren und die Schlüsselanhänger der Läufer zu schützen. Das war absurd unnötig, da Flutter über ein eigenes Build-System verfügt und der zuvor erwähnte Overhead nur für die Verwaltung von Signaturidentitäten und Bereitstellungsprofilen eingeführt wurde.

Die meisten dieser Probleme wurden im Laufe der Zeit behoben, aber wir brauchten eine robustere und zuverlässigere Lösung.

Die Idee: Anpassung eines neuen, robusteren Continuous-Integration-Tools für iOS

Die gute Nachricht ist, dass Apple die volle Kontrolle über seine Chip-Architektur erlangt und ein neues Virtualisierungs-Framework für macOS entwickelt hat. Mit diesem Framework können Benutzer virtuelle Linux- oder macOS-Maschinen erstellen, konfigurieren und ausführen, die schnell starten und sich durch eine native ähnliche Leistung auszeichnen – und ich meine wirklich native ähnliche.

Das sah vielversprechend aus und könnte ein Grundstein für unsere neuen Continuous-Integration-Tools für iOS sein. Es handelte sich jedoch nur um einen Ausschnitt einer Gesamtlösung. Da wir ein VM-Verwaltungstool hatten, brauchten wir auch etwas, das dieses Framework in Abstimmung mit unseren Gitlab-Läufern nutzen konnte.

Damit wären die meisten unserer Probleme hinsichtlich schlechter Virtualisierungsleistung obsolet. Außerdem könnten wir damit die meisten Probleme, die wir mit Fastlane lösen wollten, automatisch lösen.

Entwicklung einer maßgeschneiderten Lösung für das On-Demand-Signatur-Identitätsmanagement

Wir hatten noch ein letztes Problem zu lösen – das signierende Identitätsmanagement. Wir wollten dafür nicht Fastlane nutzen, da es für unsere Bedürfnisse zu groß erschien. Stattdessen suchten wir nach einer Lösung, die besser auf unsere Anforderungen zugeschnitten war. Unsere Anforderungen waren klar: Der Identitätsverwaltungsprozess musste bei Bedarf erfolgen, ausschließlich für die Erstellungszeit, ohne vorinstallierte Identitäten auf dem Schlüsselbund und mit jedem Computer kompatibel sein, auf dem er ausgeführt werden würde.

Das Verteilungsproblem und das Fehlen einer stabilen AppstoreConnect-API wurden obsolet, als Apple sein „Altool“ veröffentlichte, das die Kommunikation zwischen Benutzern und ASC ermöglichte.

Also hatten wir eine Idee und mussten einen Weg finden, diese drei Aspekte miteinander zu verbinden:

  1. Suche nach einer Möglichkeit, das Virtualisierungs-Framework von Apple zu nutzen.
  2. Damit es mit Gitlab-Läufern funktioniert.
  3. Suche nach einer Lösung für das signierende Identitätsmanagement über mehrere Projekte und Läufer hinweg.

Die Lösung: Ein Einblick in unseren Ansatz (inklusive Tools)

Wir haben begonnen, nach Lösungen zu suchen, um alle zuvor genannten Probleme anzugehen.

  1. Nutzung des Virtualisierungs-Frameworks von Apple.

Für das erste Hindernis fanden wir recht schnell eine Lösung: Wir stießen auf das Törtchen-Tool von Cirrus Labs. Vom ersten Moment an wussten wir, dass dies unsere Wahl sein würde.

Die wichtigsten Vorteile der Verwendung des von Cirrus Lab angebotenen Tortenwerkzeugs sind:

  • Die Möglichkeit, VMs aus rohen .ipsw-Images zu erstellen.
  • Die Möglichkeit, VMs mithilfe vorgefertigter Vorlagen zu erstellen (mit einigen installierten Dienstprogrammen wie Brew oder Xcode), verfügbar auf der GitHub-Seite von Cirrus Labs.
  • Das Tart-Tool nutzt Packer zur Unterstützung der dynamischen Bilderstellung.
  • Das Tart-Tool unterstützt sowohl Linux- als auch MacOS-Images.
  • Das Tool nutzt eine herausragende Funktion des APFS-Dateisystems, die das Duplizieren von Dateien ermöglicht, ohne tatsächlich Speicherplatz für sie zu reservieren. Auf diese Weise müssen Sie keinen Speicherplatz für das Dreifache der ursprünglichen Bildgröße zuweisen. Sie benötigen nur ausreichend Speicherplatz für das Originalbild, während der Klon nur den Speicherplatz einnimmt, der sich vom Originalbild unterscheidet. Dies ist unglaublich hilfreich, insbesondere da macOS-Bilder in der Regel recht groß sind.

Beispielsweise erfordert ein betriebsbereites macOS Ventura-Image mit installiertem Xcode und anderen Dienstprogrammen mindestens 60 GB Festplattenspeicher. Unter normalen Umständen würden ein Image und zwei seiner Klone bis zu 180 GB Speicherplatz beanspruchen, was eine beträchtliche Menge ist. Und das ist erst der Anfang, denn vielleicht möchten Sie mehr als ein Original-Image haben oder mehrere Xcode-Versionen auf einer einzelnen VM installieren, was die Größe weiter erhöhen würde.

  • Das Tool ermöglicht die IP-Adressverwaltung für ursprüngliche und geklonte VMs und ermöglicht den SSH-Zugriff auf VMs.
  • Die Möglichkeit, Verzeichnisse zwischen Host-Maschine und VMs übergreifend bereitzustellen.
  • Das Tool ist benutzerfreundlich und verfügt über eine sehr einfache CLI.

Es gibt kaum etwas, was diesem Tool in Bezug auf die Verwendung für die VM-Verwaltung fehlt. Kaum etwas, außer einer Sache: Obwohl das Packer-Plugin zum Erstellen von Bildern im Handumdrehen vielversprechend war, war es übermäßig zeitaufwändig, weshalb wir uns entschieden haben, es nicht zu verwenden.

Wir haben es mit Torte probiert und es hat fantastisch funktioniert. Die Leistung war nativ vergleichbar und die Verwaltung war einfach.

Nachdem wir tart mit beeindruckenden Ergebnissen erfolgreich integriert hatten, konzentrierten wir uns als nächstes auf die Bewältigung anderer Herausforderungen.

  1. Suche nach einer Möglichkeit, Tart mit Gitlab-Läufern zu kombinieren.

Nachdem wir das erste Problem gelöst hatten, standen wir vor der Frage, wie wir Tart mit Gitlab-Läufern kombinieren können.

Beginnen wir damit, zu beschreiben, was Gitlab-Läufer tatsächlich tun:

Das vereinfachte Diagramm der Gitlab-Jobdelegation. CI-System für iOS

Wir mussten ein zusätzliches Rätsel in das Diagramm einfügen, bei dem es um die Zuweisung von Aufgaben vom Runner-Host zur VM ging. Der GitLab-Job ist ein Shell-Skript, das wichtige Variablen, PATH-Einträge und Befehle enthält.

Unser Ziel war es, dieses Skript auf die VM zu übertragen und auszuführen.

Allerdings erwies sich diese Aufgabe als anspruchsvoller, als wir zunächst dachten.

Der Läufer

Standardmäßige Gitlab-Runner-Executoren wie Docker oder SSH sind einfach einzurichten und erfordern wenig bis gar keine Konfiguration. Wir benötigten jedoch eine größere Kontrolle über die Konfiguration, was uns dazu veranlasste, die von GitLab bereitgestellten benutzerdefinierten Executoren zu erkunden.

Benutzerdefinierte Executoren sind eine großartige Option für nicht standardmäßige Konfigurationen, da jeder Runner-Schritt (Vorbereiten, Ausführen, Bereinigen) in Form eines Shell-Skripts beschrieben wird. Das Einzige, was fehlte, war ein Befehlszeilentool, das die von uns benötigten Aufgaben ausführen und in Runner-Konfigurationsskripten ausgeführt werden konnte.

Derzeit gibt es einige Tools, die genau das tun – zum Beispiel den Gitlab-Tart-Executor von CirrusLabs. Dieses Tool ist genau das, wonach wir damals gesucht haben. Es existierte jedoch noch nicht und nach unserer Recherche fanden wir kein Werkzeug, das uns bei der Bewältigung unserer Aufgabe helfen könnte.

Schreiben der eigenen Lösung

Da wir keine perfekte Lösung finden konnten, haben wir selbst eine geschrieben. Wir sind schließlich Ingenieure! Die Idee schien solide zu sein und wir verfügten über alle notwendigen Werkzeuge, also fuhren wir mit der Entwicklung fort.

Wir haben uns für die Verwendung von Swift und einigen von Apple bereitgestellten Open-Source-Bibliotheken entschieden: Swift Argument Parser für die Befehlszeilenausführung und Swift NIO für die SSH-Verbindung mit VMs. Wir begannen mit der Entwicklung und erhielten innerhalb weniger Tage den ersten funktionierenden Prototyp eines Tools, das sich schließlich zu MQVMRunner entwickelte.

iOS CI-Infrastruktur: MQVMRunner

Auf hoher Ebene funktioniert das Tool wie folgt:

  1. (Vorbereitungsschritt)
    1. Lesen Sie die in gitlab-ci.yml bereitgestellten Variablen (Bildname und zusätzliche Variablen).
    2. Wählen Sie die gewünschte VM-Basis aus
    3. Klonen Sie die angeforderte VM-Basis.
    4. Richten Sie ein Cross-Mount-Verzeichnis ein, kopieren Sie das Gitlab-Jobskript hinein und legen Sie die erforderlichen Berechtigungen dafür fest.
    5. Führen Sie den Klon aus und überprüfen Sie die SSH-Verbindung.
    6. Richten Sie bei Bedarf alle erforderlichen Abhängigkeiten (z. B. die Xcode-Version) ein.
  2. (Schritt ausführen)
    1. Führen Sie den Gitlab-Job aus, indem Sie über SSH ein Skript aus einem Cross-Mount-Verzeichnis auf einem vorbereiteten VM-Klon ausführen.
  3. (Aufräumschritt)
    1. Geklontes Bild löschen.

Herausforderungen in der Entwicklung

Während der Entwicklung sind wir auf mehrere Probleme gestoßen, die dazu geführt haben, dass die Entwicklung nicht so reibungslos verlief, wie wir es uns gewünscht hätten.

  1. IP-Adressverwaltung.

Die Verwaltung von IP-Adressen ist eine wichtige Aufgabe, die mit Sorgfalt gehandhabt werden muss. Im Prototyp wurde das SSH-Handling über direkte und fest codierte SSH-Shell-Befehle umgesetzt. Bei nicht interaktiven Shells wird jedoch eine Schlüsselauthentifizierung empfohlen. Darüber hinaus empfiehlt es sich, den Host zur Datei „known_hosts“ hinzuzufügen, um Unterbrechungen zu vermeiden. Aufgrund der dynamischen Verwaltung der IP-Adressen virtueller Maschinen besteht jedoch die Möglichkeit, dass der Eintrag für eine bestimmte IP verdoppelt wird, was zu Fehlern führt. Daher müssen wir die bekannten_Hosts dynamisch für einen bestimmten Job zuweisen, um solche Probleme zu verhindern.

  1. Reine Swift-Lösung.

In Anbetracht dessen und der Tatsache, dass hartcodierte Shell-Befehle im Swift-Code nicht wirklich elegant sind, dachten wir, dass es schön wäre, eine dedizierte Swift-Bibliothek zu verwenden, und entschieden uns für Swift NIO. Wir haben einige Probleme gelöst, aber gleichzeitig auch ein paar neue eingeführt, wie zum Beispiel, dass manchmal auf stdout abgelegte Protokolle übertragen wurden, *nachdem* der SSH-Kanal beendet wurde, weil die Befehlsausführung beendet wurde – und, wie wir vermutet haben Diese Ausgabe in der weiteren Arbeit, die Ausführung schlug zufällig fehl.

  1. Auswahl der Xcode-Version.

Da das Packer-Plugin aufgrund des Zeitaufwands keine Option für die dynamische Image-Erstellung war, haben wir uns für eine einzelne VM-Basis mit mehreren vorinstallierten Xcode-Versionen entschieden. Wir mussten eine Möglichkeit für Entwickler finden, die benötigte Xcode-Version in ihrer gitlab-ci.yml anzugeben – und wir haben benutzerdefinierte Variablen entwickelt, die in jedem Projekt verwendet werden können. MQVMRunner führt dann „xcode-select“ auf einer geklonten VM aus, um die entsprechende Xcode-Version einzurichten.

Und viele, viele mehr

Optimierte Projektmigration und kontinuierliche Integration für iOS-Workflow mit Mac Studios

Wir hatten das auf zwei neuen Mac Studios eingerichtet und mit der Migration der Projekte begonnen. Wir wollten den Migrationsprozess für unsere Entwickler so transparent wie möglich gestalten. Wir konnten es nicht ganz nahtlos gestalten, aber irgendwann kamen wir an den Punkt, an dem sie nur noch ein paar Dinge in gitlab-ci.yml tun mussten:

  • Die Tags der Läufer: Mac Studios anstelle von Intels verwenden.
  • Der Name des Images: optionaler Parameter, der für zukünftige Kompatibilität eingeführt wurde, falls wir mehr als eine Basis-VM benötigen. Im Moment wird standardmäßig immer die einzelne Basis-VM verwendet, die wir haben.
  • Die Version von Xcode: optionaler Parameter; Falls nicht angegeben, wird die neueste verfügbare Version verwendet.

Das Tool erhielt zunächst ein sehr gutes Feedback, daher haben wir uns entschieden, es als Open-Source-Lösung anzubieten. Wir haben ein Installationsskript hinzugefügt, um Gitlab Custom Runner und alle erforderlichen Aktionen und Variablen einzurichten. Mit unserem Tool können Sie in wenigen Minuten Ihren eigenen GitLab-Runner einrichten – Sie benötigen lediglich einen Tart und eine Basis-VM, auf der die Jobs ausgeführt werden.

Die endgültige Struktur der kontinuierlichen Integration für iOS sieht wie folgt aus:

Die endgültige CI-Infrastruktur: MQVMRunner

3. Lösung für effizientes Identitätsmanagement

Wir hatten Mühe, eine effiziente Lösung für die Verwaltung der Signaturidentitäten unserer Kunden zu finden. Dies stellte eine besondere Herausforderung dar, da es sich bei der Signaturidentität um streng vertrauliche Daten handelt, die nicht länger als nötig an einem unsicheren Ort gespeichert werden sollten.

Darüber hinaus wollten wir diese Identitäten nur während der Erstellungszeit laden, ohne projektübergreifende Lösungen. Dies bedeutete, dass die Identität außerhalb der App- (oder Build-)Sandbox nicht zugänglich sein sollte. Letzteres Problem haben wir bereits durch den Übergang zu VMs angegangen. Wir mussten jedoch noch eine Möglichkeit finden, die Signaturidentität nur für die Erstellungszeit zu speichern und in die VM zu laden.

Probleme mit Fastlane Match

Damals verwendeten wir noch den Fastlane-Match, der verschlüsselte Identitäten und Bereitstellungen in einem separaten Repository speichert, sie während des Build-Prozesses in eine separate Schlüsselbundinstanz lädt und diese Instanz nach dem Build entfernt.

Dieser Ansatz erscheint praktisch, bringt jedoch einige Probleme mit sich:

  • Erfordert, dass das gesamte Fastlane-Setup funktioniert.

Fastlane ist Rubygem und alle im ersten Kapitel aufgeführten Probleme gelten hier.

  • Auschecken des Repositorys zur Erstellungszeit.

Wir haben unsere Identitäten in einem separaten Repository gespeichert, das während des Build-Prozesses und nicht während des Setup-Prozesses ausgecheckt wurde. Das bedeutete, dass wir einen separaten Zugriff auf das Identitäts-Repository einrichten mussten, nicht nur für Gitlab, sondern auch für die spezifischen Läufer, ähnlich wie wir mit privaten Abhängigkeiten von Drittanbietern umgehen würden.

  • Außerhalb von Match schwer zu bewältigen.

Wenn Sie Match zur Verwaltung von Identitäten oder zur Bereitstellung nutzen, sind kaum oder gar keine manuellen Eingriffe erforderlich. Das manuelle Bearbeiten, Entschlüsseln und Verschlüsseln von Profilen, damit Matches später noch mit ihnen funktionieren, ist mühsam und zeitaufwändig. Die Verwendung von Fastlane zur Durchführung dieses Prozesses führt normalerweise dazu, dass das Anwendungsbereitstellungs-Setup vollständig gelöscht und ein neues erstellt wird.

  • Etwas schwer zu debuggen.

Bei Problemen mit der Codesignatur kann es schwierig sein, die Identität und die Bereitstellungsübereinstimmung zu ermitteln, die gerade installiert wurde, da Sie diese zuerst dekodieren müssten.

  • Sicherheitsbedenken.

Matchen Sie mithilfe der bereitgestellten Anmeldeinformationen Zugriff auf Entwicklerkonten, um in ihrem Namen Änderungen vorzunehmen. Obwohl Fastlane Open Source ist, lehnten einige Kunden die Nutzung aus Sicherheitsgründen ab.

  • Zu guter Letzt würde die Abschaffung von Match das größte Hindernis auf unserem Weg zur vollständigen Abschaffung von Fastlane beseitigen.

Unsere ursprünglichen Anforderungen waren wie folgt:

  • Beim Laden muss die Identität von einem sicheren Ort, vorzugsweise in Nicht-Klartext-Form, signiert und im Schlüsselbund abgelegt werden.
  • Auf diese Identität sollte Xcode zugreifen können.
  • Vorzugsweise sollten die Variablen „Identitätspasswort“, „Schlüsselbundname“ und „Schlüsselbundpasswort“ für Debugging-Zwecke einstellbar sein.

Match hatte alles, was wir brauchten, aber die Implementierung von Fastlane nur zur Nutzung von Match erschien uns wie ein Overkill, insbesondere für plattformübergreifende Lösungen mit eigenem Build-System. Wir wollten etwas Ähnliches wie Match, aber ohne die schwere Ruby-Last, die es mit sich brachte.

Erstellen der eigenen Lösung

Also dachten wir uns – schreiben wir das selbst! Wir haben das mit MQVMRunner gemacht, also könnten wir es auch hier machen. Wir haben uns dafür auch für Swift entschieden, vor allem weil wir mit dem Apple Security Framework viele notwendige APIs kostenlos bekommen konnten.

Natürlich lief es auch nicht so reibungslos wie erwartet.

  • Sicherheitsrahmen vorhanden.

Die einfachste Strategie bestand darin, die Bash-Befehle wie Fastlane aufzurufen. Da wir jedoch das Security-Framework zur Verfügung hatten, hielten wir es für eleganter, es für die Entwicklung zu verwenden.

  • Mangel an Erfahrung.

Wir hatten nicht viel Erfahrung mit dem Sicherheits-Framework für macOS und es stellte sich heraus, dass es sich erheblich von dem unterschied, was wir von iOS gewohnt waren. Dies hat sich in vielen Fällen negativ auf uns ausgewirkt, da wir uns der Einschränkungen von macOS nicht bewusst waren oder davon ausgingen, dass es genauso funktioniert wie unter iOS – die meisten dieser Annahmen waren falsch.

  • Schreckliche Dokumentation.

Die Dokumentation des Apple Security Frameworks ist, gelinde gesagt, bescheiden. Es handelt sich um eine sehr alte API, die auf die ersten Versionen von OSX zurückgeht, und manchmal hatten wir den Eindruck, dass sie seitdem nicht aktualisiert wurde. Ein großer Teil des Codes ist nicht dokumentiert, aber wir haben durch das Lesen des Quellcodes vorhergesehen, wie er funktioniert. Zum Glück ist es Open Source.

  • Abschreibungen ohne Ersatz.

Ein großer Teil dieses Frameworks ist veraltet; Apple versucht, vom typischen „MacOS-Stil“-Schlüsselbund (mehrere Schlüsselbunde, auf die per Passwort zugegriffen werden kann) wegzukommen und den „iOS-Stil“-Schlüsselbund (einzelner Schlüsselbund, synchronisiert über iCloud) zu implementieren. Deshalb haben sie es in macOS Yosemite bereits 2014 als veraltet markiert, in den letzten neun Jahren jedoch keinen Ersatz dafür gefunden – die einzige API, die uns derzeit zur Verfügung steht, ist also veraltet, da es noch keine neue gibt.

Wir gingen davon aus, dass Signaturidentitäten als Base64-codierte Zeichenfolgen in projektspezifischen Gitlab-Variablen gespeichert werden können. Es ist sicher, projektbasiert und kann, wenn es als maskierte Variable festgelegt ist, als Nicht-Klartext gelesen und in Build-Protokollen angezeigt werden.

Wir hatten also die Identitätsdaten. Wir mussten es nur in den Schlüsselbund stecken. Verwenden der Sicherheits-API Nach ein paar Versuchen und einem heftigen Durchgehen der Dokumentation zum Sicherheits-Framework haben wir einen Prototyp von etwas vorbereitet, das später zu MQSwiftSign wurde.

Lernen Sie das macOS-Sicherheitssystem kennen, aber auf die harte Tour

Um unser Tool zu entwickeln, mussten wir ein tiefes Verständnis für die Funktionsweise des macOS-Schlüsselbunds erlangen. Dabei wurde untersucht, wie der Schlüsselbund Elemente, deren Zugriff und Berechtigungen sowie die Struktur der Schlüsselbunddaten verwaltet. Wir haben beispielsweise herausgefunden, dass der Schlüsselbund die einzige macOS-Datei ist, deren ACL-Satz vom Betriebssystem ignoriert wird. Darüber hinaus haben wir erfahren, dass ACL für bestimmte Schlüsselbundelemente eine reine Textliste ist, die in einer Schlüsselbunddatei gespeichert wird. Auf dem Weg dorthin standen wir vor einigen Herausforderungen, aber wir haben auch viel gelernt.

Eine große Herausforderung, auf die wir gestoßen sind, waren Eingabeaufforderungen. Unser Tool war in erster Linie für die Ausführung auf den CI-iOS-Systemen konzipiert, was bedeutete, dass es nicht interaktiv sein durfte. Wir konnten Benutzer nicht auffordern, ein Passwort auf dem CI zu bestätigen.

Allerdings ist das macOS-Sicherheitssystem gut konzipiert, sodass es ohne ausdrückliche Benutzererlaubnis unmöglich ist, vertrauliche Informationen, einschließlich der Signaturidentität, zu bearbeiten oder zu lesen. Um ohne Bestätigung auf eine Ressource zuzugreifen, muss das zugreifende Programm in der Zugriffskontrollliste der Ressource enthalten sein. Dies ist eine strikte Anforderung, die kein Programm beschädigen kann, auch nicht die Apple-Programme, die mit dem System geliefert werden. Wenn ein Programm einen Schlüsselbundeintrag lesen oder bearbeiten muss, muss der Benutzer ein Schlüsselbundkennwort angeben, um ihn zu entsperren, und es optional zur ACL des Eintrags hinzufügen.

Herausforderungen bei Benutzerberechtigungen meistern

Wir mussten also eine Möglichkeit für Xcode finden, auf eine von unserem Schlüsselbund eingerichtete Identität zuzugreifen, ohne einen Benutzer über die Passwortabfrage um Erlaubnis zu bitten. Dazu können wir die Zugriffskontrollliste eines Elements ändern, aber dafür ist auch die Erlaubnis des Benutzers erforderlich – und das ist natürlich auch der Fall. Andernfalls würde es den gesamten Sinn der ACL untergraben. Wir haben versucht, diese Sicherheitsmaßnahme zu umgehen – wir haben versucht, den gleichen Effekt wie mit dem Befehl „security set-key-partition-list“ zu erzielen.

Nach einem tiefen Einblick in die Framework-Dokumentation haben wir keine API gefunden, die das Bearbeiten der ACL ermöglicht, ohne den Benutzer zur Eingabe eines Passworts aufzufordern. Das nächstgelegene, was wir gefunden haben, ist „SecKeychainItemSetAccess“, das jedes Mal eine UI-Eingabeaufforderung auslöst. Dann tauchten wir erneut ein, dieses Mal jedoch in die beste Dokumentation, nämlich den Quellcode selbst. Wie hat Apple es umgesetzt?

Es stellte sich erwartungsgemäß heraus, dass sie eine private API nutzten. Eine Methode namens „SecKeychainItemSetAccessWithPassword“ macht im Grunde dasselbe wie „SecKeychainItemSetAccess“, aber anstatt den Benutzer zur Eingabe eines Passworts aufzufordern, wird das Passwort als Argument für eine Funktion bereitgestellt. Natürlich – als private API ist sie nicht in der Dokumentation aufgeführt, aber Apple fehlt die Dokumentation für solche APIs, als ob sie nicht daran denken könnten, eine App für den persönlichen oder geschäftlichen Gebrauch zu erstellen. Da das Tool nur für den internen Gebrauch gedacht war, haben wir nicht gezögert, die private API zu verwenden. Das Einzige, was getan werden musste, war, die C-Methode in Swift zu überbrücken.

Herausforderungen bei Benutzerberechtigungen meistern

Der endgültige Arbeitsablauf des Prototyps war also wie folgt:

  1. Erstellen Sie den vorübergehend entsperrten Schlüsselbund mit deaktivierter automatischer Sperre.
  2. Rufen Sie die Base64-codierten Signaturidentitätsdaten aus Umgebungsvariablen (von Gitlab übergeben) ab und dekodieren Sie sie.
  3. Importieren Sie die Identität in den erstellten Schlüsselbund.
  4. Legen Sie die richtigen Zugriffsoptionen für die importierte Identität fest, damit Xcode und andere Tools sie für das Codesign lesen können.

Weitere Upgrades

Der Prototyp funktionierte gut, daher haben wir einige zusätzliche Funktionen identifiziert, die wir dem Tool hinzufügen möchten. Unser Ziel war es, Fastlane irgendwann zu ersetzen; Wir haben die Aktion „Match“ bereits implementiert. Fastlane bot jedoch immer noch zwei wertvolle Funktionen, die wir noch nicht hatten – die Installation von Bereitstellungsprofilen und die Erstellung von export.plist.

Installation des Bereitstellungsprofils

Die Installation des Bereitstellungsprofils ist ziemlich einfach – sie besteht aus dem Extrahieren der Profil-UUID und dem Kopieren der Datei nach „~/Library/MobileDevice/Provisioning Profiles/“ mit UUID als Dateinamen – und das reicht aus, damit Xcode sie richtig erkennt. Es ist kein Hexenwerk, unserem Tool ein einfaches Plugin hinzuzufügen, um das bereitgestellte Verzeichnis zu durchlaufen und dies für jede darin gefundene .mobileprovision-Datei zu tun.

Export.plist-Erstellung

Die Erstellung von export.plist ist jedoch etwas schwieriger. Um eine ordnungsgemäße IPA-Datei zu generieren, verlangt Xcode von Benutzern, dass sie eine Plist-Datei mit spezifischen Informationen bereitstellen, die aus verschiedenen Quellen gesammelt wurden – der Projektdatei, der Berechtigungs-Plist, den Arbeitsbereichseinstellungen usw. Der Grund, warum Xcode diese Daten nur über den Verteilungsassistenten sammeln kann, nicht jedoch über die CLI ist mir unbekannt. Wir sollten sie jedoch mithilfe von Swift-APIs sammeln, wobei wir nur Projekt-/Arbeitsbereichsreferenzen und ein wenig Wissen darüber hatten, wie die Xcode-Projektdatei erstellt wird.

Das Ergebnis war besser als erwartet, daher haben wir beschlossen, es als weiteres Plugin zu unserem Tool hinzuzufügen. Wir haben es auch als Open-Source-Projekt für ein breiteres Publikum veröffentlicht. Derzeit ist MQSwiftSign ein Mehrzwecktool, das erfolgreich als Ersatz für grundlegende Fastlane-Aktionen eingesetzt werden kann, die zum Erstellen und Verteilen Ihrer iOS-Anwendung erforderlich sind. Wir verwenden es in jedem unserer Projekte in Miquido.

Abschließende Gedanken: Der Erfolg

Der Wechsel von der Intel- zur iOS-ARM-Architektur war eine anspruchsvolle Aufgabe. Wir standen vor zahlreichen Hindernissen und verbrachten aufgrund mangelnder Dokumentation viel Zeit mit der Entwicklung von Tools. Letztlich haben wir jedoch ein robustes System etabliert:

  • Es sind zwei statt neun Läufer zu bewältigen;
  • Ausführen von Software, die vollständig unter unserer Kontrolle steht, ohne großen Overhead in Form von Rubygems – wir konnten Fastlane oder Software von Drittanbietern in unseren Build-Konfigurationen loswerden;
  • VIEL Wissen und Verständnis für Dinge, denen wir normalerweise keine Aufmerksamkeit schenken – wie die Systemsicherheit von macOS und das Sicherheitsframework selbst, eine tatsächliche Xcode-Projektstruktur und vieles mehr.

Ich würde Sie gerne ermutigen – Wenn Sie Schwierigkeiten haben, Ihren GitLab-Runner für iOS-Builds einzurichten, probieren Sie unseren MQVMRunner aus. Wenn Sie Hilfe beim Erstellen und Verteilen Ihrer App mit einem einzigen Tool benötigen und sich nicht auf Rubygems verlassen möchten, probieren Sie MQSwiftSign aus. Funktioniert bei mir, funktioniert möglicherweise auch bei Ihnen!