A última peça — onde colocar as atualizações

Na Parte 1 preparamos o certificado Developer ID e a notarização, e na Parte 2 preparamos a chave de assinatura do Sparkle. Isso significa que agora temos uma forma de assinar o app, notarizá-lo e verificar a autenticidade das atualizações.

Mas o local apontado pelo SUFeedURL (https://updates.example.com/appcast.xml), que escrevemos no Info.plist do app na Parte 2, ainda não tem nada. Nesta parte final, vamos hospedar o feed de atualizações que vai naquele local e finalizar as configurações de build, completando toda a configuração inicial.

Como nas Partes 1 e 2, todos os nomes e domínios (FocusTimer, example.com, example-dev, etc.) são valores de exemplo. Na prática, substitua-os pelas suas próprias informações.

Por que manter um repositório de atualizações separado

Para que as atualizações automáticas funcionem, dois itens precisam estar disponíveis na internet.

  • appcast.xml — o feed de atualizações que informa ao app qual versão é a mais recente e onde obtê-la
  • .dmg — o arquivo instalador real do app

Há uma restrição importante aqui. O código do Sparkle dentro do app busca esses arquivos com uma requisição HTTPS GET simples e sem autenticação. Isso significa que o app no computador do usuário deve conseguir baixá-los diretamente, sem nenhum procedimento como um login.

Muitos desenvolvedores mantêm o repositório de código-fonte principal do app como privado. Mas os arquivos de lançamento em um repositório privado exigem autenticação, então o Sparkle não consegue buscá-los. É por isso que uma estrutura comum é separar os repositórios.

  • Repositório principal (ex.: FocusTimer) — o código-fonte. Pode ser mantido privado.
  • Repositório de atualizações (ex.: FocusTimer-updates) — hospeda apenas o appcast.xml. Deve ser público.

Neste artigo, vamos operar o repositório de atualizações no GitHub Pages, a hospedagem estática gratuita do GitHub.

Passo 1 — Criar o repositório público de atualizações

Crie um novo repositório no GitHub.

  1. Clique em New repository
  2. Nome: FocusTimer-updates — este nome é usado no endereço do feed em breve, então defina-o com precisão, incluindo as maiúsculas e minúsculas.
  3. Proprietário (Owner): sua conta ou uma organização (exemplo: example-dev)
  4. Visibilidade: Public — obrigatório, pois o Sparkle deve buscá-lo sem autenticação.
  5. Marque Add a README file (para um primeiro commit conveniente)
  6. Clique em Create repository

Passo 2 — Ativar o GitHub Pages

Sirva o repositório que você acabou de criar como um site estático.

  1. Vá em Settings do repositório → Pages no menu à esquerda
  2. Source: Deploy from a branch
  3. Branch: selecione main / (root)Save
  4. Após 1–2 minutos, se https://example-dev.github.io/FocusTimer-updates/ ficar acessível, funcionou.

Neste ponto você já tem um endereço público onde pode colocar os arquivos de atualização. Mas um passo a mais ainda é necessário.

Passo 3 — Conectar um domínio personalizado

Você pode usar o endereço padrão do GitHub Pages (example-dev.github.io/...) como está e funcionará. Mas se você incorporar esse endereço no SUFeedURL do app, terá problemas se precisar migrar a hospedagem do GitHub Pages para outro lugar — porque todos os apps dos usuários já distribuídos ainda estão apontando para o endereço antigo.

A solução é inserir uma camada de um domínio que você controla. Se você definir SUFeedURL para seu próprio domínio, como https://updates.example.com/appcast.xml, quando você migrar a hospedagem mais tarde, você só muda uma linha de configuração de DNS e os usuários existentes seguem automaticamente para o novo local. Você só precisa fazer essa configuração uma vez, e ela permanece válida para sempre.

3-1. Adicionar um registro DNS

Na tela de configuração do provedor de DNS que gerencia seu domínio (example.com), adicione o seguinte registro.

CampoValor
TypeCNAME
Nameupdates
Valueexample-dev.github.io
TTL3600 (padrão)

Isso faz com que o subdomínio updates.example.com aponte para o GitHub Pages.

3-2. Adicionar um arquivo CNAME ao repositório

Na raiz do repositório de atualizações (FocusTimer-updates), crie um arquivo chamado CNAME cujo conteúdo é apenas uma linha — o domínio.

updates.example.com

Faça commit e push deste arquivo.

3-3. Registrar o domínio no GitHub Pages

No campo Settings → Pages → Custom domain do repositório, insira updates.example.com e clique em Save. O GitHub emite automaticamente um certificado HTTPS e, depois que for emitido, marque Enforce HTTPS.

3-4. Verificação

A propagação de DNS e a emissão do certificado geralmente levam cerca de 10 minutos. Após uma breve espera, verifique com o seguinte comando.

curl -I https://updates.example.com/appcast.xml

Se a primeira linha da resposta mostrar HTTP/2 200, está tudo bem. (Se você ainda não tiver enviado o appcast.xml, poderá receber um 404, mas o domínio e a conexão HTTPS em si podem ser confirmados por outros caminhos. O ponto principal é que https://updates.example.com retorna uma resposta.)

Para referência, o arquivo instalador .dmg é geralmente enviado para o GitHub Releases em vez do GitHub Pages. Colocar binários grandes diretamente em um repositório o sobrecarrega. Os Releases são servidos por um caminho diferente do Pages, portanto, deixe-os como estão, independentemente da configuração de domínio personalizado acima.

Passo 4 — Manter o repositório de atualizações localmente

Para editar e fazer commit do appcast.xml durante o trabalho de lançamento, você precisa ter o repositório de atualizações clonado localmente também. Se você cloná-lo em um local adequado dentro da pasta do projeto principal (ex.: release/updates), os scripts de build e lançamento podem lidar com ambos os repositórios a partir de um único lugar, o que é conveniente.

git clone [email protected]:example-dev/FocusTimer-updates.git release/updates

Quando você mantém um repositório dentro de outro assim, adicione release/updates/ ao .gitignore do repositório principal para que não interfiram entre si.

Passo 5 — ExportOptions.plist

Agora para as configurações do lado do build. O comando xcodebuild -exportArchive do Xcode lê um arquivo de configuração chamado ExportOptions.plist ao exportar um archive para distribuição. O conteúdo para distribuição direta (o canal Developer ID) é o seguinte.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>method</key>
    <string>developer-id</string>
    <key>signingStyle</key>
    <string>automatic</string>
    <key>teamID</key>
    <string>ABCDE12345</string>
</dict>
</plist>
  • methoddeveloper-id. Significa “distribuição direta, não a Mac App Store.”
  • signingStyleautomatic. Permite que o Xcode escolha e use automaticamente o certificado emitido na Parte 1.
  • teamID — o Team ID que você anotou na Parte 1.

Crie este arquivo uma vez dentro do projeto (ex.: release/ExportOptions.plist) e reutilize-o para cada lançamento.

Passo 6 — Verificar as configurações do lado do app

Por fim, vamos revisar as configurações que devem estar presentes no próprio projeto do app. Se você estiver adicionando o Sparkle a um novo app pela primeira vez, pode usar esta lista como checklist.

Info.plist

Estas são as chaves do Sparkle adicionadas na Parte 2. O Info.plist deve incluir as seguintes chaves.

<key>SUFeedURL</key>
<string>https://updates.example.com/appcast.xml</string>
<key>SUPublicEDKey</key>
<string>5vT3kQbA9mZ0wR1yX8cD2eF4gH6jK7lN0pS2uV5xW8c=</string>

Verifique novamente se SUFeedURL aponta para o domínio personalizado criado no Passo 3, e se SUPublicEDKey corresponde à chave pública gerada na Parte 2.

Entitlements

Se o app usar o App Sandbox, o Sparkle precisa de entitlements de exceção para poder se comunicar com serviços internos e instalar atualizações. As seguintes entradas vão no arquivo FocusTimer.entitlements.

<key>com.apple.security.app-sandbox</key><true/>
<key>com.apple.security.network.client</key><true/>
<key>com.apple.security.temporary-exception.mach-lookup.global-name</key>
<array>
    <string>$(PRODUCT_BUNDLE_IDENTIFIER)-spks</string>
    <string>$(PRODUCT_BUNDLE_IDENTIFIER)-spki</string>
</array>

No momento do build, o Xcode substitui automaticamente $(PRODUCT_BUNDLE_IDENTIFIER) pelo identificador de bundle real (com.example.FocusTimer). Para detalhes sobre cada entrada, consulte o guia oficial de sandboxing do Sparkle.

O sandboxing não é obrigatório para um app distribuído diretamente (o sandbox é um requisito da Mac App Store). Se o seu app não usa o sandbox, a exceção mach-lookup acima não é necessária. No entanto, como as atualizações automáticas usam a rede, o entitlement network.client e o Hardened Runtime devem estar ativados para a notarização.

Configurações de Build

Nas configurações de build do Xcode, verifique o seguinte.

  • CODE_SIGN_ENTITLEMENTS — o caminho para o arquivo de entitlements acima
  • ENABLE_HARDENED_RUNTIME = YES — uma condição necessária para notarização
  • ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES — permite acesso à rede para verificar atualizações

Resumo da série — Configuração inicial concluída

A configuração inicial de três partes para distribuição direta está agora completa. Você agora tem em mãos o seguinte.

  • (Parte 1) Um certificado Developer ID Application + credenciais para notarização
  • (Parte 2) Um par de chaves de assinatura EdDSA do Sparkle + backup da chave privada
  • (Parte 3) Um repositório público de atualizações conectado a um domínio personalizado + ExportOptions.plist + configuração do lado do app

Isso é tudo que só precisa ser feito uma vez. Você não precisará refazer esse trabalho toda vez que lançar uma nova versão.

A partir de agora, o fluxo para distribuir uma nova versão se repete de maneira muito semelhante a cada vez — compilar o archive → exportar com ExportOptions.plist → assinar com o certificado Developer ID → notarizar com notarytool → criar o .dmg → assinar com a chave do Sparkle → atualizar o appcast.xml → enviar o .dmg para um GitHub Release. Esse processo repetitivo pode ser em grande parte automatizado com um único script, e isso é um tópico para outro artigo.

Entre essas etapas, criar o .dmg envolve elementos de design como imagens de fundo e posicionamento de ícones, então é abordado em detalhes na série separada Projetando um DMG de distribuição para seu app macOS.

A distribuição direta pode parecer assustadora no início porque há muito a configurar, mas o ponto principal é que “uma vez configurado, continua sendo reutilizado.” Em troca de abrir mão de parte da conveniência da App Store, você ganha controle sobre cada parte do processo de distribuição.

Referências