Sistemi CI per lo sviluppo iOS: trasformazione da Intel ad ARM

Pubblicato: 2024-02-14

Nel panorama tecnologico in continua evoluzione, le aziende devono adattarsi ai venti del cambiamento per rimanere rilevanti e competitive. Una di queste trasformazioni che ha preso d'assalto il mondo tecnologico è la transizione dall'architettura Intel x86_64 all'architettura ARM iOS, esemplificata dall'innovativo chip Apple M1 di Apple. In questo contesto, i sistemi CI per iOS sono diventati una considerazione cruciale per le aziende che affrontano questo cambiamento, garantendo che i processi di sviluppo e test del software rimangano efficienti e aggiornati con gli standard tecnologici più recenti.

Apple ha annunciato i suoi chip M1 quasi tre anni fa e da allora è stato chiaro che l'azienda avrebbe abbracciato l'architettura ARM e alla fine avrebbe abbandonato il supporto per il software basato su Intel. Per mantenere la compatibilità tra le architetture, Apple ha introdotto una nuova versione di Rosetta, il suo framework proprietario di traduzione binaria, che si è dimostrato affidabile in passato durante la significativa trasformazione dell’architettura da PowerPC a Intel nel 2006. La trasformazione è ancora in corso, e abbiamo visto Xcode perde il supporto Rosetta nella versione 14.3.

In Miquido abbiamo riconosciuto la necessità di migrare da Intel ad ARM alcuni anni fa. Abbiamo iniziato i preparativi a metà del 2021. Essendo una software house con più clienti, app e progetti in corso contemporaneamente, abbiamo dovuto affrontare alcune sfide che dovevamo superare. Questo articolo può essere la tua guida pratica se la tua azienda si trova ad affrontare situazioni simili. Le situazioni e le soluzioni descritte sono rappresentate dal punto di vista dello sviluppo iOS, ma potresti trovare approfondimenti adatti anche ad altre tecnologie. Una componente fondamentale della nostra strategia di transizione prevedeva che il nostro sistema CI per iOS fosse completamente ottimizzato per la nuova architettura, evidenziando l'importanza di I sistemi CI per iOS mantengono flussi di lavoro efficienti e output di alta qualità in un cambiamento così significativo.

Il problema: migrazione dell'architettura ARM da Intel a iOS

Apple ha rilasciato il chip M1 nel 2020

Il processo migratorio è stato diviso in due rami principali.

1. Sostituzione dei computer esistenti degli sviluppatori basati su Intel con i nuovi MacBook M1

Questo processo avrebbe dovuto essere relativamente semplice. Abbiamo stabilito una politica per sostituire gradualmente tutti i Macbook Intel dello sviluppatore nell'arco di due anni. Al momento, il 95% dei nostri dipendenti utilizza MacBook basati su ARM.

Tuttavia, durante questo processo abbiamo riscontrato alcune sfide inaspettate. A metà del 2021, una carenza di Mac M1 ha rallentato il nostro processo di sostituzione. Alla fine del 2021 eravamo riusciti a sostituire solo una manciata di Macbook dei quasi 200 in attesa. Abbiamo stimato che ci vorranno circa due anni per sostituire completamente tutti i Mac Intel dell'azienda con Macbook M1, compresi gli ingegneri non iOS.

Fortunatamente, Apple ha rilasciato i suoi nuovi chip M1 Pro e M2. Di conseguenza, abbiamo spostato la nostra attenzione dalla sostituzione degli Intel con i Mac M1 alla loro sostituzione con i chip M1 Pro e M2.

Il software non pronto per il passaggio ha causato la frustrazione degli sviluppatori

I primi ingegneri che hanno ricevuto i nuovi MacBook M1 hanno avuto difficoltà poiché la maggior parte del software non era pronta per passare alla nuova architettura iOS ARM di Apple. Strumenti di terze parti come Rubygems e Cocoapods, che sono strumenti di gestione delle dipendenze che si basano su molti altri Rubygems, sono stati i più colpiti. Alcuni di questi strumenti allora non erano compilati per l'architettura ARM di iOS, quindi la maggior parte del software doveva essere eseguita utilizzando Rosetta, causando problemi di prestazioni e frustrazione.

Tuttavia, i creatori di software hanno lavorato per risolvere la maggior parte di questi problemi non appena si sono presentati. Il momento decisivo è arrivato con il rilascio di Xcode 14.3, che non aveva più il supporto Rosetta. Questo è stato un chiaro segnale per tutti gli sviluppatori di software che Apple stava spingendo per una migrazione dell'architettura ARM da Intel a iOS. Ciò ha costretto la maggior parte degli sviluppatori di software di terze parti che in precedenza si erano affidati a Rosetta a migrare il proprio software su ARM. Al giorno d'oggi, il 99% del software di terze parti utilizzato quotidianamente da Miquido funziona senza Rosetta.

2. Sostituzione del sistema CI di Miquido per iOS

Sostituire il sistema iOS di integrazione continua presso Miquido si è rivelato un compito più complicato rispetto al semplice cambio di macchine. Per prima cosa, dai un'occhiata alla nostra infrastruttura di allora:

