From 19dfc4c6b472cde7e819fcc3c3ef85124ae7502e Mon Sep 17 00:00:00 2001 From: Phelipe Lorran <112786693+httpsphl@users.noreply.github.com> Date: Wed, 30 Sep 2026 10:59:59 -0300 Subject: [PATCH] fix(installer): the publisher is Botloft, and installs made as "github" are adopted The app's file properties and Installed apps named "github" as the publisher: Tauri's default, the second part of the identifier. On an unsigned program that reads as someone posing as GitHub. bundle.publisher is now "Botloft" for the app and the setup window, and the installer's own properties name the company too. Tauri also names the registry key that keeps the install folder after the publisher, so the installer's hooks adopt an install made under the old name: BotloftAdoptOldFolder copies the folder from Software\github\Botloft to Software\Botloft\Botloft when the new key is missing and installs there, unless /D chose another folder. It runs before the first page (wizard and passive update) and at the start of the install (the quiet installer has no pages). After installing, the old key goes, and Software\github only if it is left empty. The setup window reads the new key, then the old one. Tested for real: over 0.6.0 installed under the old publisher, with the app open, the new installer run as the updater (/P /UPDATE /R) adopted the folder, removed the old key and reopened the app; the daemon kept running. A custom folder, /D and the quiet path were tested with a small installer compiled around the same hooks file and test keys. Spec 15.4 follows, and no longer says an update runs the old uninstaller: with Tauri 2.12 it does not. --- app/src-setup/src/installed.rs | 20 ++++++----- app/src-setup/tauri.conf.json | 1 + app/src-tauri/tauri.conf.json | 1 + app/src-tauri/windows/hooks.nsh | 61 ++++++++++++++++++++++++++++----- docs/spec.md | 7 ++-- 5 files changed, 71 insertions(+), 19 deletions(-) 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.**