Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions app/src-setup/src/installed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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\<publisher>\<product>`; the publisher is "github", the second
/// part of the identifier (spec 15.4).
const FOLDER_KEY: &str = r"Software\github\Botloft";
/// `Software\<publisher>\<product>`. 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<String> {
RegKey::predef(HKEY_CURRENT_USER)
Expand All @@ -52,11 +53,14 @@ mod registry {
}

pub fn remembered_folder() -> Option<String> {
RegKey::predef(HKEY_CURRENT_USER)
.open_subkey(FOLDER_KEY)
.ok()?
.get_value::<String, _>("")
.ok()
let user = RegKey::predef(HKEY_CURRENT_USER);
FOLDER_KEYS.iter().find_map(|key| {
user.open_subkey(key)
.ok()?
.get_value::<String, _>("")
.ok()
.filter(|folder| !folder.trim().is_empty())
})
}
}

Expand Down
1 change: 1 addition & 0 deletions app/src-setup/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
}
},
"bundle": {
"publisher": "Botloft",
"copyright": "Copyright 2026 Phelipe Lorran",
"icon": ["../src-tauri/icons/icon.ico"]
}
Expand Down
1 change: 1 addition & 0 deletions app/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
},
"bundle": {
"active": true,
"publisher": "Botloft",
"copyright": "Copyright 2026 Phelipe Lorran",
"targets": [
"nsis"
Expand Down
61 changes: 53 additions & 8 deletions app/src-tauri/windows/hooks.nsh
Original file line number Diff line number Diff line change
Expand Up @@ -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\<publisher>\<product>. 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\<publisher>\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
Expand Down
7 changes: 4 additions & 3 deletions docs/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<target triple>.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 `<home>\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\<publisher>\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\<editor>\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
Expand Down Expand Up @@ -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.**

Expand Down
Loading