L'architettura CI a Miquido. Sistema CI per la trasformazione iOS.

Avevamo un'istanza cloud Gitlab e 9 Mac Mini basati su Intel collegati ad essa. Queste macchine fungevano da corridori di lavoro e Gitlab era responsabile dell'orchestrazione. Ogni volta che un lavoro CI veniva messo in coda, Gitlab lo assegnava al primo runner disponibile che soddisfaceva i requisiti del progetto specificati nel file gitlab-ci.yml. Gitlab creerebbe uno script di lavoro contenente tutti i comandi di compilazione, variabili, percorsi, ecc. Questo script veniva quindi spostato sul runner ed eseguito su quella macchina.

Sebbene questa configurazione possa sembrare solida, abbiamo riscontrato problemi con la virtualizzazione a causa dello scarso supporto dei processori Intel. Di conseguenza, abbiamo deciso di non utilizzare la virtualizzazione come Docker ed eseguire i lavori sulle stesse macchine fisiche. Abbiamo provato a impostare una soluzione efficiente e affidabile basata su Docker, ma i limiti di virtualizzazione, come la mancanza di accelerazione GPU, hanno comportato che i lavori richiedessero il doppio del tempo per l'esecuzione rispetto alle macchine fisiche. Ciò ha portato a maggiori spese generali e al rapido riempimento delle code.

A causa dello SLA di macOS, abbiamo potuto configurare solo due VM contemporaneamente. Pertanto, abbiamo deciso di estendere il pool di corridori fisici e di configurarli per eseguire lavori Gitlab direttamente sul loro sistema operativo. Tuttavia, questo approccio presentava anche alcuni inconvenienti.

Sfide nel processo di costruzione e nella gestione dei corridori

  1. Nessun isolamento delle build al di fuori della sandbox della directory di build.

Il runner esegue ogni build su una macchina fisica, il che significa che le build non sono isolate dalla sandbox della directory di build. Questo ha i suoi vantaggi e svantaggi. Da un lato, possiamo utilizzare le cache di sistema per velocizzare la compilazione poiché la maggior parte dei progetti utilizza lo stesso insieme di dipendenze di terze parti.

D'altra parte, la cache diventa ingestibile poiché gli avanzi di un progetto possono influenzare tutti gli altri progetti. Ciò è particolarmente importante per le cache a livello di sistema, poiché gli stessi runner vengono utilizzati sia per lo sviluppo Flutter che per quello React Native. React Native, in particolare, richiede molte dipendenze memorizzate nella cache tramite NPM.

  1. Potenziale disordine degli strumenti di sistema.

Sebbene nessuno dei due lavori fosse stato eseguito con privilegi sudo, era ancora possibile per loro accedere ad alcuni strumenti del sistema o dell'utente, come Ruby. Ciò rappresentava una potenziale minaccia di danneggiare alcuni di questi strumenti, soprattutto perché macOS utilizza Ruby per alcuni dei suoi software legacy, comprese alcune funzionalità Xcode legacy. La versione di sistema di Ruby non è qualcosa con cui vorresti scherzare.

Tuttavia, l'introduzione di rbenv crea un ulteriore livello di complessità da affrontare. È importante notare che i Rubygem sono installati in base alla versione di Ruby e alcuni di questi gem richiedono versioni specifiche di Ruby. Quasi tutti gli strumenti di terze parti che utilizzavamo dipendevano da Ruby, con Cocoapods e Fastlane come attori principali.

  1. Gestione delle identità di firma.

Gestire più identità di firma da vari account di sviluppo cliente può rappresentare una sfida quando si tratta dei portachiavi di sistema sui corridori. L'identità della firma è un dato altamente sensibile in quanto ci consente di coprogettare l'applicazione, rendendola vulnerabile a potenziali minacce.

Per garantire la sicurezza, le identità dovrebbero essere inserite in sandbox tra i progetti e protette. Tuttavia, questo processo può diventare un incubo considerando la complessità aggiuntiva introdotta da macOS nell’implementazione del portachiavi.

  1. Sfide in ambienti multi-progetto.

Non tutti i progetti sono stati creati utilizzando gli stessi strumenti, in particolare Xcode. Alcuni progetti, soprattutto quelli in fase di supporto, sono stati mantenuti utilizzando l'ultima versione di Xcode con cui è stato sviluppato il progetto. Ciò significa che se fosse necessario del lavoro su quei progetti, l’IC doveva essere in grado di realizzarlo. Di conseguenza, i corridori dovevano supportare più versioni di Xcode contemporaneamente, il che riduceva di fatto il numero di corridori disponibili per un particolare lavoro.

5. È richiesto uno sforzo extra.

Qualsiasi modifica apportata ai corridori, come l'installazione del software, deve essere eseguita su tutti i corridori contemporaneamente. Sebbene disponessimo di uno strumento di automazione per questo, è stato necessario uno sforzo aggiuntivo per mantenere gli script di automazione.

