Sistemas CI para desenvolvimento iOS: transformação Intel para ARM
Publicados: 2024-02-14No cenário em constante evolução da tecnologia, as empresas devem adaptar-se aos ventos da mudança para permanecerem relevantes e competitivas. Uma dessas transformações que conquistou o mundo da tecnologia é a transição da arquitetura Intel x86_64 para a arquitetura iOS ARM, exemplificada pelo inovador chip Apple M1 da Apple. Neste contexto, os sistemas CI para iOS tornaram-se uma consideração crucial para as empresas que navegam nesta mudança, garantindo que os processos de desenvolvimento e teste de software permanecem eficientes e atualizados com os mais recentes padrões tecnológicos.
A Apple anunciou seus chips M1 há quase três anos e, desde então, ficou claro que a empresa adotaria a arquitetura ARM e eventualmente abandonaria o suporte para software baseado em Intel. Para manter a compatibilidade entre arquiteturas, a Apple lançou uma nova versão do Rosetta, sua estrutura proprietária de tradução binária, que se mostrou confiável no passado durante a significativa transformação da arquitetura de PowerPC para Intel em 2006. A transformação ainda está em andamento e vimos O Xcode perde o suporte Rosetta na versão 14.3.
Na Miquido, reconhecemos a necessidade de migrar da Intel para a ARM há alguns anos. Iniciamos os preparativos em meados de 2021. Como uma software house com vários clientes, aplicativos e projetos em andamento simultaneamente, enfrentamos alguns desafios que tivemos que superar. Este artigo pode ser seu guia prático caso sua empresa enfrente situações semelhantes. As situações e soluções descritas são descritas a partir de uma perspectiva de desenvolvimento iOS – mas você também pode encontrar insights adequados para outras tecnologias. Um componente crítico de nossa estratégia de transição envolveu garantir que nosso sistema de CI para iOS fosse totalmente otimizado para a nova arquitetura, destacando a importância de Sistemas CI para iOS na manutenção de fluxos de trabalho eficientes e resultados de alta qualidade em meio a uma mudança tão significativa.
O problema: migração da arquitetura Intel para iOS ARM
O processo de migração foi dividido em dois ramos principais.
1. Substituindo os computadores de desenvolvedores existentes baseados em Intel por novos Macbooks M1
Este processo deveria ser relativamente simples. Estabelecemos uma política para substituir gradualmente todos os Macbooks Intel do desenvolvedor ao longo de dois anos. Atualmente, 95% dos nossos funcionários usam Macbooks baseados em ARM.
No entanto, encontramos alguns desafios inesperados durante esse processo. Em meados de 2021, a escassez de Macs M1 atrasou nosso processo de substituição. No final de 2021, só conseguimos substituir alguns Macbooks dos quase 200 que estavam em espera. Estimamos que levaria cerca de dois anos para substituir totalmente todos os Macs Intel da empresa por Macbooks M1, incluindo engenheiros não iOS.
Felizmente, a Apple lançou seus novos chips M1 Pro e M2. Como resultado, mudamos nosso foco de substituir Intels por M1 Macs para substituí-los por chips M1 Pro e M2.
Software não pronto para a mudança causou frustração aos desenvolvedores
Os primeiros engenheiros que receberam os novos Macbooks M1 tiveram dificuldades, pois a maior parte do software não estava pronta para mudar para a nova arquitetura iOS ARM da Apple. Ferramentas de terceiros como Rubygems e Cocoapods, que são ferramentas de gerenciamento de dependências que dependem de muitos outros Rubygems, foram as mais afetadas. Algumas dessas ferramentas não foram compiladas para a arquitetura iOS ARM, então a maior parte do software teve que ser executada usando Rosetta, causando problemas de desempenho e frustração.
No entanto, os criadores de software trabalharam para resolver a maioria desses problemas à medida que surgiam. O momento inovador veio com o lançamento do Xcode 14.3, que não tinha mais suporte para Rosetta. Este foi um sinal claro para todos os desenvolvedores de software de que a Apple estava pressionando por uma migração da arquitetura Intel para iOS ARM. Isso forçou a maioria dos desenvolvedores de software terceirizados que anteriormente dependiam do Rosetta a migrar seu software para ARM. Hoje em dia, 99% do software de terceiros usado diariamente no Miquido funciona sem Rosetta.
2. Substituindo o sistema CI do Miquido para iOS
Substituir o sistema iOS de integração contínua em Miquido provou ser uma tarefa mais complicada do que apenas trocar máquinas. Primeiro, dê uma olhada em nossa infraestrutura naquela época:
Tínhamos uma instância de nuvem Gitlab e 9 Mac Minis baseados em Intel conectados a ela. Essas máquinas serviam como executores de tarefas e o Gitlab era responsável pela orquestração. Sempre que um trabalho de CI era colocado na fila, o Gitlab o atribuía ao primeiro executor disponível que atendesse aos requisitos do projeto especificados no arquivo gitlab-ci.yml. O Gitlab criaria um script de trabalho contendo todos os comandos de construção, variáveis, caminhos, etc. Esse script era então movido para o executor e executado naquela máquina.
Embora esta configuração possa parecer robusta, enfrentamos problemas com a virtualização devido ao fraco suporte dos processadores Intel. Como resultado, decidimos não usar virtualização como Docker e executar trabalhos nas próprias máquinas físicas. Tentamos configurar uma solução eficiente e confiável baseada no Docker, mas as limitações de virtualização, como a falta de aceleração da GPU, fizeram com que os trabalhos demorassem duas vezes mais para serem executados do que nas máquinas físicas. Isso levou a mais sobrecarga e ao rápido preenchimento das filas.
Devido ao SLA do macOS, só pudemos configurar duas VMs simultaneamente. Portanto, decidimos estender o conjunto de executores físicos e configurá-los para executar tarefas do Gitlab diretamente em seu sistema operacional. No entanto, esta abordagem também teve algumas desvantagens.
Desafios no processo de construção e gerenciamento de executores
- Nenhum isolamento das compilações fora da sandbox do diretório de compilação.
O executor executa cada compilação em uma máquina física, o que significa que as compilações não são isoladas da sandbox do diretório de compilação. Isto tem suas vantagens e desvantagens. Por um lado, podemos usar caches do sistema para acelerar compilações, já que a maioria dos projetos usa o mesmo conjunto de dependências de terceiros.
Por outro lado, o cache torna-se insustentável, pois as sobras de um projeto podem afetar todos os outros projetos. Isso é particularmente importante para caches de todo o sistema, já que os mesmos executores são usados para desenvolvimento em Flutter e React Native. O React Native, em particular, requer muitas dependências armazenadas em cache por meio do NPM.
- Potencial bagunça na ferramenta do sistema.
Embora nenhum dos trabalhos tenha sido executado com privilégios sudo, ainda era possível para eles acessar algumas ferramentas do sistema ou do usuário, como Ruby. Isso representava uma ameaça potencial de quebra de algumas dessas ferramentas, especialmente porque o macOS usa Ruby para alguns de seus softwares legados, incluindo alguns recursos legados do Xcode. A versão do sistema do Ruby não é algo que você gostaria de mexer.
No entanto, a introdução do rbenv cria outra camada de complexidade a ser enfrentada. É importante observar que Rubygems são instalados por versão Ruby, e algumas dessas gems requerem versões específicas de Ruby. Quase todas as ferramentas de terceiros que usávamos dependiam de Ruby, com Cocoapods e Fastlane sendo os atores principais.
- Gerenciando identidades de assinatura.
Gerenciar múltiplas identidades de assinatura de várias contas de desenvolvimento de clientes pode ser um desafio quando se trata das chaves do sistema nos executores. A identidade de assinatura é um dado altamente sensível , pois nos permite codificar o aplicativo, tornando-o vulnerável a ameaças potenciais.
Para garantir a segurança, as identidades devem ser colocadas em área restrita entre projetos e protegidas. No entanto, esse processo pode se tornar um pesadelo, considerando a complexidade adicional introduzida pelo macOS na implementação das chaves.
- Desafios em ambientes multiprojetos.
Nem todos os projetos foram criados usando as mesmas ferramentas, principalmente o Xcode. Alguns projetos, principalmente aqueles em fase de suporte, foram mantidos utilizando a última versão do Xcode com a qual o projeto foi desenvolvido. Isto significa que se fosse necessário algum trabalho nesses projetos, a CI teria que ser capaz de construí-lo. Como resultado, os executores tinham que oferecer suporte a diversas versões do Xcode ao mesmo tempo, o que efetivamente restringia o número de executores disponíveis para um trabalho específico.
5. Esforço extra necessário.
Quaisquer alterações feitas nos executores, como instalação de software, devem ser executadas em todos os executores simultaneamente. Embora tivéssemos uma ferramenta de automação para isso, era necessário um esforço extra para manter os scripts de automação.
Soluções de infraestrutura personalizadas para diversas necessidades dos clientes
Miquido é uma software house que trabalha com múltiplos clientes com diferentes necessidades. Personalizamos nossos serviços para atender às necessidades específicas de cada cliente. Muitas vezes hospedamos a base de código e a infraestrutura necessária para pequenas empresas ou start-ups, uma vez que podem não ter os recursos ou o conhecimento para mantê-la.
Os clientes corporativos geralmente possuem infraestrutura própria para hospedar seus projetos. No entanto, alguns não têm capacidade para o fazer ou são obrigados pelos regulamentos da indústria a utilizar a sua infra-estrutura. Eles também preferem não usar nenhum serviço SaaS de terceiros, como Xcode Cloud ou Codemagic. Em vez disso, eles querem uma solução que se adapte à arquitetura existente.
Para acomodar esses clientes, frequentemente hospedamos os projetos em nossa infraestrutura ou definimos a mesma configuração iOS de integração contínua em sua infraestrutura. No entanto, tomamos cuidado extra ao lidar com informações e arquivos confidenciais, como assinaturas de identidades.
Aproveitando o Fastlane para um gerenciamento de construção eficiente
Aqui o Fastlane surge como uma ferramenta útil. É composto por vários módulos denominados ações que ajudam a agilizar o processo e separá-lo entre diferentes clientes. Uma dessas ações, chamada match, ajuda a manter identidades de assinatura de desenvolvimento e produção, bem como perfis de provisionamento. Ele também funciona no nível do sistema operacional para separar essas identidades em chaves separadas durante o tempo de compilação e executa uma limpeza após a compilação, o que é extremamente útil porque executamos todas as nossas compilações em máquinas físicas.
Inicialmente recorremos ao Fastlane por um motivo específico, mas descobrimos que ele tinha recursos adicionais que poderiam ser úteis para nós.
- Criar upload para Testflight
No passado, a API AppStoreConnect não estava disponível publicamente para desenvolvedores. Isso significava que a única maneira de fazer upload de uma compilação para o Testflight era por meio do Xcode ou do Fastlane. Fastlane foi uma ferramenta que essencialmente eliminou a API ASC e a transformou em uma ação chamada pilot . No entanto, esse método frequentemente falhava com a próxima atualização do Xcode. Se um desenvolvedor quisesse fazer upload de sua compilação para Testflight usando a linha de comando, Fastlane era a melhor opção disponível.
- Troca fácil entre versões do Xcode
Tendo mais de uma instância do Xcode em uma única máquina, foi necessário selecionar qual Xcode usar para a construção. Infelizmente, a Apple tornou inconveniente alternar entre as versões do Xcode – você precisa usar 'xcode-select' para fazer isso, o que requer privilégios adicionais de sudo. Fastlane também cobre isso.
- Utilitários adicionais para desenvolvedores
Fastlane fornece muitos outros utilitários úteis, incluindo controle de versão e a capacidade de enviar resultados de construção para webhooks.
As desvantagens do Fastlane
Adaptar o Fastlane aos nossos projetos foi sólido e sólido, então seguimos nessa direção. Nós o usamos com sucesso por vários anos. No entanto, ao longo destes anos, identificamos alguns problemas:
- Fastlane requer conhecimento de Ruby.
Fastlane é uma ferramenta escrita em Ruby e requer um bom conhecimento de Ruby para usá-la de forma eficaz. Quando há bugs na configuração do Fastlane ou na própria ferramenta, depurá-los usando irb ou pry pode ser bastante desafiador.
- Dependência de inúmeras joias.
O próprio Fastlane depende de aproximadamente 70 joias. Para mitigar os riscos de quebrar o Ruby do sistema, os projetos usavam gems de empacotadores locais. Buscar todas essas joias gerou muita sobrecarga de tempo.
- Problemas do sistema Ruby e rubygems.
Como resultado, todos os problemas com Ruby e rubygems do sistema mencionados anteriormente também são aplicáveis aqui.
- Redundância para projetos Flutter.
Os projetos Flutter também foram forçados a usar fastlane match apenas para preservar a compatibilidade com projetos iOS e proteger as chaves dos corredores. Isso era absurdamente desnecessário, já que o Flutter tem seu próprio sistema de compilação integrado e a sobrecarga mencionada anteriormente foi introduzida apenas para gerenciar identidades de assinatura e perfis de provisionamento.
A maioria desses problemas foi corrigida ao longo do caminho, mas precisávamos de uma solução mais robusta e confiável.
A ideia: adaptar ferramentas de integração contínua novas e mais robustas para iOS
A boa notícia é que a Apple obteve controle total sobre a arquitetura de seu chip e desenvolveu uma nova estrutura de virtualização para macOS. Essa estrutura permite que os usuários criem, configurem e executem máquinas virtuais Linux ou macOS que iniciam rapidamente e se caracterizam por um desempenho semelhante ao nativo - e eu realmente quero dizer semelhante ao nativo.
Isso parecia promissor e poderia ser a base para nossas novas ferramentas de integração contínua para iOS. No entanto, foi apenas uma fatia de uma solução completa. Tendo uma ferramenta de gerenciamento de VM, também precisávamos de algo que pudesse usar essa estrutura em coordenação com nossos executores do Gitlab.
Tendo isso, a maioria dos nossos problemas relacionados ao baixo desempenho da virtualização se tornariam obsoletos. Também nos permitiria resolver automaticamente a maioria dos problemas que pretendíamos resolver com Fastlane.
Desenvolvendo uma solução personalizada para gerenciamento de identidade de assinatura sob demanda
Tínhamos um último problema para resolver: gerenciamento de identidade de assinatura. Não queríamos usar o Fastlane para isso, pois parecia excessivo para as nossas necessidades. Em vez disso, procurávamos uma solução mais adaptada às nossas necessidades. Nossas necessidades eram simples: o processo de gerenciamento de identidades tinha que ser feito sob demanda, exclusivamente durante o tempo de construção, sem nenhuma identidade pré-instalada no chaveiro, e ser compatível com qualquer máquina em que fosse executado.
O problema de distribuição e a falta de uma API AppstoreConnect estável tornaram-se obsoletos quando a Apple lançou seu `altool,` que permitia a comunicação entre os usuários e o ASC.
Então tivemos uma ideia e tivemos que encontrar uma maneira de conectar esses três aspectos:
- Encontrando uma maneira de utilizar a estrutura de virtualização da Apple.
- Fazendo funcionar com executores do Gitlab.
- Encontrar uma solução para assinar o gerenciamento de identidade em vários projetos e executores.
A solução: um vislumbre da nossa abordagem (ferramentas incluídas)
Começamos a procurar soluções para resolver todos os problemas mencionados anteriormente.
- Utilizando a estrutura de virtualização da Apple.
Para o primeiro obstáculo, encontramos uma solução bastante rápida: nos deparamos com a ferramenta torta do Cirrus Labs. Desde o primeiro momento sabíamos que esta seria a nossa escolha.
As vantagens mais significativas de usar a ferramenta tart oferecida pelo Cirrus Lab são:
- A possibilidade de criar vms a partir de imagens .ipsw brutas.
- A possibilidade de criar vms usando modelos pré-empacotados (com algumas ferramentas utilitárias instaladas, como brew ou Xcode), disponíveis na página GitHub do Cirrus Labs.
- A ferramenta Tart utiliza packer para suporte de construção dinâmica de imagens.
- A ferramenta Tart oferece suporte a imagens Linux e MacOS.
- A ferramenta utiliza um recurso excelente do sistema de arquivos APFS que permite a duplicação de arquivos sem realmente reservar espaço em disco para eles. Dessa forma, você não precisa alocar espaço em disco três vezes o tamanho da imagem original. Você só precisa de espaço em disco suficiente para a imagem original, enquanto o clone ocupa apenas o espaço que é diferente entre ele e a imagem original. Isso é extremamente útil, especialmente porque as imagens do macOS tendem a ser muito grandes.
Por exemplo, uma imagem operacional do macOS Ventura com Xcode e outros utilitários instalados requer um mínimo de 60 GB de espaço em disco. Em circunstâncias normais, uma imagem e dois de seus clones ocupariam até 180 GB de espaço em disco, o que é uma quantidade significativa. E isso é apenas o começo, pois você pode querer ter mais de uma imagem original ou instalar diversas versões do Xcode em uma única VM, o que aumentaria ainda mais o tamanho.
- A ferramenta permite o gerenciamento de endereços IP para VMs originais e clonadas, permitindo acesso SSH às VMs.
- A capacidade de montar diretórios cruzados entre a máquina host e as VMs.
- A ferramenta é fácil de usar e possui uma CLI muito simples.
Não há quase nada que falte nesta ferramenta em termos de utilização para gerenciamento de VM. Quase nada, exceto por uma coisa: embora promissor, o plugin packer para construir imagens dinamicamente consumia muito tempo, então decidimos não usá-lo.
Tentamos torta e funcionou fantasticamente. Seu desempenho era semelhante ao nativo e o gerenciamento era fácil.
Tendo integrado o tart com sucesso e com resultados impressionantes, em seguida nos concentramos em enfrentar outros desafios.
- Encontrando uma maneira de combinar torta com corredores Gitlab.
Depois de resolver o primeiro problema, enfrentamos a questão de como combinar o tart com os corredores do Gitlab.
Vamos começar descrevendo o que os executores do Gitlab realmente fazem:
Precisávamos incluir um quebra-cabeça extra no diagrama, que envolvia a alocação de tarefas do host do executor para a VM. O trabalho do GitLab é um script de shell que contém variáveis cruciais, entradas PATH e comandos.
Nosso objetivo era transferir esse script para a VM e executá-lo.
No entanto, esta tarefa revelou-se mais desafiante do que pensávamos inicialmente.
O corredor
Os executores padrão do Gitlab, como Docker ou SSH, são simples de configurar e exigem pouca ou nenhuma configuração. Porém, precisávamos de maior controle sobre a configuração, o que nos levou a explorar executores customizados fornecidos pelo GitLab.
Executores personalizados são uma ótima opção para configurações não padrão, pois cada etapa do executor (preparar, executar, limpar) é descrita na forma de um script de shell. A única coisa que faltava era uma ferramenta de linha de comando que pudesse realizar as tarefas que precisávamos e ser executada em scripts de configuração do executor.
Atualmente, existem algumas ferramentas disponíveis que fazem exatamente isso – por exemplo, o executor tart CirrusLabs Gitlab. Essa ferramenta é exatamente o que procurávamos na época. Porém, ainda não existia e, após realizarmos pesquisas, não encontramos nenhuma ferramenta que pudesse nos ajudar a realizar nossa tarefa.
Escrevendo a própria solução
Como não conseguimos encontrar uma solução perfeita, nós mesmos escrevemos uma. Afinal, somos engenheiros! A ideia parecia sólida e tínhamos todas as ferramentas necessárias, por isso prosseguimos com o desenvolvimento.
Optamos por usar Swift e algumas bibliotecas de código aberto fornecidas pela Apple: Swift Argument Parser para lidar com a execução de linha de comando e Swift NIO para lidar com conexões SSH com VMs. Iniciamos o desenvolvimento e, em alguns dias, obtivemos o primeiro protótipo funcional de uma ferramenta que eventualmente evoluiu para o MQVMRunner.
Em alto nível, a ferramenta funciona da seguinte forma:
- (Etapa de preparação)
- Leia as variáveis fornecidas em gitlab-ci.yml (nome da imagem e variáveis adicionais).
- Escolha a base de VM solicitada
- Clone a base de VM solicitada.
- Configure um diretório de montagem cruzada e copie o script de trabalho do Gitlab, definindo as permissões necessárias para ele.
- Execute o clone e verifique a conexão SSH.
- Configure quaisquer dependências necessárias (como a versão Xcode), se necessário.
- (Executar etapa)
- Execute o trabalho do Gitlab executando um script de um diretório montado em um clone de VM preparado por meio de SSH.
- (Etapa de limpeza)
- Exclua a imagem clonada.
Desafios no desenvolvimento
Durante o desenvolvimento, encontramos vários problemas, que fizeram com que não corresse tão bem como gostaríamos.
- Gerenciamento de endereço IP.
Gerenciar endereços IP é uma tarefa crucial que deve ser realizada com cuidado. No protótipo, o tratamento SSH foi implementado usando comandos shell SSH diretos e codificados. Entretanto, no caso de shells não interativos, a autenticação por chave é recomendada. Além disso, é aconselhável adicionar o host ao arquivoknown_hosts para evitar interrupções. Porém, devido ao gerenciamento dinâmico dos endereços IP das máquinas virtuais, existe a possibilidade de duplicar a entrada de um determinado IP, gerando erros. Portanto, precisamos atribuir dinamicamente os conhecidos_hosts para um trabalho específico para evitar tais problemas.
- Solução Swift pura.
Considerando isso, e o fato de que os comandos shell codificados no código Swift não são realmente elegantes, pensamos que seria bom usar uma biblioteca Swift dedicada e decidimos usar o Swift NIO. Resolvemos alguns problemas, mas, ao mesmo tempo, introduzimos alguns novos, como - por exemplo - às vezes, os logs colocados no stdout eram transferidos *após* o canal SSH ser encerrado devido ao término da execução do comando - e, como estávamos nos baseando em essa saída no trabalho posterior, a execução falhou aleatoriamente.
- Seleção de versão do Xcode.
Como o plugin Packer não era uma opção para construção dinâmica de imagens devido ao consumo de tempo, decidimos usar uma única base de VM com várias versões do Xcode pré-instaladas. Tivemos que encontrar uma maneira para os desenvolvedores especificarem a versão do Xcode necessária em seu gitlab-ci.yml – e criamos variáveis personalizadas disponíveis para uso em qualquer projeto. O MQVMRunner executará `xcode-select` em uma VM clonada para configurar a versão correspondente do Xcode.
E muitos, muitos mais
Simplificando a migração de projetos e integração contínua para fluxo de trabalho iOS com Mac Studios
Havíamos configurado isso em dois novos Mac Studios e começamos a migrar os projetos. Queríamos tornar o processo de migração para nossos desenvolvedores o mais transparente possível. Não conseguimos torná-lo totalmente perfeito, mas eventualmente chegamos ao ponto em que eles tiveram que fazer apenas algumas coisas no gitlab-ci.yml:
- As tags dos corredores: usar Mac Studios em vez de Intels.
- O nome da imagem: parâmetro opcional, introduzido para compatibilidade futura caso precisemos de mais de uma VM base. No momento, o padrão é sempre a VM de base única que temos.
- A versão do Xcode: parâmetro opcional; caso não seja fornecido, será utilizada a versão mais recente disponível.
A ferramenta obteve um feedback inicial muito bom, por isso decidimos torná-la de código aberto. Adicionamos um script de instalação para configurar o Gitlab Custom Runner e todas as ações e variáveis necessárias. Usando nossa ferramenta, você pode configurar seu próprio executor GitLab em questão de minutos – a única coisa que você precisa é uma VM inicial e base na qual os trabalhos serão executados.
A estrutura final de integração contínua para iOS é a seguinte:
3. Solução para gerenciamento eficiente de identidades
Temos lutado para encontrar uma solução eficiente para gerenciar as identidades de assinatura de nossos clientes. Isto foi particularmente desafiador, uma vez que a assinatura de identidade é um dado altamente confidencial que não deve ser armazenado em local não seguro por mais tempo do que o necessário.
Além disso, queríamos carregar essas identidades apenas durante o tempo de construção, sem soluções entre projetos. Isso significava que a identidade não deveria ser acessível fora da sandbox do aplicativo (ou build). Já abordamos o último problema fazendo a transição para VMs. No entanto, ainda precisávamos encontrar uma maneira de armazenar e carregar a identidade de assinatura na VM apenas durante o tempo de construção.
Problemas com partida Fastlane
Na época, ainda estávamos usando a correspondência Fastlane, que armazena identidades criptografadas e provisões em um repositório separado, carrega-as durante o processo de construção em uma instância de chaveiro separada e remove essa instância após a construção.
Esta abordagem parece conveniente, mas tem alguns problemas:
- Requer toda a configuração do Fastlane para funcionar.
Fastlane é rubygem, e todos os problemas listados no primeiro capítulo se aplicam aqui.
- Check-out do repositório no momento da construção.
Mantivemos nossas identidades em um repositório separado que foi verificado durante o processo de construção, e não durante o processo de configuração. Isso significava que tínhamos que estabelecer acesso separado ao repositório de identidades, não apenas para o Gitlab, mas para executores específicos, semelhante à forma como lidaríamos com dependências privadas de terceiros.
- Difícil de gerenciar fora do Match.
Se você estiver utilizando o Match para gerenciar identidades ou provisionamento, haverá pouca ou nenhuma necessidade de intervenção manual. Editar, descriptografar e criptografar perfis manualmente para que as correspondências ainda possam funcionar com eles mais tarde é tedioso e demorado. Usar o Fastlane para realizar esse processo geralmente resulta na eliminação completa da configuração de provisionamento do aplicativo e na criação de uma nova.
- Um pouco difícil de depurar.
No caso de qualquer problema de assinatura de código, poderá ser difícil determinar a identidade e a correspondência de provisionamento que acabou de ser instalada, pois seria necessário decodificá-los primeiro.
- Preocupações com segurança.
Combine contas de desenvolvedor acessadas usando as credenciais fornecidas para fazer alterações em seu nome. Apesar do Fastlane ser de código aberto, alguns clientes o recusaram devido a questões de segurança.
- Por último, mas não menos importante, livrar-se do Match removeria o maior obstáculo no nosso caminho para nos livrarmos completamente do Fastlane.
Nossos requisitos iniciais foram os seguintes:
- O carregamento requer assinar a identidade em um local seguro, de preferência em formato que não seja de texto simples, e colocá-la no chaveiro.
- Essa identidade deve ser acessível pelo Xcode.
- De preferência, as variáveis de senha de identidade, nome do chaveiro e senha do chaveiro devem ser configuráveis para fins de depuração.
O Match tinha tudo o que precisávamos, mas implementar o Fastlane apenas para usar o Match parecia um exagero, especialmente para soluções multiplataforma com seu próprio sistema de construção. Queríamos algo semelhante ao Match, mas sem o pesado fardo do Ruby, ele estava carregando.
Criando a própria solução
Então pensamos – vamos escrever isso nós mesmos! Fizemos isso com o MQVMRunner, então também poderíamos fazer aqui. Também escolhemos o Swift para fazer isso, principalmente porque poderíamos obter muitas APIs necessárias gratuitamente usando a estrutura de segurança da Apple.
Claro, também não correu tão bem quanto o esperado.
- Estrutura de segurança implementada.
A estratégia mais fácil era chamar os comandos bash como faz o Fastlane. Porém, tendo o framework de Segurança disponível, pensamos que seria mais elegante utilizá-lo para desenvolvimento.
- Falta de experiência.
Não tínhamos muita experiência com a estrutura de segurança do macOS e descobrimos que ela era significativamente diferente daquela com a qual estávamos acostumados no iOS. O tiro saiu pela culatra em muitos casos em que não estávamos cientes das limitações do macOS ou assumimos que ele funcionava da mesma forma que no iOS – a maioria dessas suposições estava errada.
- Documentação terrível.
A documentação da estrutura de segurança da Apple é, para dizer o mínimo, humilde. É uma API muito antiga que remonta às primeiras versões do OSX e, às vezes, tínhamos a impressão de que não foi atualizada desde então. Uma grande parte do código não está documentada, mas antecipamos como ele funciona lendo o código-fonte. Felizmente para nós, é de código aberto.
- Depreciações sem substituições.
Uma boa parte desta estrutura está obsoleta; A Apple está tentando se afastar do típico chaveiro “estilo macOS” (vários chaveiros acessíveis por senha) e implementar o chaveiro “estilo iOS” (chaveiro único, sincronizado via iCloud). Então, eles a descontinuaram no macOS Yosemite em 2014, mas não encontraram nenhum substituto para ela nos últimos nove anos – então, a única API disponível para nós, por enquanto, está obsoleta porque ainda não há nenhuma nova.
Presumimos que as identidades de assinatura podem ser armazenadas como strings codificadas em base64 em variáveis Gitlab por projeto. É seguro, baseado em projeto e, se definido como uma variável mascarada, pode ser lido e exibido nos logs de construção como um texto não simples.
Então, tínhamos os dados de identidade. Só precisávamos colocá-lo no chaveiro. Usando a API de segurança Depois de algumas tentativas e muita dificuldade na documentação da estrutura de segurança, preparamos um protótipo de algo que mais tarde se tornou MQSwiftSign.
Aprendendo o sistema de segurança do macOS, mas da maneira mais difícil
Tivemos que compreender profundamente como as chaves do macOS funcionam para desenvolver nossa ferramenta. Isso envolveu pesquisar como o chaveiro gerencia itens, seus acessos e permissões e a estrutura dos dados do chaveiro. Por exemplo, descobrimos que o chaveiro é o único arquivo macOS cujo sistema operacional ignora o conjunto ACL. Além disso, aprendemos que a ACL em itens específicos das chaves é uma lista de texto simples salva em um arquivo das chaves. Enfrentamos vários desafios ao longo do caminho, mas também aprendemos muito.
Um desafio significativo que encontramos foram os prompts. Nossa ferramenta foi projetada principalmente para ser executada em sistemas CI iOS, o que significava que não deveria ser interativa. Não podíamos pedir aos usuários que confirmassem uma senha no CI.
No entanto, o sistema de segurança do macOS é bem projetado, impossibilitando a edição ou leitura de informações confidenciais, incluindo a identidade de assinatura, sem a permissão explícita do usuário. Para acessar um recurso sem confirmação, o programa de acesso deve estar incluído na Lista de Controle de Acesso do recurso. Este é um requisito estrito que nenhum programa pode quebrar, mesmo os programas da Apple que acompanham o sistema. Se algum programa precisar ler ou editar uma entrada de chaveiro, o usuário deverá fornecer uma senha de chaveiro para desbloqueá-lo e, opcionalmente, adicioná-la à ACL da entrada.
Superando os desafios de permissão do usuário
Portanto, tivemos que encontrar uma maneira de o Xcode acessar uma identidade configurada por nosso chaveiro sem pedir permissão ao usuário usando o prompt de senha. Para isso, podemos alterar a lista de controle de acesso de um item, mas isso também requer permissão do usuário – e, claro, exige. Caso contrário, isso prejudicaria todo o objetivo de ter o ACL. Temos tentado contornar essa salvaguarda – tentamos obter o mesmo efeito que com o comando `security set-key-partition-list`.
Depois de nos aprofundarmos na documentação da estrutura, não encontramos nenhuma API que permita editar a ACL sem solicitar que o usuário forneça uma senha. A coisa mais próxima que encontramos é `SecKeychainItemSetAccess`, que sempre aciona um prompt da IU. Depois demos outro mergulho, mas desta vez, na melhor documentação, que é o próprio código-fonte. Como a Apple implementou isso?
Descobriu-se que – como era de se esperar – eles estavam usando uma API privada. Um método chamado `SecKeychainItemSetAccessWithPassword` faz basicamente a mesma coisa que `SecKeychainItemSetAccess`, mas em vez de solicitar uma senha ao usuário, a senha é fornecida como um argumento para uma função. Claro – como uma API privada, ela não está listada na documentação, mas a Apple não possui a documentação para tais APIs, como se não pudesse pensar em criar um aplicativo para uso pessoal ou empresarial. Como a ferramenta era destinada apenas para uso interno, não hesitamos em usar a API privada. A única coisa que precisava ser feita era conectar o método C ao Swift.
Assim, o fluxo de trabalho final do protótipo foi o seguinte:
- Crie o chaveiro desbloqueado temporariamente com o bloqueio automático desativado.
- Obtenha e decodifique os dados de identidade de assinatura codificados em base64 de variáveis ambientais (passados pelo Gitlab).
- Importe a identidade para o chaveiro criado.
- Defina opções de acesso adequadas para identidade importada para que o Xcode e outras ferramentas possam lê-la para codesign.
Outras atualizações
O protótipo estava funcionando bem, por isso identificamos alguns recursos adicionais que gostaríamos de adicionar à ferramenta. Nosso objetivo era eventualmente substituir a fastlane; já implementamos a ação `match`. No entanto, o fastlane ainda oferecia dois recursos valiosos que ainda não tínhamos – provisionamento de instalação de perfil e criação de export.plist.
Instalação do perfil de provisionamento
A instalação do perfil de provisionamento é bastante direta - ela se divide em extrair o UUID do perfil e copiar o arquivo para `~/Library/MobileDevice/Provisioning Profiles/` com UUID como nome de arquivo - e isso é suficiente para o Xcode vê-lo corretamente. Não é ciência de foguetes adicionar à nossa ferramenta um plugin simples para percorrer o diretório fornecido e fazer isso para cada arquivo .mobileprovision que encontrar dentro dele.
Criação de Export.plist
A criação do export.plist, entretanto, é um pouco mais complicada. Para gerar um arquivo IPA adequado, o Xcode exige que os usuários forneçam um arquivo plist com informações específicas coletadas de várias fontes – o arquivo do projeto, o plist de direitos, as configurações do espaço de trabalho, etc. através da CLI é desconhecido para mim. Porém, deveríamos coletá-los usando APIs Swift, tendo apenas referências de projeto/espaço de trabalho e uma pequena dose de conhecimento sobre como o arquivo de projeto Xcode é construído.
O resultado foi melhor que o esperado, então decidimos adicioná-lo como mais um plugin à nossa ferramenta. Também o lançamos como um projeto de código aberto para um público mais amplo. No momento, MQSwiftSign é uma ferramenta multifuncional que pode ser usada com sucesso como um substituto para ações básicas de fastlane necessárias para construir e distribuir seu aplicativo iOS e nós a usamos em todos os nossos projetos em Miquido.
Considerações Finais: O Sucesso
Mudar da arquitetura Intel para iOS ARM foi uma tarefa desafiadora. Enfrentamos inúmeros obstáculos e gastamos um tempo significativo desenvolvendo ferramentas devido à falta de documentação. No entanto, finalmente estabelecemos um sistema robusto:
- Dois corredores para administrar em vez de nove;
- Executando software que está inteiramente sob nosso controle, sem muita sobrecarga na forma de rubygems – conseguimos nos livrar do fastlane ou de qualquer software de terceiros em nossas configurações de construção;
- MUITO conhecimento e compreensão de coisas às quais normalmente não prestamos atenção - como a segurança do sistema macOS e a própria estrutura de segurança, uma estrutura real de projeto Xcode e muito, muito mais.
Eu ficaria feliz em encorajá-lo – Se você está tendo dificuldades para configurar seu executor GitLab para compilações iOS, experimente nosso MQVMRunner. Se você precisar de ajuda para construir e distribuir seu aplicativo usando uma única ferramenta e não quiser depender de rubygems, experimente o MQSwiftSign. Funciona para mim, também pode funcionar para você!