From 92d02d00088dee90374e7db18c7894f225448998 Mon Sep 17 00:00:00 2001 From: Davito Date: Wed, 14 Oct 2026 10:34:48 +0200 Subject: [PATCH] Eigener Installer mit Standard- und Portable-Modus, Updater fuer beide Varianten --- README.md | 11 +- build.bat | 20 +- installer/EuerChat.nsi | 387 ++++++++++++++++++++++++++++++++++++++ src-tauri/src/storage.rs | 10 +- src-tauri/src/updater.rs | 103 ++++++++-- src-tauri/tauri.conf.json | 13 +- 6 files changed, 508 insertions(+), 36 deletions(-) create mode 100644 installer/EuerChat.nsi diff --git a/README.md b/README.md index 2077e31..49e1a5d 100644 --- a/README.md +++ b/README.md @@ -38,14 +38,17 @@ Ein moderner, performanter und schlanker Desktop-Client für Windows 64-bit, geb ## Schnellstart -1. **Installieren:** `EuerChat__x64-setup.exe` ausführen (Installation pro Benutzer, keine Administratorrechte nötig). Beim ersten Start einfach den Namen eingeben und verbinden. - * Die Konfiguration liegt in `%APPDATA%\EuerChat\config.json` und enthält eventuell Passwörter. Sie ist nicht Teil des Installers und gehört nicht ins Repository (steht in `.gitignore`). - * Alternativ ohne Installation: `EuerChat.exe` direkt starten. Eine alte `config.json` neben der EXE wird noch gelesen und beim nächsten Speichern nach `%APPDATA%` übernommen. +1. **Installieren:** `EuerChat__x64-setup.exe` ausführen und die Installationsart wählen. Beim ersten Start einfach den Namen eingeben und verbinden. + * **Standard** (empfohlen): für alle Benutzer, standardmäßig nach `C:\Program Files\EuerChat`, mit Startmenü- und Desktop-Verknüpfung. Erfordert Administratorrechte (UAC-Abfrage), Deinstallation über die Windows-Einstellungen. Die Konfiguration liegt in `%APPDATA%\EuerChat\config.json`. + * **Portable:** nur Kopieren in einen frei wählbaren Ordner (Vorschlag: neben dem Installer), z. B. auf einen USB-Stick. Keine Administratorrechte, keine Registry-Einträge. Die Konfiguration liegt im Unterordner `data` (Marker-Datei `portable.flag`). Entfernen: Ordner löschen. + * Die Konfiguration kann Passwörter enthalten. Sie ist nicht Teil des Installers und gehört nicht ins Repository (steht in `.gitignore`). + * Der Installer lässt sich auch still nutzen: `/S /MODE=standard|portable /D=` (`/D=` immer zuletzt). Der Updater verwendet genau diese Parameter und aktualisiert Standard- und Portable-Installationen an Ort und Stelle (Standard mit UAC-Abfrage, Portable ohne). Einstellungen bleiben erhalten. + * Alternativ ohne Installer: `EuerChat.exe` direkt starten. Eine alte `config.json` neben der EXE wird noch gelesen und beim nächsten Speichern nach `%APPDATA%` übernommen. 2. **Installer bauen:** ```powershell .\build.bat ``` - Benötigt Rust und Node.js. Der Installer liegt danach in `dist\`, die portable `EuerChat.exe` im Hauptordner. + Benötigt Rust, Node.js und NSIS 3 (`winget install NSIS.NSIS`; der von Tauri geladene NSIS-Cache wird ebenfalls gefunden). Das Skript baut die App ohne Tauri-Bundler und dann den eigenen Installer `installer\EuerChat.nsi`. Ergebnis: `dist\EuerChat__x64-setup.exe`, dazu die lose `EuerChat.exe` im Hauptordner. 3. **Tests:** `cd src-tauri; cargo test` 4. **Release veröffentlichen:** 1. Version in `src-tauri\tauri.conf.json` und `src-tauri\Cargo.toml` erhöhen, optional Release-Notizen in `RELEASE_NOTES.md` schreiben. diff --git a/build.bat b/build.bat index d8e3e90..37e4a44 100644 --- a/build.bat +++ b/build.bat @@ -8,14 +8,26 @@ if not exist node_modules ( if errorlevel 1 goto :fail ) -echo Baue Installer (NSIS)... -call npx tauri build +echo Baue EuerChat.exe ... +call npx tauri build --no-bundle if errorlevel 1 goto :fail +for /f "usebackq delims=" %%v in (`node -p "require('./src-tauri/tauri.conf.json').version"`) do set VERSION=%%v + +set MAKENSIS=%LOCALAPPDATA%\tauri\NSIS\makensis.exe +if not exist "%MAKENSIS%" set MAKENSIS=%ProgramFiles(x86)%\NSIS\makensis.exe +if not exist "%MAKENSIS%" ( + echo [FEHLER] makensis nicht gefunden. NSIS 3 installieren: winget install NSIS.NSIS + goto :fail +) + if not exist dist mkdir dist -copy /y "src-tauri\target\release\bundle\nsis\EuerChat_*_x64-setup.exe" dist\ >nul +echo Baue Installer %VERSION% ... +"%MAKENSIS%" /V2 /DVERSION=%VERSION% installer\EuerChat.nsi +if errorlevel 1 goto :fail + copy /y src-tauri\target\release\euer-chat.exe EuerChat.exe >nul -echo [OK] Installer liegt in dist\ - EuerChat.exe (portabel) im Hauptordner. +echo [OK] Installer: dist\EuerChat_%VERSION%_x64-setup.exe pause exit /b 0 diff --git a/installer/EuerChat.nsi b/installer/EuerChat.nsi new file mode 100644 index 0000000..908511c --- /dev/null +++ b/installer/EuerChat.nsi @@ -0,0 +1,387 @@ +; EuerChat-Installer +; Standard (Program Files, alle Benutzer) oder Portable (frei waehlbarer Ordner). +; Parameter, auch fuer stille Updates: /S /UPDATE /MODE=standard|portable /NS /D= (/D= immer zuletzt) + +Unicode true +ManifestDPIAware true +SetCompressor /SOLID lzma +RequestExecutionLevel user + +!ifndef VERSION + !define VERSION "0.0.0" +!endif +!ifndef EXE_SRC + !define EXE_SRC "..\src-tauri\target\release\euer-chat.exe" +!endif +!ifndef OUT_FILE + !define OUT_FILE "..\dist\EuerChat_${VERSION}_x64-setup.exe" +!endif + +!define PRODUCT "EuerChat" +!define PUBLISHER "euer.tv" +!define MAINEXE "euer-chat.exe" +!define BUNDLEID "tv.euer.chat" +!define WEBVIEW2APPGUID "{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" +!define PLACEHOLDER_DIR "placeholder\${PRODUCT}" + +; TEST_USER_CONTEXT: Testbuild ohne Administratorrechte (HKCU, eigener Registry-Schluessel). +!ifdef TEST_USER_CONTEXT + !define ROOT HKCU + !define UNINSTKEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT}-Test" +!else + !define ROOT HKLM + !define UNINSTKEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT}" +!endif + +Name "${PRODUCT}" +OutFile "${OUT_FILE}" +BrandingText "${PRODUCT} ${VERSION}" +InstallDir "${PLACEHOLDER_DIR}" + +VIProductVersion "${VERSION}.0" +VIAddVersionKey "ProductName" "${PRODUCT}" +VIAddVersionKey "FileDescription" "${PRODUCT} Installer" +VIAddVersionKey "FileVersion" "${VERSION}" +VIAddVersionKey "ProductVersion" "${VERSION}" +VIAddVersionKey "CompanyName" "${PUBLISHER}" +VIAddVersionKey "LegalCopyright" "${PUBLISHER}" + +!include MUI2.nsh +!include FileFunc.nsh +!include LogicLib.nsh +!include nsDialogs.nsh +!include x64.nsh + +!insertmacro GetParameters +!insertmacro GetOptions +!insertmacro un.GetParameters +!insertmacro un.GetOptions + +Var Portable +Var UpdateMode +Var NoShortcut +Var Elevated +Var ModeGiven +Var RadioStandard +Var RadioPortable + +!define MUI_ICON "..\src-tauri\icons\icon.ico" +!define MUI_UNICON "..\src-tauri\icons\icon.ico" +!define MUI_ABORTWARNING + +; Seiten +!define MUI_PAGE_CUSTOMFUNCTION_PRE WelcomePre +!insertmacro MUI_PAGE_WELCOME +Page custom ModePageCreate ModePageLeave +!insertmacro MUI_PAGE_DIRECTORY +!insertmacro MUI_PAGE_INSTFILES +!define MUI_FINISHPAGE_NOAUTOCLOSE +!define MUI_FINISHPAGE_RUN +!define MUI_FINISHPAGE_RUN_TEXT "${PRODUCT} jetzt starten" +!define MUI_FINISHPAGE_RUN_FUNCTION RunMainBinary +!insertmacro MUI_PAGE_FINISH + +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES + +!insertmacro MUI_LANGUAGE "German" + +; --------------------------------------------------------------------------- +; Hilfsfunktionen +; --------------------------------------------------------------------------- + +Function SetDefaultDir + ${If} $Portable = 1 + StrCpy $INSTDIR "$EXEDIR\${PRODUCT}" + ${Else} + ReadRegStr $0 ${ROOT} "${UNINSTKEY}" "InstallLocation" + ${If} $0 != "" + StrCpy $INSTDIR $0 + ${Else} + StrCpy $INSTDIR "$PROGRAMFILES64\${PRODUCT}" + ${EndIf} + ${EndIf} +FunctionEnd + +; Startet den Installer bei Bedarf mit Administratorrechten neu (nur fuer die Standard-Installation). +Function RequireAdmin + !ifndef TEST_USER_CONTEXT + System::Call 'shell32::IsUserAnAdmin()i.r0' + ${If} $0 = 0 + ${If} $Elevated = 1 + ${IfNot} ${Silent} + MessageBox MB_ICONSTOP "Für die Standard-Installation sind Administratorrechte nötig." + ${EndIf} + Abort + ${EndIf} + StrCpy $1 "/MODE=standard /ELEVATED" + ${If} ${Silent} + StrCpy $1 "$1 /S" + ${EndIf} + ${If} $UpdateMode = 1 + StrCpy $1 "$1 /UPDATE" + ${EndIf} + ${If} $NoShortcut = 1 + StrCpy $1 "$1 /NS" + ${EndIf} + StrCpy $1 "$1 /D=$INSTDIR" + System::Call 'shell32::ShellExecute(i 0, t "runas", t "$EXEPATH", t "$1", t "", i 1) i .r2' + ${If} $2 > 32 + Quit + ${EndIf} + ${IfNot} ${Silent} + MessageBox MB_ICONEXCLAMATION "Ohne Administratorrechte kann die Standard-Installation nicht fortgesetzt werden. Wähle stattdessen „Portable“." + ${EndIf} + Abort + ${EndIf} + !endif +FunctionEnd + +Function CloseRunningApp + nsExec::ExecToStack 'cmd /c tasklist /FI "IMAGENAME eq ${MAINEXE}" /NH | find /I "${MAINEXE}"' + Pop $0 + Pop $1 + ${If} $0 = 0 + ${IfNot} ${Silent} + MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "${PRODUCT} läuft noch und wird für die Installation beendet." IDOK +2 + Abort + ${EndIf} + nsExec::ExecToStack 'taskkill /F /IM "${MAINEXE}"' + Pop $0 + Pop $1 + Sleep 800 + ${EndIf} +FunctionEnd + +Function RunMainBinary + ; Ueber den Explorer starten, damit die App ohne Administratorrechte laeuft. + Exec '"$WINDIR\explorer.exe" "$INSTDIR\${MAINEXE}"' +FunctionEnd + +; --------------------------------------------------------------------------- +; Installer +; --------------------------------------------------------------------------- + +Function .onInit + ${If} ${RunningX64} + SetRegView 64 + ${EndIf} + + ${GetParameters} $R9 + ${GetOptions} $R9 "/UPDATE" $0 + ${IfNot} ${Errors} + StrCpy $UpdateMode 1 + ${EndIf} + ${GetOptions} $R9 "/NS" $0 + ${IfNot} ${Errors} + StrCpy $NoShortcut 1 + ${EndIf} + ${GetOptions} $R9 "/ELEVATED" $0 + ${IfNot} ${Errors} + StrCpy $Elevated 1 + ${EndIf} + ${GetOptions} $R9 "/MODE=" $0 + ${IfNot} ${Errors} + StrCpy $ModeGiven 1 + ${If} $0 == "portable" + StrCpy $Portable 1 + ${EndIf} + ${EndIf} + + ; Stille Installation ohne Angabe: Standard + ${If} ${Silent} + StrCpy $ModeGiven 1 + ${EndIf} + + ${If} $INSTDIR == "${PLACEHOLDER_DIR}" + Call SetDefaultDir + ${EndIf} + + ${If} $ModeGiven = 1 + ${AndIf} $Portable <> 1 + Call RequireAdmin + ${EndIf} +FunctionEnd + +Function WelcomePre + ${If} $Elevated = 1 + Abort + ${EndIf} +FunctionEnd + +Function ModePageCreate + ${If} $ModeGiven = 1 + Abort + ${EndIf} + + !insertmacro MUI_HEADER_TEXT "Installationsart" "Wie soll ${PRODUCT} installiert werden?" + nsDialogs::Create 1018 + Pop $0 + + ${NSD_CreateRadioButton} 0 6u 100% 12u "Standard-Installation (empfohlen)" + Pop $RadioStandard + ${NSD_AddStyle} $RadioStandard ${WS_GROUP} + ${NSD_CreateLabel} 12u 20u 94% 38u "Installiert ${PRODUCT} für alle Benutzer, standardmäßig nach C:\Program Files. Mit Startmenü- und Desktop-Verknüpfung; deinstallierbar über die Windows-Einstellungen. Erfordert Administratorrechte." + Pop $0 + + ${NSD_CreateRadioButton} 0 66u 100% 12u "Portable (Ordner frei wählbar)" + Pop $RadioPortable + ${NSD_CreateLabel} 12u 80u 94% 46u "Kopiert ${PRODUCT} nur in einen Ordner deiner Wahl, z. B. auf einen USB-Stick. Keine Administratorrechte und keine Einträge in der Registry. Die Einstellungen liegen im Unterordner „data“. Zum Entfernen genügt es, den Ordner zu löschen." + Pop $0 + + ${If} $Portable = 1 + ${NSD_Check} $RadioPortable + ${Else} + ${NSD_Check} $RadioStandard + ${EndIf} + + nsDialogs::Show +FunctionEnd + +Function ModePageLeave + ${NSD_GetState} $RadioPortable $0 + ${If} $0 = ${BST_CHECKED} + StrCpy $Portable 1 + ${Else} + StrCpy $Portable 0 + ${EndIf} + + Call SetDefaultDir + + ${If} $Portable = 0 + Call RequireAdmin + ${EndIf} +FunctionEnd + +Section "WebView2" + ${If} $UpdateMode <> 1 + ReadRegStr $4 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\${WEBVIEW2APPGUID}" "pv" + ${If} $4 == "" + ReadRegStr $4 HKCU "SOFTWARE\Microsoft\EdgeUpdate\Clients\${WEBVIEW2APPGUID}" "pv" + ${EndIf} + ${If} $4 == "" + DetailPrint "Microsoft Edge WebView2 wird heruntergeladen ..." + Delete "$TEMP\MicrosoftEdgeWebview2Setup.exe" + NSISdl::download "https://go.microsoft.com/fwlink/p/?LinkId=2124703" "$TEMP\MicrosoftEdgeWebview2Setup.exe" + Pop $0 + ${If} $0 != "success" + Abort "WebView2 konnte nicht heruntergeladen werden. Bitte die Internetverbindung prüfen und erneut versuchen." + ${EndIf} + DetailPrint "Microsoft Edge WebView2 wird installiert ..." + ExecWait '"$TEMP\MicrosoftEdgeWebview2Setup.exe" /silent /install' $1 + ${If} $1 <> 0 + Abort "Die Installation von WebView2 ist fehlgeschlagen." + ${EndIf} + ${EndIf} + ${EndIf} +SectionEnd + +Section "Install" + SetOutPath $INSTDIR + Call CloseRunningApp + File "${EXE_SRC}" + + ${If} $Portable = 1 + ; Die Marker-Datei schaltet den portablen Datenordner der App frei. + FileOpen $0 "$INSTDIR\portable.flag" w + FileWrite $0 "${PRODUCT} portable: Einstellungen liegen im Ordner data.$\r$\n" + FileClose $0 + CreateDirectory "$INSTDIR\data" + ${Else} + Delete "$INSTDIR\portable.flag" + WriteUninstaller "$INSTDIR\uninstall.exe" + + !ifdef TEST_USER_CONTEXT + SetShellVarContext current + !else + SetShellVarContext all + !endif + + WriteRegStr ${ROOT} "${UNINSTKEY}" "DisplayName" "${PRODUCT}" + WriteRegStr ${ROOT} "${UNINSTKEY}" "DisplayVersion" "${VERSION}" + WriteRegStr ${ROOT} "${UNINSTKEY}" "Publisher" "${PUBLISHER}" + WriteRegStr ${ROOT} "${UNINSTKEY}" "InstallLocation" "$INSTDIR" + WriteRegStr ${ROOT} "${UNINSTKEY}" "DisplayIcon" "$INSTDIR\${MAINEXE}" + WriteRegStr ${ROOT} "${UNINSTKEY}" "UninstallString" '"$INSTDIR\uninstall.exe"' + WriteRegDWORD ${ROOT} "${UNINSTKEY}" "NoModify" 1 + WriteRegDWORD ${ROOT} "${UNINSTKEY}" "NoRepair" 1 + WriteRegDWORD ${ROOT} "${UNINSTKEY}" "EstimatedSize" 7600 + + CreateShortcut "$SMPROGRAMS\${PRODUCT}.lnk" "$INSTDIR\${MAINEXE}" + ${If} $UpdateMode <> 1 + ${AndIf} $NoShortcut <> 1 + CreateShortcut "$DESKTOP\${PRODUCT}.lnk" "$INSTDIR\${MAINEXE}" + ${EndIf} + ${EndIf} +SectionEnd + +; --------------------------------------------------------------------------- +; Deinstallation (nur Standard-Installation) +; --------------------------------------------------------------------------- + +Function un.onInit + ${If} ${RunningX64} + SetRegView 64 + ${EndIf} + + ${GetParameters} $R9 + ${GetOptions} $R9 "/ELEVATED" $0 + ${IfNot} ${Errors} + StrCpy $Elevated 1 + ${EndIf} + ${GetOptions} $R9 "/UPDATE" $0 + ${IfNot} ${Errors} + StrCpy $UpdateMode 1 + ${EndIf} + + !ifndef TEST_USER_CONTEXT + System::Call 'shell32::IsUserAnAdmin()i.r0' + ${If} $0 = 0 + ${If} $Elevated = 1 + Abort + ${EndIf} + StrCpy $1 "/ELEVATED" + ${If} ${Silent} + StrCpy $1 "$1 /S" + ${EndIf} + ${If} $UpdateMode = 1 + StrCpy $1 "$1 /UPDATE" + ${EndIf} + StrCpy $1 "$1 _?=$INSTDIR" + System::Call 'shell32::ShellExecute(i 0, t "runas", t "$EXEPATH", t "$1", t "", i 1) i .r2' + ${If} $2 > 32 + Quit + ${EndIf} + Abort + ${EndIf} + !endif +FunctionEnd + +Section "Uninstall" + !ifdef TEST_USER_CONTEXT + SetShellVarContext current + !else + SetShellVarContext all + !endif + + nsExec::ExecToStack 'taskkill /F /IM "${MAINEXE}"' + Pop $0 + Pop $1 + Sleep 500 + + Delete "$INSTDIR\${MAINEXE}" + Delete "$INSTDIR\uninstall.exe" + RMDir "$INSTDIR" + Delete "$SMPROGRAMS\${PRODUCT}.lnk" + Delete "$DESKTOP\${PRODUCT}.lnk" + DeleteRegKey ${ROOT} "${UNINSTKEY}" + + ${If} $UpdateMode <> 1 + ${AndIfNot} ${Silent} + MessageBox MB_YESNO|MB_ICONQUESTION|MB_DEFBUTTON2 "Sollen auch die Einstellungen von ${PRODUCT} (Konfiguration und gespeicherte Passwörter) gelöscht werden?" IDNO skip_data + SetShellVarContext current + RMDir /r "$APPDATA\${PRODUCT}" + RMDir /r "$LOCALAPPDATA\${BUNDLEID}" + skip_data: + ${EndIf} +SectionEnd diff --git a/src-tauri/src/storage.rs b/src-tauri/src/storage.rs index f68a0a4..a24eecf 100644 --- a/src-tauri/src/storage.rs +++ b/src-tauri/src/storage.rs @@ -5,14 +5,22 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; use tauri::{PhysicalPosition, PhysicalSize}; -fn exe_dir() -> PathBuf { +pub fn exe_dir() -> PathBuf { std::env::current_exe() .ok() .and_then(|p| p.parent().map(PathBuf::from)) .unwrap_or_else(|| PathBuf::from(".")) } +/// Der Installer legt bei der portablen Installation die Marker-Datei `portable.flag` neben die EXE. +pub fn is_portable() -> bool { + exe_dir().join("portable.flag").is_file() +} + pub fn data_dir() -> PathBuf { + if is_portable() { + return exe_dir().join("data"); + } std::env::var_os("APPDATA") .map(|d| PathBuf::from(d).join("EuerChat")) .unwrap_or_else(exe_dir) diff --git a/src-tauri/src/updater.rs b/src-tauri/src/updater.rs index 9eaf33f..eec4076 100644 --- a/src-tauri/src/updater.rs +++ b/src-tauri/src/updater.rs @@ -366,28 +366,65 @@ async fn download_to_file( // Installation // --------------------------------------------------------------------------- +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum InstallKind { + /// Mit dem Standard-Installer installiert (uninstall.exe neben der EXE), meist in Program Files. + Standard, + /// Portable Installation (portable.flag neben der EXE). + Portable, + /// Lose EXE ohne Installation, z. B. aus dem Build-Ordner. + Loose, +} + +fn detect_install_kind(dir: &Path) -> InstallKind { + if dir.join("portable.flag").is_file() { + InstallKind::Portable + } else if dir.join("uninstall.exe").is_file() { + InstallKind::Standard + } else { + InstallKind::Loose + } +} + +/// PowerShell-Skript: wartet auf das Ende dieser App, installiert still und startet EuerChat neu. +fn build_update_script(pid: u32, installer: &Path, current_exe: &Path, kind: InstallKind) -> String { + let quote = |p: &Path| p.to_string_lossy().replace('\'', "''"); + let dir = current_exe.parent().map(quote).unwrap_or_default(); + + // Standard-Installationen liegen meist in Program Files und brauchen Administratorrechte (UAC). + let (args, elevate) = match kind { + InstallKind::Standard => (format!("/S /UPDATE /MODE=standard /D={dir}"), true), + InstallKind::Portable => (format!("/S /UPDATE /MODE=portable /D={dir}"), false), + InstallKind::Loose => ("/S /UPDATE /MODE=standard".to_string(), true), + }; + let verb = if elevate { " -Verb RunAs" } else { "" }; + let relaunch = match kind { + InstallKind::Loose => format!( + "$t = Join-Path $env:ProgramFiles 'EuerChat\\euer-chat.exe'; \ + if (Test-Path $t) {{ Start-Process -FilePath $t }} else {{ Start-Process -FilePath '{}' }}", + quote(current_exe) + ), + _ => format!("Start-Process -FilePath '{}'", quote(current_exe)), + }; + + // Bricht der Nutzer die UAC-Abfrage ab, wird trotzdem die bisherige Version neu gestartet. + format!( + "Wait-Process -Id {pid} -Timeout 30 -ErrorAction SilentlyContinue; \ + try {{ Start-Process -FilePath '{installer}' -ArgumentList '{args}'{verb} -Wait }} catch {{}}; \ + {relaunch}", + installer = quote(installer), + ) +} + /// Startet den Installer erst, wenn diese App beendet ist, und danach EuerChat neu. fn spawn_installer_after_exit(installer: &Path) -> std::io::Result<()> { use std::os::windows::process::CommandExt; - let quote = |p: &Path| p.to_string_lossy().replace('\'', "''"); let current = std::env::current_exe()?; - let installed_here = current + let kind = current .parent() - .is_some_and(|dir| dir.join("uninstall.exe").exists()); - - // Eine portable EXE wird per Installer in %LOCALAPPDATA%\EuerChat aktualisiert; dort neu starten. - let script = format!( - "Wait-Process -Id {pid} -Timeout 30 -ErrorAction SilentlyContinue; \ - Start-Process -FilePath '{installer}' -ArgumentList '/S','/UPDATE' -Wait; \ - $installed = Join-Path $env:LOCALAPPDATA 'EuerChat\\euer-chat.exe'; \ - if (-not {installed_here} -and (Test-Path $installed)) {{ Start-Process -FilePath $installed }} \ - else {{ Start-Process -FilePath '{current}' }}", - pid = std::process::id(), - installer = quote(installer), - installed_here = if installed_here { "$true" } else { "$false" }, - current = quote(¤t), - ); + .map_or(InstallKind::Loose, detect_install_kind); + let script = build_update_script(std::process::id(), installer, ¤t, kind); std::process::Command::new("powershell.exe") .args(["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", &script]) @@ -529,6 +566,40 @@ mod tests { assert!(parse_expected_checksum(&other, "x.exe").unwrap_err().contains("x.exe")); } + #[test] + fn install_kind_detection() { + let dir = std::env::temp_dir().join(format!("euerchat-kind-test-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + assert_eq!(detect_install_kind(&dir), InstallKind::Loose); + std::fs::write(dir.join("uninstall.exe"), b"x").unwrap(); + assert_eq!(detect_install_kind(&dir), InstallKind::Standard); + std::fs::write(dir.join("portable.flag"), b"x").unwrap(); + assert_eq!(detect_install_kind(&dir), InstallKind::Portable); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn update_script_per_install_kind() { + let installer = Path::new(r"C:\Users\O'Brien\AppData\Roaming\EuerChat\updates\EuerChat_0.2.0_x64-setup.exe"); + let exe = Path::new(r"C:\Program Files\EuerChat\euer-chat.exe"); + + let standard = build_update_script(42, installer, exe, InstallKind::Standard); + assert!(standard.contains("Wait-Process -Id 42")); + assert!(standard.contains("/MODE=standard /D=C:\\Program Files\\EuerChat'")); + assert!(standard.contains("-Verb RunAs")); + assert!(standard.contains("O''Brien"), "Apostrophe müssen verdoppelt werden"); + assert!(standard.contains("Start-Process -FilePath 'C:\\Program Files\\EuerChat\\euer-chat.exe'")); + + let portable = build_update_script(1, installer, Path::new(r"E:\Tools\EuerChat\euer-chat.exe"), InstallKind::Portable); + assert!(portable.contains("/MODE=portable /D=E:\\Tools\\EuerChat'")); + assert!(!portable.contains("RunAs"), "portable Updates brauchen keine Administratorrechte"); + + let loose = build_update_script(1, installer, Path::new(r"D:\build\euer-chat.exe"), InstallKind::Loose); + assert!(loose.contains("/MODE=standard'")); + assert!(!loose.contains("/D=")); + assert!(loose.contains("$env:ProgramFiles")); + } + #[test] fn managed_installer_files() { assert!(is_managed_installer_file("EuerChat_0.2.0_x64-setup.exe")); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index dde1d14..313bf26 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -39,16 +39,7 @@ } }, "bundle": { - "active": true, - "targets": ["nsis"], - "icon": ["icons/32x32.png", "icons/128x128.png", "icons/icon.ico"], - "shortDescription": "EuerChat - IRC-Chat und Livestream-Client fuer euer.tv", - "windows": { - "nsis": { - "installMode": "currentUser", - "languages": ["German"], - "displayLanguageSelector": false - } - } + "active": false, + "icon": ["icons/32x32.png", "icons/128x128.png", "icons/icon.ico"] } }