diff --git a/app/src-setup/src/installed.rs b/app/src-setup/src/installed.rs index 43a64037..4dfe00d8 100644 --- a/app/src-setup/src/installed.rs +++ b/app/src-setup/src/installed.rs @@ -39,9 +39,10 @@ mod registry { const UNINSTALL_KEY: &str = r"Software\Microsoft\Windows\CurrentVersion\Uninstall\Botloft"; /// Tauri's NSIS remembers the install folder under - /// `Software\\`; the publisher is "github", the second - /// part of the identifier (spec 15.4). - const FOLDER_KEY: &str = r"Software\github\Botloft"; + /// `Software\\`. The publisher is "Botloft"; installs + /// made while it was "github", Tauri's default from the identifier, are + /// adopted by the installer's hooks (spec 15.4), which look in this order. + const FOLDER_KEYS: [&str; 2] = [r"Software\Botloft\Botloft", r"Software\github\Botloft"]; pub fn entry(name: &str) -> Option { RegKey::predef(HKEY_CURRENT_USER) @@ -52,11 +53,14 @@ mod registry { } pub fn remembered_folder() -> Option { - RegKey::predef(HKEY_CURRENT_USER) - .open_subkey(FOLDER_KEY) - .ok()? - .get_value::("") - .ok() + let user = RegKey::predef(HKEY_CURRENT_USER); + FOLDER_KEYS.iter().find_map(|key| { + user.open_subkey(key) + .ok()? + .get_value::("") + .ok() + .filter(|folder| !folder.trim().is_empty()) + }) } } diff --git a/app/src-setup/tauri.conf.json b/app/src-setup/tauri.conf.json index 8c3713cb..91dbcb19 100644 --- a/app/src-setup/tauri.conf.json +++ b/app/src-setup/tauri.conf.json @@ -13,6 +13,7 @@ } }, "bundle": { + "publisher": "Botloft", "copyright": "Copyright 2026 Phelipe Lorran", "icon": ["../src-tauri/icons/icon.ico"] } diff --git a/app/src-tauri/tauri.conf.json b/app/src-tauri/tauri.conf.json index 0d8b1814..8dd09f71 100644 --- a/app/src-tauri/tauri.conf.json +++ b/app/src-tauri/tauri.conf.json @@ -37,6 +37,7 @@ }, "bundle": { "active": true, + "publisher": "Botloft", "copyright": "Copyright 2026 Phelipe Lorran", "targets": [ "nsis" diff --git a/app/src-tauri/windows/hooks.nsh b/app/src-tauri/windows/hooks.nsh index 8aa931cf..49f0295f 100644 --- a/app/src-tauri/windows/hooks.nsh +++ b/app/src-tauri/windows/hooks.nsh @@ -6,19 +6,64 @@ ; next to the title, as the Botloft mascot on the page's white. !define MUI_HEADERIMAGE_RIGHT +; The installer's own file properties name the publisher too; Tauri's +; template leaves the company out. +VIAddVersionKey "CompanyName" "Botloft" + +; Tauri keeps the install folder in the registry under +; Software\\. The publisher used to be "github", Tauri's +; default from the identifier; it is "Botloft" now (`bundle.publisher`). +; An install made under the old name is adopted under the new one, so an +; update goes to the folder Botloft is in, and "uninstall before installing" +; still finds that folder. +!ifndef BOTLOFT_FOLDER_KEY + !define BOTLOFT_FOLDER_KEY "Software\Botloft\Botloft" +!endif +!ifndef BOTLOFT_OLD_FOLDER_KEY + !define BOTLOFT_OLD_FOLDER_KEY "Software\github\Botloft" +!endif +!ifndef BOTLOFT_OLD_PUBLISHER_KEY + !define BOTLOFT_OLD_PUBLISHER_KEY "Software\github" +!endif + +; Copies the folder of an install made under the old publisher to the new +; key, and installs there unless /D chose another folder. It does nothing +; once the new key exists. +Function BotloftAdoptOldFolder + Push $0 + ReadRegStr $0 SHCTX "${BOTLOFT_FOLDER_KEY}" "" + StrCmp $0 "" 0 botloft_adopt_done + ReadRegStr $0 SHCTX "${BOTLOFT_OLD_FOLDER_KEY}" "" + StrCmp $0 "" botloft_adopt_done 0 + WriteRegStr SHCTX "${BOTLOFT_FOLDER_KEY}" "" $0 + ; Only when the installer is still on its default folder: /D wins. + StrCmp $INSTDIR "$LOCALAPPDATA\Botloft" 0 botloft_adopt_done + StrCpy $INSTDIR $0 + botloft_adopt_done: + Pop $0 +FunctionEnd + +; Before the first page, so the pages and "uninstall before installing" see +; the adopted folder. +!define MUI_CUSTOMFUNCTION_GUIINIT BotloftAdoptOldFolder + +!macro NSIS_HOOK_PREINSTALL + ; The quiet installer (/S) has no pages, so nothing adopted the folder yet. + ; The install section already set its output to the default folder. + Call BotloftAdoptOldFolder + SetOutPath $INSTDIR +!macroend + !macro NSIS_HOOK_POSTINSTALL - ; Installed apps shows Botloft as the publisher. `bundle.publisher` stays - ; unset on purpose: Tauri also names the key that keeps the install folder - ; after it (Software\\Botloft), so a new name would make the - ; first update of an existing install run the old uninstaller without its - ; folder. - WriteRegStr SHCTX "${UNINSTKEY}" "Publisher" "Botloft" + ; The folder is under the new name now; the old key goes. + DeleteRegKey SHCTX "${BOTLOFT_OLD_FOLDER_KEY}" + DeleteRegKey /ifempty SHCTX "${BOTLOFT_OLD_PUBLISHER_KEY}" !macroend !macro NSIS_HOOK_PREUNINSTALL ; A real uninstall stops the daemon and removes its scheduled task and - ; binary. Bots and data stay in %LOCALAPPDATA%\Botloft. An update runs - ; this uninstaller too, with /UPDATE: then the daemon keeps running. + ; binary. Bots and data stay in %LOCALAPPDATA%\Botloft. An uninstaller run + ; with /UPDATE leaves the daemon running. ${If} $UpdateMode <> 1 nsExec::Exec '"$INSTDIR\botloftd.exe" service uninstall' Pop $0 diff --git a/docs/spec.md b/docs/spec.md index 1be68ac7..55745e6b 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -706,8 +706,9 @@ Identidade: o mascote do Botloft é uma chama com olhos. O ícone do app (`app/a - `pnpm bundle` (em `app/`) roda `scripts/sidecar.mjs`, que compila o `botloftd` em release e o copia para `src-tauri/binaries/botloftd-.exe`, e depois `tauri build --config src-tauri/tauri.bundle.conf.json`. Esse arquivo liga o `externalBin` e o NSIS; ele fica fora do `tauri.conf.json` porque o `tauri-build` copia o `externalBin` também em dev e no `cargo clippy`, o que exigiria o sidecar em todo build e sobrescreveria o `target\debug\botloftd.exe` com a cópia de release. - Quem baixa o Botloft instala pela tela de instalação (15.7), que roda este instalador NSIS em silêncio. O NSIS com as próprias telas só aparece na atualização (15.5), na desinstalação e quando a tela de instalação não pode abrir. - NSIS por usuário (`installMode = currentUser`), sem pedir administrador, em `%LOCALAPPDATA%\Botloft` (seção 5). O instalador não mexe no daemon: ele roda da própria cópia em `\bin`, então o arquivo do sidecar nunca está em uso. -- Hook `NSIS_HOOK_PREUNINSTALL` (`src-tauri/windows/hooks.nsh`): numa desinstalação de verdade, `botloftd service uninstall` para o daemon e apaga a tarefa e o binário, e o valor `Botloft` da chave Run sai (15.2); bots e dados ficam. Uma atualização também roda o desinstalador antigo, com `/UPDATE`: aí o hook não faz nada e o daemon continua rodando até o app novo abrir e atualizá-lo (15.2). -- Marca do instalador, sem nome de terceiros à vista: o instalador e o desinstalador usam o ícone do app (`icons/icon.ico`); as telas de boas-vindas e de conclusão têm a imagem lateral `windows/installer-sidebar.bmp` (164×314, o mascote e "Botloft" sobre o preto) e as outras telas o cabeçalho `windows/installer-header.bmp` (150×57, à direita, com `MUI_HEADERIMAGE_RIGHT` no arquivo de hooks). As duas imagens saem do `app-icon.svg` sem o quadrado preto. O rodapé mostra o `bundle.copyright` no lugar de "Nullsoft Install System", e o `NSIS_HOOK_POSTINSTALL` grava "Botloft" como editor em Aplicativos instalados. O `bundle.publisher` fica vazio de propósito: o Tauri dá o nome dele à chave que guarda a pasta de instalação (`Software\\Botloft`, hoje `github`, a segunda parte do identificador), e trocá-lo faria a primeira atualização de uma instalação existente rodar o desinstalador antigo sem a pasta. Por isso a empresa nas propriedades do `Botloft.exe` continua `github`. +- Hook `NSIS_HOOK_PREUNINSTALL` (`src-tauri/windows/hooks.nsh`): numa desinstalação de verdade, `botloftd service uninstall` para o daemon e apaga a tarefa e o binário, e o valor `Botloft` da chave Run sai (15.2); bots e dados ficam. Numa atualização (`/UPDATE`), o instalador do Tauri 2.12 nem roda o desinstalador antigo: só troca os arquivos, e o daemon continua rodando até o app novo abrir e atualizá-lo (15.2). Um desinstalador chamado com `/UPDATE` também não mexe no daemon. +- Marca do instalador, sem nome de terceiros à vista: o instalador e o desinstalador usam o ícone do app (`icons/icon.ico`); as telas de boas-vindas e de conclusão têm a imagem lateral `windows/installer-sidebar.bmp` (164×314, o mascote e "Botloft" sobre o preto) e as outras telas o cabeçalho `windows/installer-header.bmp` (150×57, à direita, com `MUI_HEADERIMAGE_RIGHT` no arquivo de hooks). As duas imagens saem do `app-icon.svg` sem o quadrado preto. O rodapé mostra o `bundle.copyright` no lugar de "Nullsoft Install System", e o editor é "Botloft" (`bundle.publisher`), em Aplicativos instalados e como empresa nas propriedades do `Botloft.exe` e da tela de instalação. +- Editor antigo: até a 0.6.0 o `bundle.publisher` ficava vazio e o Tauri usava `github`, a segunda parte do identificador, o que num programa sem assinatura parece alguém se passando pelo GitHub. O Tauri também dá o nome do editor à chave que guarda a pasta de instalação (`Software\\Botloft`), lida para atualizar na mesma pasta e para o "desinstalar antes de instalar" do assistente. Por isso os hooks adotam a instalação antiga: `BotloftAdoptOldFolder` copia a pasta de `Software\github\Botloft` para `Software\Botloft\Botloft` quando a chave nova não existe e passa a instalar nela, a menos que `/D` tenha escolhido outra. Roda antes da primeira tela (`MUI_CUSTOMFUNCTION_GUIINIT`), o que cobre o assistente e a atualização passiva, e no começo da instalação (`NSIS_HOOK_PREINSTALL`), para o modo silencioso, que não tem telas. Depois de instalar, o `NSIS_HOOK_POSTINSTALL` apaga a chave antiga, e a `Software\github` só se ficar vazia. A tela de instalação (15.7) lê a chave nova e, na falta dela, a antiga. **Verificado** (Windows 11 25H2, 2026-09-30): sobre a 0.6.0 instalada com o editor antigo e com o app aberto, o instalador novo rodado como o atualizador (`/P /UPDATE /R`) terminou em 3 s, criou `Software\Botloft\Botloft` com a pasta que estava na chave antiga, apagou a antiga e a `Software\github` vazia, deixou o editor "Botloft" em Aplicativos instalados e reabriu o app da mesma pasta; o daemon não foi tocado. Os casos de pasta escolhida pelo dono, `/D` e modo silencioso foram testados com um instalador mínimo compilado com o mesmo arquivo de hooks e chaves de teste. - O manifesto do app (`src-tauri/windows/app.manifest`) é o padrão do Tauri (controles comuns v6) mais `longPathAware`. ### 15.5 Atualizações @@ -774,7 +775,7 @@ Identidade: o mascote do Botloft é uma chama com olhos. O ícone do app (`app/a **Instalação real** (I1, Windows 11 25H2, 2026-09-29, 0.5.0): numa máquina com os arquivos do Botloft mas sem a entrada em Aplicativos instalados, a tela nova instalou em cerca de 3 s, criou a entrada (editor "Botloft", pasta `%LOCALAPPDATA%\Botloft`), abriu o app dessa pasta e apagou a pasta temporária. O daemon seguiu rodando, porque o instalador não mexe nele (15.4). -Um teste que instala uma cópia em outra pasta e depois a desinstala apaga a entrada da instalação de verdade, porque a entrada tem o nome do produto e não o da pasta; e deixa a pasta de teste como a lembrada (`Software\github\Botloft`). Foi o que aconteceu nesta máquina. Para testar numa pasta separada, use outra conta do Windows. +Um teste que instala uma cópia em outra pasta e depois a desinstala apaga a entrada da instalação de verdade, porque a entrada tem o nome do produto e não o da pasta; e deixa a pasta de teste como a lembrada (a chave do editor, 15.4). Foi o que aconteceu nesta máquina. Para testar numa pasta separada, use outra conta do Windows. **Marcos.**