Soluzioni infrastrutturali personalizzate per le diverse esigenze dei clienti

Miquido è una software house che lavora con molteplici clienti con esigenze diverse. Personalizziamo i nostri servizi per soddisfare le esigenze specifiche di ogni cliente. Spesso ospitiamo la codebase e l'infrastruttura necessaria per piccole imprese o start-up poiché potrebbero non avere le risorse o le conoscenze per mantenerla.

I clienti aziendali solitamente dispongono della propria infrastruttura per ospitare i propri progetti. Tuttavia, alcuni non hanno la capacità di farlo o sono obbligati dalle normative di settore a utilizzare la propria infrastruttura. Preferiscono inoltre non utilizzare servizi SaaS di terze parti come Xcode Cloud o Codemagic. Vogliono invece una soluzione che si adatti alla loro architettura esistente.

Per soddisfare questi clienti, spesso ospitiamo i progetti sulla nostra infrastruttura o impostiamo la stessa configurazione iOS di integrazione continua sulla loro infrastruttura. Tuttavia, prestiamo particolare attenzione quando gestiamo informazioni e file sensibili, come la firma delle identità.

Sfruttare Fastlane per una gestione efficiente della costruzione

Qui Fastlane si presenta come uno strumento utile. È composto da vari moduli chiamati azioni che aiutano a semplificare il processo e a separarlo tra diversi client. Una di queste azioni, denominata match, aiuta a mantenere le identità di firma di sviluppo e produzione, nonché i profili di provisioning. Funziona anche a livello del sistema operativo per separare tali identità in portachiavi separati durante la fase di creazione ed esegue una pulizia dopo la creazione, il che è estremamente utile perché eseguiamo tutte le nostre build su macchine fisiche.

Fastlane: strumento di automazione dello sviluppo
Crediti immagine: Fastlane

Inizialmente ci siamo rivolti a Fastlane per un motivo specifico, ma abbiamo scoperto che aveva funzionalità aggiuntive che potevano esserci utili.

  1. Crea Carica su Testflight

In passato, l'API AppStoreConnect non era disponibile pubblicamente per gli sviluppatori. Ciò significava che l'unico modo per caricare una build su Testflight era tramite Xcode o utilizzando Fastlane. Fastlane era uno strumento che essenzialmente raschiava l'API ASC e la trasformava in un'azione chiamata pilot . Tuttavia, questo metodo spesso si interrompeva con il successivo aggiornamento di Xcode. Se uno sviluppatore desiderava caricare la propria build su Testflight utilizzando la riga di comando, Fastlane era la migliore opzione disponibile.

  1. Passaggio facile tra le versioni di Xcode

Avendo più di un'istanza Xcode su una singola macchina, era necessario selezionare quale Xcode utilizzare per la build. Sfortunatamente, Apple ha reso scomodo il passaggio da una versione all'altra di Xcode: per farlo è necessario utilizzare "xcode-select", che richiede inoltre i privilegi sudo. Fastlane copre anche questo.

  1. Utilità aggiuntive per gli sviluppatori

Fastlane fornisce molte altre utili utilità tra cui il controllo delle versioni e la possibilità di inviare i risultati della creazione ai webhook.

Gli svantaggi di Fastlane

Adattare Fastlane ai nostri progetti è stato valido e solido, quindi siamo andati in quella direzione. Lo usiamo con successo da diversi anni. Tuttavia, nel corso di questi anni, abbiamo individuato alcuni problemi:

  1. Fastlane richiede la conoscenza di Ruby.

Fastlane è uno strumento scritto in Ruby e richiede una buona conoscenza di Ruby per utilizzarlo in modo efficace. Quando sono presenti bug nella configurazione di Fastlane o nello strumento stesso, eseguirne il debug utilizzando irb o pry può essere piuttosto impegnativo.

  1. Dipendenza da numerose gemme.

La stessa Fastlane si basa su circa 70 gemme. Per mitigare i rischi di rottura del sistema Ruby, i progetti utilizzavano gemme bundler locali. Recuperare tutte queste gemme richiedeva molto tempo.

  1. Problemi con System Ruby e Rubygems.

Di conseguenza, tutti i problemi menzionati in precedenza con il sistema Ruby e rubygems sono applicabili anche qui.

  1. Ridondanza per progetti Flutter.

Anche i progetti Flutter sono stati costretti a utilizzare la corrispondenza fastlane solo per preservare la compatibilità con i progetti iOS e proteggere i portachiavi dei corridori. Ciò era assurdamente inutile, poiché Flutter ha un proprio sistema di build integrato e il sovraccarico menzionato in precedenza è stato introdotto solo per gestire le identità di firma e i profili di provisioning.

La maggior parte di questi problemi sono stati risolti strada facendo, ma avevamo bisogno di una soluzione più solida e affidabile.

L'idea: adattare strumenti di integrazione continua nuovi e più robusti per iOS

La buona notizia è che Apple ha acquisito il pieno controllo sull’architettura dei suoi chip e ha sviluppato un nuovo framework di virtualizzazione per macOS. Questo framework consente agli utenti di creare, configurare ed eseguire macchine virtuali Linux o macOS che si avviano rapidamente e si caratterizzano per prestazioni di tipo nativo – e intendo davvero di tipo nativo.

