dovetail_bundler 0.1.0
dovetail_bundler: ^0.1.0 copied to clipboard
Build orchestrator for Flutter desktop apps with a Rust core: stages what a platform installer needs and drives the tool that produces it, on Windows, macOS and Linux.
dovetail_bundler #
Parte do
dovetail. Versionado só localmente.
Empacota um app Flutter desktop com núcleo Rust, chamando a ferramenta de cada plataforma. No lugar do bundler do Tauri.
O bundler do Tauri não empacota: ele orquestra ferramenta alheia. Chama makensis no Windows, WiX para MSI, hdiutil no macOS, linuxdeploy para AppImage — e .deb/.rpm ele nem chama nada, reimplementou em Rust porque é ar + tar + md5. Este package faz o mesmo, em Dart.
Por que Dart: é a linguagem da equipe daqui para frente, o cargokit que já usamos para compilar o Rust é exatamente isso, e — o que decide — os modos de falha do Tauri são de lógica, não de ferramenta. Ordem errada de !addplugindir, aviso onde devia ser erro, timestamp ausente. Isso se pega com teste; três scripts de shell divergentes não têm teste.
Estado #
| Alvo | Situação | O que se prova aqui |
|---|---|---|
| Windows, NSIS | construído aqui, com ressalva | ordem do script, escape, staging, invocação. Compila no macOS com unicode: false; o stub Unicode estoura em std::bad_alloc no arm64 |
| Windows, MSI | construído aqui | dois backends: wix no Windows, wixl em qualquer outro host. O .wxs gerado é validado por xmllint, o GUID tem golden test, e o MSI produzido é conferido com msiinfo e msiextract |
macOS, .dmg |
escrito | recusa um .app que não carrega as arquiteturas prometidas, antes do hdiutil |
macOS, .app.tar.gz |
escrito | o bundle assinado na raiz do tar, sem entradas ._* (COPYFILE_DISABLE); é o que o updater extrai e o manifesto publica — bundle --macos-format tar |
macOS, .app universal |
escrito | UniversalBundle funde dois builds de arquitetura única, provado com rustc e lipo de verdade |
Linux, .deb |
escrito | lido pelo ar e tar do sistema, com modo de arquivo e scripts de manutenção |
Linux, .rpm |
escrito | spec e scriptlets; rpmbuild local só conhece aarch64 |
| Ícones | escrito | o .icns desmontado pelo iconutil da Apple, o .ico lido pelo file |
Contagem de teste não fica escrita aqui, porque envelhece. dart test diz.
As armadilhas do Tauri que aqui são erro fatal #
O levantamento em my-docs/TAURI-COMO-ARTE-ANTERIOR.md achou ~30 avisos que eles pagaram. Três viraram teste:
!addplugindir antes de qualquer !include. Fora do topo absoluto, o makensis cai em silêncio para as DLLs não assinadas do toolset, apesar de a etapa de assinatura ter passado. Foi bug em produção lá. Um teste compara a linha do !addplugindir com a do primeiro !include.
Metadado de build não numérico é erro, não zero silencioso. O Tauri troca 1.2.3+abc por 0 no VIProductVersion com um aviso e segue. AppVersion.parse recusa, e a mensagem diz o que fazer.
DovetailStrContains existe duas vezes, e tem que existir. O NSIS recusa
Call numa seção de desinstalação a menos que a função se chame
un.algo — então o corpo é instanciado com e sem o prefixo, e
CheckIfAppIsRunning recebe qual usar. Sem isso o script gerado não
compilava em host nenhum, Windows incluído; o estouro do stub Unicode no
arm64 escondia o erro real atrás de um crash mais cedo.
O stub Unicode não compila no Apple silicon. O makensis 3.12 estoura em
std::bad_alloc ao escrever a saída, num script de quatro linhas, e compilar da
fonte não muda. Com unicode: false ele compila, ao custo real de um
instalador ANSI: caminhos e strings são lidos na code page do sistema, e uma
máquina cujo caminho de instalação está fora dela instala no lugar errado. A
alternativa sem ressalva é o MSI, que o wixl constrói em qualquer host.
CheckIfAppIsRunning imediatamente depois do NSIS_HOOK_PREINSTALL. Essa ordem foi corrigida por bug reportado duas vezes no Tauri. Um teste exige que só o !endif fique entre as duas linhas.
O hooks.nsh atual passa intacto #
Os quatro ganchos têm o mesmo nome e a mesma posição do template do Tauri — NSIS_HOOK_PREINSTALL, POSTINSTALL, PREUNINSTALL, POSTUNINSTALL, todos guardados por !ifmacrodef. As 280 linhas de app-tauri/installer/hooks.nsh, que instalam e registram o serviço privilegiado, entram sem alteração.
E não há dependência do plugin da Tauri: medido que aquele hook usa só nsExec e taskkill, ambos nativos. A macro CheckIfAppIsRunning daqui é implementada com nsExec + tasklist, sem nsis_tauri_utils.dll.
Uso #
dart run dovetail_bundler \
--product-name "Example" \
--manufacturer "Example Ltd" \
--identifier com.example.app \
--version 1.2.3+47 \
--main-binary example \
--app-dir build/windows/x64/runner/Release \
--out-dir dist \
--hooks installer/hooks.nsh
Escreve o caminho do instalador na saída padrão. Falha com código 1 e uma mensagem que traz o remédio.
O dmg recusa um .app que não diz de qual macOS precisa #
LSMinimumSystemVersion é a única coisa que impede um Mac antigo de abrir um
binário que ele não roda. Sem a chave o app abre e morre num símbolo ausente, o
que chega até nós como "ele só fecha" e chega até quem usa como nada.
Antes do hdiutil, não depois — dmg que existe é dmg que alguém sobe. Três
recusas: bundle sem a chave; bundle com a chave carregando
$(MACOSX_DEPLOYMENT_TARGET) por expandir, que o macOS lê como versão
nenhuma; e bundle que discorda do que a release declara, dizendo qual dos dois o
sistema obedece.
O Xcode preenche a chave a partir de MACOSX_DEPLOYMENT_TARGET, então projeto
correto já tem uma — isto existe para o dia em que alguém editar o template. O
app que este repositório constrói carrega 10.15, e um teste confere isso
contra o bundle de verdade.
O que não está provado aqui #
Que o makensis aceita o script gerado. A estrutura é testada, mas validade de NSIS só se prova compilando — e o makensis 3.12 do Homebrew em arm64 macOS aborta com std::bad_alloc até no script mínimo de quatro linhas. O teste sonda usabilidade, não presença: se o makensis local não compila um script trivial, ele é marcado como pulado com esse motivo, em vez de dar um verde falso.
Essa prova pertence a um runner Windows, que é onde o instalador importa.
Assinatura #
Não está aqui, de propósito. Assinatura é passo depois do build, nunca dentro — o acoplamento inverso é o que hoje amarra o instalador ao cargo tauri build. Sem segredo, artefato não assinado e um aviso; com --require-signature, que só a esteira passa, o mesmo aviso vira erro.