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 oappcast.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.
- Clique em New repository
- 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. - Proprietário (Owner): sua conta ou uma organização (exemplo:
example-dev) - Visibilidade: Public — obrigatório, pois o Sparkle deve buscá-lo sem autenticação.
- Marque Add a README file (para um primeiro commit conveniente)
- Clique em Create repository
Passo 2 — Ativar o GitHub Pages
Sirva o repositório que você acabou de criar como um site estático.
- Vá em Settings do repositório → Pages no menu à esquerda
- Source: Deploy from a branch
- Branch: selecione
main/(root)→ Save - 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.
| Campo | Valor |
|---|---|
| Type | CNAME |
| Name | updates |
| Value | example-dev.github.io |
| TTL | 3600 (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>
method—developer-id. Significa “distribuição direta, não a Mac App Store.”signingStyle—automatic. 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-lookupacima não é necessária. No entanto, como as atualizações automáticas usam a rede, o entitlementnetwork.cliente 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 acimaENABLE_HARDENED_RUNTIME = YES— uma condição necessária para notarizaçãoENABLE_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
.dmgenvolve 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.