Sembrava promettente e potrebbe essere una pietra angolare per i nostri nuovi strumenti di integrazione continua per iOS. Tuttavia, era solo una parte della soluzione completa. Avendo uno strumento di gestione delle VM, avevamo anche bisogno di qualcosa che potesse utilizzare quel framework in coordinamento con i nostri corridori Gitlab.

Detto questo, la maggior parte dei nostri problemi relativi alle scarse prestazioni di virtualizzazione diventerebbero obsoleti. Ci consentirebbe inoltre di risolvere automaticamente la maggior parte dei problemi che intendevamo risolvere con Fastlane.

Sviluppo di una soluzione su misura per la gestione dell'identità della firma on-demand

Avevamo un ultimo problema da risolvere: la gestione dell'identità della firma. Non volevamo utilizzare Fastlane per questo perché ci sembrava eccessivo per le nostre esigenze. Cercavamo invece una soluzione più adatta alle nostre esigenze. Le nostre esigenze erano semplici: il processo di gestione delle identità doveva essere eseguito su richiesta, esclusivamente per il momento della creazione, senza identità preinstallate sul portachiavi, ed essere compatibile con qualsiasi macchina su cui sarebbe stato eseguito.

Il problema della distribuzione e la mancanza di un'API AppstoreConnect stabile sono diventati obsoleti quando Apple ha rilasciato il suo "altool", che consentiva la comunicazione tra utenti e ASC.

Quindi abbiamo avuto un'idea e abbiamo dovuto trovare un modo per collegare insieme questi tre aspetti:

  1. Trovare un modo per utilizzare il framework di virtualizzazione di Apple.
  2. Farlo funzionare con i runner Gitlab.
  3. Trovare una soluzione per firmare la gestione delle identità su più progetti e corridori.

La soluzione: uno sguardo al nostro approccio (strumenti inclusi)

Abbiamo iniziato a cercare soluzioni per affrontare tutti i problemi menzionati in precedenza.

  1. Utilizzando il framework di virtualizzazione di Apple.

Per il primo ostacolo abbiamo trovato una soluzione abbastanza velocemente: ci siamo imbattuti nello strumento Tar di Cirrus Labs. Fin dal primo momento sapevamo che questa sarebbe stata la nostra scelta.

I vantaggi più significativi derivanti dall'utilizzo dello strumento tart offerto da Cirrus Lab sono:

  • La possibilità di creare macchine virtuali da immagini .ipsw grezze.
  • La possibilità di creare macchine virtuali utilizzando modelli preconfezionati (con alcuni strumenti di utilità installati, come brew o Xcode), disponibili sulla pagina GitHub di Cirrus Labs.
  • Lo strumento Tart utilizza packer per il supporto dinamico della creazione di immagini.
  • Lo strumento Tart supporta sia immagini Linux che MacOS.
  • Lo strumento utilizza una funzionalità eccezionale del file system APFS che consente la duplicazione dei file senza riservare effettivamente spazio su disco per essi. In questo modo, non è necessario allocare spazio su disco pari a 3 volte la dimensione originale dell'immagine. Hai solo bisogno di spazio su disco sufficiente per l'immagine originale, mentre il clone occupa solo lo spazio che differisce tra esso e l'immagine originale. Questo è incredibilmente utile, soprattutto perché le immagini macOS tendono ad essere piuttosto grandi.

Ad esempio, un'immagine operativa di macOS Ventura con Xcode e altre utilità installate richiede un minimo di 60 GB di spazio su disco. In circostanze normali, un'immagine e due dei suoi cloni occuperebbero fino a 180 GB di spazio su disco, una quantità significativa. E questo è solo l'inizio, poiché potresti voler avere più di un'immagine originale o installare più versioni di Xcode su una singola VM, il che aumenterebbe ulteriormente le dimensioni.

  • Lo strumento consente la gestione degli indirizzi IP per le VM originali e clonate, consentendo l'accesso SSH alle VM.
  • La possibilità di eseguire il montaggio incrociato di directory tra la macchina host e le macchine virtuali.
  • Lo strumento è facile da usare e ha una CLI molto semplice.

Non c'è quasi nulla che manchi a questo strumento in termini di utilizzo per la gestione delle VM. Quasi nulla, tranne una cosa: anche se promettente, il plugin packer per costruire le immagini al volo richiedeva troppo tempo, quindi abbiamo deciso di non usarlo.

Abbiamo provato la crostata e ha funzionato in modo fantastico. Le sue prestazioni erano quelle native e la gestione era semplice.

Dopo aver integrato con successo la crostata ottenendo risultati impressionanti, ci siamo poi concentrati sull'affrontare altre sfide.

  1. Trovare un modo per combinare la crostata con i corridori Gitlab.

Dopo aver risolto il primo problema, abbiamo affrontato la questione di come combinare tart con i runner Gitlab.

Iniziamo descrivendo cosa fanno effettivamente i corridori Gitlab:

Il diagramma semplificato della delega del lavoro Gitlab. Sistema CI per iOS

Avevamo bisogno di includere un ulteriore enigma nel diagramma, che prevedeva l'assegnazione delle attività dall'host runner alla VM. Il lavoro GitLab è uno script di shell che contiene variabili cruciali, voci PATH e comandi.

Il nostro obiettivo era trasferire questo script sulla VM ed eseguirlo.

Tuttavia, questo compito si è rivelato più impegnativo di quanto pensassimo inizialmente.

Il corridore

Gli esecutori runner Gitlab standard come Docker o SSH sono semplici da configurare e richiedono poca o nessuna configurazione. Avevamo però bisogno di un maggiore controllo sulla configurazione, il che ci ha portato ad esplorare gli esecutori personalizzati forniti da GitLab.

Gli esecutori personalizzati sono un'ottima opzione per configurazioni non standard poiché ogni passaggio del runner (preparazione, esecuzione, pulizia) è descritto sotto forma di uno script di shell. L'unica cosa che mancava era uno strumento da riga di comando che potesse eseguire le attività di cui avevamo bisogno ed essere eseguito negli script di configurazione del runner.

Attualmente sono disponibili un paio di strumenti che fanno esattamente questo, ad esempio l’esecutore Gitlab tart di CirrusLabs. Questo strumento è esattamente quello che stavamo cercando in quel momento. Tuttavia, non esisteva ancora e, dopo aver condotto le ricerche, non abbiamo trovato nessuno strumento che potesse aiutarci a svolgere il nostro compito.

Scrivere la propria soluzione

Poiché non siamo riusciti a trovare una soluzione perfetta, ne abbiamo scritta una noi stessi. Dopotutto siamo ingegneri! L'idea sembrava solida e avevamo tutti gli strumenti necessari, quindi abbiamo proceduto con lo sviluppo.

Abbiamo scelto di utilizzare Swift e un paio di librerie open source fornite da Apple: Swift Argument Parser per gestire l'esecuzione della riga di comando e Swift NIO per gestire la connessione SSH con le VM. Abbiamo iniziato lo sviluppo e in un paio di giorni abbiamo ottenuto il primo prototipo funzionante di uno strumento che alla fine si è evoluto in MQVMRunner.

Infrastruttura CI iOS: MQVMRunner

Ad alto livello, lo strumento funziona come segue:

  1. (Passaggio Prepara)
    1. Leggi le variabili fornite in gitlab-ci.yml (nome dell'immagine e variabili aggiuntive).
    2. Scegli la base VM richiesta
    3. Clona la base VM richiesta.
    4. Configura una directory con montaggio incrociato e copia lo script del lavoro Gitlab, impostando le autorizzazioni necessarie per esso.
    5. Esegui il clone e controlla la connessione SSH.
    6. Configura tutte le dipendenze richieste (come la versione Xcode), se necessario.
  2. (Esegui passo)
    1. Esegui il lavoro Gitlab eseguendo uno script da una directory montata su un clone di VM preparato tramite SSH.
  3. (Passaggio di pulizia)
    1. Elimina l'immagine clonata.

Sfide nello sviluppo

Durante lo sviluppo abbiamo riscontrato diversi problemi che hanno impedito che il gioco non procedesse così bene come avremmo voluto.

  1. Gestione degli indirizzi IP.

La gestione degli indirizzi IP è un compito cruciale che deve essere gestito con cura. Nel prototipo, la gestione SSH è stata implementata utilizzando comandi shell SSH diretti e codificati. Tuttavia, nel caso di shell non interattive, si consiglia l'autenticazione con chiave. Inoltre, è consigliabile aggiungere l'host al fileknown_hosts per evitare interruzioni. Tuttavia, a causa della gestione dinamica degli indirizzi IP delle macchine virtuali, esiste la possibilità di raddoppiare la voce per un determinato IP, causando errori. Pertanto, dobbiamo assegnare iknown_hosts dinamicamente per un particolare lavoro per evitare tali problemi.

  1. Soluzione Swift pura.

Considerando questo, e il fatto che i comandi shell codificati nel codice Swift non sono molto eleganti, abbiamo pensato che sarebbe stato carino utilizzare una libreria Swift dedicata e abbiamo deciso di utilizzare Swift NIO. Abbiamo risolto alcuni problemi ma allo stesso tempo ne abbiamo introdotti un paio di nuovi come, ad esempio, a volte i log posizionati su stdout venivano trasferiti *dopo* che il canale SSH veniva terminato a causa della fine dell'esecuzione del comando - e, poiché ci basavamo su quell'output nel lavoro successivo, l'esecuzione falliva casualmente.

  1. Selezione della versione Xcode.

Poiché il plug-in Packer non era un'opzione per la creazione di immagini dinamiche a causa del consumo di tempo, abbiamo deciso di utilizzare un'unica base VM con più versioni Xcode preinstallate. Dovevamo trovare un modo per consentire agli sviluppatori di specificare la versione di Xcode di cui avevano bisogno nel loro gitlab-ci.yml e abbiamo creato variabili personalizzate disponibili da utilizzare in qualsiasi progetto. MQVMRunner eseguirà quindi "xcode-select" su una VM clonata per configurare la versione Xcode corrispondente.

E molti, molti altri

Semplificazione della migrazione dei progetti e integrazione continua per il flusso di lavoro iOS con Mac Studios

Lo avevamo configurato su due nuovi Mac Studios e abbiamo iniziato la migrazione dei progetti. Volevamo rendere il processo di migrazione per i nostri sviluppatori il più trasparente possibile. Non siamo riusciti a renderlo del tutto fluido, ma alla fine siamo arrivati ​​al punto in cui dovevano fare solo un paio di cose in gitlab-ci.yml:

  • I tag dei corridori: usare Mac Studios invece di Intel.
  • Il nome dell'immagine: parametro opzionale, introdotto per compatibilità futura nel caso in cui serva più di una VM base. Al momento, per impostazione predefinita viene sempre utilizzata la VM a base singola di cui disponiamo.
  • La versione di Xcode: parametro opzionale; se non fornita, verrà utilizzata la versione più recente disponibile.

Lo strumento ha ricevuto un ottimo riscontro iniziale, quindi abbiamo deciso di renderlo open source. Abbiamo aggiunto uno script di installazione per configurare Gitlab Custom Runner e tutte le azioni e variabili richieste. Utilizzando il nostro strumento, puoi configurare il tuo runner GitLab in pochi minuti: l'unica cosa di cui hai bisogno è la VM iniziale e di base su cui verranno eseguiti i lavori.

La struttura di integrazione continua finale per iOS è la seguente:

L'infrastruttura CI finale: MQVMRunner

3. Soluzione per una gestione efficiente delle identità

Abbiamo lottato per trovare una soluzione efficiente per la gestione delle identità di firma dei nostri clienti. Ciò è stato particolarmente impegnativo, poiché l’identità della firma è un dato altamente confidenziale che non dovrebbe essere archiviato in un luogo non sicuro più a lungo del necessario.

Inoltre, volevamo caricare queste identità solo durante la fase di creazione, senza soluzioni tra progetti. Ciò significava che l'identità non doveva essere accessibile al di fuori della sandbox dell'app (o della build). Abbiamo già affrontato quest'ultimo problema passando alle VM. Tuttavia, dovevamo ancora trovare un modo per archiviare e caricare l'identità di firma nella VM solo per il momento della compilazione.

Problemi con Fastlane Match

All'epoca utilizzavamo ancora la corrispondenza Fastlane, che memorizza identità e disposizioni crittografate in un repository separato, li carica durante il processo di creazione in un'istanza di portachiavi separata e rimuove tale istanza dopo la creazione.

Questo approccio sembra conveniente, ma presenta alcuni problemi:

  • Richiede l'intera configurazione Fastlane per funzionare.

Fastlane è Rubygem e qui si applicano tutti i problemi elencati nel primo capitolo.

  • Checkout del repository in fase di compilazione.

Abbiamo mantenuto le nostre identità in un repository separato che è stato estratto durante il processo di creazione anziché durante il processo di installazione. Ciò significava che dovevamo stabilire un accesso separato al repository di identità, non solo per Gitlab, ma per i corridori specifici, in modo simile a come gestiremmo le dipendenze private di terze parti.

  • Difficile da gestire al di fuori della partita.

Se utilizzi Match per la gestione delle identità o il provisioning, la necessità di intervento manuale è minima o nulla. Modificare, decrittografare e crittografare manualmente i profili in modo che le corrispondenze possano ancora funzionare con essi in seguito è noioso e richiede tempo. L'utilizzo di Fastlane per eseguire questo processo di solito comporta la cancellazione completa della configurazione del provisioning dell'applicazione e la creazione di una nuova.

  • Un po' difficile da eseguire il debug.

In caso di problemi di firma del codice, potrebbe essere difficile determinare l'identità e la corrispondenza di provisioning appena installati, poiché dovresti prima decodificarli.

  • Problemi di sicurezza.

Abbina gli account sviluppatore a cui si accede utilizzando le credenziali fornite per apportare modifiche per loro conto. Nonostante Fastlane sia open source, alcuni clienti lo hanno rifiutato per motivi di sicurezza.

  • Ultimo ma non meno importante, eliminare Match eliminerebbe l’ostacolo più grande sulla nostra strada verso l’eliminazione completa di Fastlane.

I nostri requisiti iniziali erano i seguenti:

  • Il caricamento richiede la firma dell'identità da un luogo sicuro, preferibilmente in formato non semplice, e l'inserimento nel portachiavi.
  • Tale identità dovrebbe essere accessibile da Xcode.
  • Preferibilmente, le variabili identità password, nome portachiavi e password portachiavi dovrebbero essere impostabili per scopi di debug.

Match aveva tutto ciò di cui avevamo bisogno, ma implementare Fastlane solo per utilizzare Match sembrava eccessivo, soprattutto per le soluzioni multipiattaforma con il proprio sistema di build. Volevamo qualcosa di simile a Match, ma senza il pesante fardello di Ruby che portava.

Creare la propria soluzione

Quindi abbiamo pensato: scriviamolo noi stessi! Lo abbiamo fatto con MQVMRunner, quindi potremmo farlo anche qui. Anche noi abbiamo scelto Swift per farlo, soprattutto perché potremmo ottenere gratuitamente molte API necessarie utilizzando il framework Apple Security.

Naturalmente, anche le cose non sono andate bene come previsto.

  • Quadro di sicurezza in atto.

La strategia più semplice era chiamare i comandi bash come fa Fastlane. Tuttavia, avendo a disposizione il framework di sicurezza, abbiamo pensato che sarebbe stato più elegante utilizzarlo per lo sviluppo.

  • Mancanza di esperienza.

Non avevamo molta esperienza con il framework di sicurezza per macOS e si è scoperto che differiva notevolmente da quello a cui eravamo abituati su iOS. Ciò si è ritorto contro di noi in molti casi in cui non eravamo a conoscenza delle limitazioni di macOS o presumevamo che funzionasse come su iOS: la maggior parte di queste ipotesi erano sbagliate.

  • Documentazione terribile.

La documentazione del framework di sicurezza Apple è, per usare un eufemismo, umile. Si tratta di un'API molto vecchia, che risale alle prime versioni di OSX e, a volte, abbiamo avuto l'impressione che da allora non sia stata più aggiornata. Una grossa porzione di codice non è documentata, ma ne abbiamo anticipato il funzionamento leggendo il codice sorgente. Fortunatamente per noi, è open source.

  • Deprecazioni senza sostituzioni.

Una buona parte di questo framework è deprecato; Apple sta cercando di allontanarsi dal tipico portachiavi “stile macOS” (portachiavi multipli accessibili tramite password) e implementare il portachiavi “stile iOS” (portachiavi singolo, sincronizzato tramite iCloud). Quindi l'hanno deprecato in macOS Yosemite nel 2014, ma non ne hanno trovato alcun sostituto negli ultimi nove anni, quindi l'unica API disponibile per noi, per ora, è deprecata perché non ce n'è ancora una nuova.

Abbiamo presupposto che le identità di firma possano essere archiviate come stringhe codificate base64 nelle variabili Gitlab per progetto. È sicuro, basato sul progetto e, se impostato come variabile mascherata, può essere letto e visualizzato nei registri di compilazione come testo non normale.

Quindi, avevamo i dati di identità. Dovevamo solo inserirlo nel portachiavi. Utilizzo dell'API di sicurezza Dopo una manciata di tentativi e un lungo periodo di analisi della documentazione del framework di sicurezza, abbiamo preparato un prototipo di qualcosa che in seguito è diventato MQSwiftSign.

Imparare il sistema di sicurezza macOS, ma nel modo più duro

Abbiamo dovuto acquisire una conoscenza approfondita del funzionamento del portachiavi macOS per sviluppare il nostro strumento. Ciò ha comportato la ricerca su come il portachiavi gestisce gli elementi, il loro accesso e le autorizzazioni e la struttura dei dati del portachiavi. Ad esempio, abbiamo scoperto che il portachiavi è l'unico file macOS il cui sistema operativo ignora il set ACL. Inoltre, abbiamo appreso che le ACL su elementi portachiavi specifici sono un plist di testo semplice salvato in un file portachiavi. Abbiamo affrontato diverse sfide lungo il percorso, ma abbiamo anche imparato molto.

Una sfida significativa che abbiamo riscontrato è stata quella dei prompt. Il nostro strumento è stato progettato principalmente per essere eseguito sui sistemi iOS CI, il che significava che doveva essere non interattivo. Non potevamo chiedere agli utenti di confermare una password sul CI.

Tuttavia, il sistema di sicurezza di macOS è ben progettato, rendendo impossibile modificare o leggere informazioni riservate, inclusa l'identità della firma, senza l'esplicita autorizzazione dell'utente. Per accedere a una risorsa senza conferma, il programma che accede deve essere incluso nell'elenco di controllo degli accessi della risorsa. Questo è un requisito rigoroso che nessun programma può interrompere, nemmeno i programmi Apple forniti con il sistema. Se un programma deve leggere o modificare una voce del portachiavi, l'utente deve fornire una password del portachiavi per sbloccarlo e, facoltativamente, aggiungerla all'ACL della voce.

Superare le sfide legate ai permessi degli utenti

Quindi, abbiamo dovuto trovare un modo per consentire a Xcode di accedere a un'identità impostata dal nostro portachiavi senza chiedere l'autorizzazione a un utente utilizzando la richiesta della password. Per fare ciò, possiamo modificare l'elenco di controllo degli accessi di un elemento, ma ciò richiede anche l'autorizzazione dell'utente – e, ovviamente, lo fa. Altrimenti, minerebbe l’intero scopo di avere l’ACL. Abbiamo cercato di aggirare questa protezione: abbiamo cercato di ottenere lo stesso effetto del comando "security set-key-partition-list".

Dopo un'analisi approfondita della documentazione del framework, non abbiamo trovato alcuna API che consenta di modificare l'ACL senza chiedere all'utente di fornire una password. La cosa più simile che abbiamo trovato è "SecKeychainItemSetAccess", che attiva ogni volta un prompt dell'interfaccia utente. Poi abbiamo fatto un altro tuffo, ma questa volta nella migliore documentazione, ovvero il codice sorgente stesso. Come lo ha implementato Apple?

Si è scoperto che, come era prevedibile, stavano utilizzando un'API privata. Un metodo chiamato "SecKeychainItemSetAccessWithPassword" fa sostanzialmente la stessa cosa di "SecKeychainItemSetAccess", ma invece di richiedere all'utente una password, la password viene fornita come argomento a una funzione. Naturalmente, in quanto API privata, non è elencata nella documentazione, ma ad Apple manca la documentazione per tali API, come se non potessero pensare di creare un'app per uso personale o aziendale. Poiché lo strumento doveva essere esclusivamente per uso interno, non abbiamo esitato a utilizzare l'API privata. L'unica cosa che doveva essere fatta era collegare il metodo C a Swift.

Superare le sfide legate ai permessi degli utenti

Quindi, il flusso di lavoro finale del prototipo è stato il seguente:

  1. Crea il portachiavi sbloccato temporaneo con il blocco automatico disattivato.
  2. Ottieni e decodifica i dati di identità della firma codificati base64 dalle variabili ambientali (trasmessi da Gitlab).
  3. Importa l'identità nel portachiavi creato.
  4. Imposta le opzioni di accesso adeguate per l'identità importata in modo che Xcode e altri strumenti possano leggerla per la coprogettazione.

Ulteriori aggiornamenti

Il prototipo funzionava bene, quindi abbiamo identificato alcune funzionalità aggiuntive che vorremmo aggiungere allo strumento. Il nostro obiettivo era quello di sostituire prima o poi la fastlane; abbiamo già implementato l'azione "match". Tuttavia, fastlane offriva ancora due preziose funzionalità che non avevamo ancora: l'installazione del profilo di provisioning e la creazione di export.plist.

Installazione del profilo di provisioning

L'installazione del profilo di provisioning è piuttosto semplice: si riduce all'estrazione dell'UUID del profilo e alla copia del file in "~/Library/MobileDevice/Provisioning Profiles/" con UUID come nome file - e questo è sufficiente affinché Xcode lo veda correttamente. Non è una scienza missilistica aggiungere al nostro strumento un semplice plugin per eseguire il loop della directory fornita e farlo per ogni file .mobileprovision che trova al suo interno.

Creazione Export.plist

La creazione di export.plist, tuttavia, è leggermente più complicata. Per generare un file IPA corretto, Xcode richiede agli utenti di fornire un file plist con informazioni specifiche raccolte da varie fonti: file di progetto, plist di autorizzazione, impostazioni dell'area di lavoro, ecc. Il motivo per cui Xcode può raccogliere tali dati solo tramite la procedura guidata di distribuzione ma non tramite la CLI non mi è noto. Tuttavia, dovevamo raccoglierli utilizzando le API Swift, avendo solo riferimenti a progetti/spazi di lavoro e una piccola dose di conoscenza su come viene creato il file di progetto Xcode.

Il risultato è stato migliore del previsto, quindi abbiamo deciso di aggiungerlo come un altro plugin al nostro strumento. Lo abbiamo anche rilasciato come progetto open source per un pubblico più ampio. Al momento, MQSwiftSign è uno strumento multiuso che può essere utilizzato con successo in sostituzione delle azioni fastlane di base necessarie per creare e distribuire la tua applicazione iOS e lo utilizziamo in ogni nostro progetto in Miquido.

Considerazioni finali: il successo

Il passaggio dall'architettura Intel all'architettura ARM iOS è stato un compito impegnativo. Abbiamo dovuto affrontare numerosi ostacoli e dedicare molto tempo allo sviluppo degli strumenti a causa della mancanza di documentazione. Tuttavia, alla fine abbiamo creato un sistema robusto:

  • Due corridori da gestire invece di nove;
  • Eseguendo software interamente sotto il nostro controllo, senza un sacco di spese generali sotto forma di rubygems: siamo stati in grado di sbarazzarci di fastlane o di qualsiasi software di terze parti nelle nostre configurazioni di build;
  • MOLTA conoscenza e comprensione di cose a cui di solito non prestiamo attenzione, come la sicurezza del sistema macOS e il framework di sicurezza stesso, un'effettiva struttura del progetto Xcode e molto, molto altro ancora.

Ti incoraggio volentieri: se hai difficoltà a configurare il tuo runner GitLab per build iOS, prova il nostro MQVMRunner. Se hai bisogno di aiuto per creare e distribuire la tua app utilizzando un unico strumento e non vuoi fare affidamento su rubygems, prova MQSwiftSign. Funziona per me, potrebbe funzionare anche per te!