diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 4c8d98ce2f6..ef867d99109 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -2,6 +2,14 @@ name: Build
on:
workflow_dispatch:
+ inputs:
+ installer_type:
+ description: 'Windows installer type'
+ type: choice
+ options:
+ - velopack
+ - nsis
+ default: 'velopack'
pull_request:
push:
branches: ["main", "release/*", "project/*"]
@@ -84,6 +92,8 @@ jobs:
# Only set variants to the one configuration: don't let build.sh loop
# over variants, let GitHub distribute variants over multiple hosts.
variants: ${{ matrix.configuration }}
+ # Pass USE_VELOPACK to CMake when using Velopack installer (default) - Windows and macOS
+ autobuild_configure_parameters: ${{ (contains(matrix.runner, 'windows') || contains(matrix.runner, 'macos')) && (github.event.inputs.installer_type || 'velopack') == 'velopack' && '-- -DUSE_VELOPACK:BOOL=ON' || '' }}
steps:
- name: Checkout code
uses: actions/checkout@v5
@@ -126,6 +136,17 @@ jobs:
with:
token: ${{ github.token }}
+ - name: Setup .NET for Velopack
+ if: (runner.os == 'Windows' || runner.os == 'macOS') && (github.event.inputs.installer_type || 'velopack') == 'velopack'
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '9.0.x'
+
+ - name: Install Velopack CLI
+ if: (runner.os == 'Windows' || runner.os == 'macOS') && (github.event.inputs.installer_type || 'velopack') == 'velopack'
+ shell: bash
+ run: dotnet tool install -g vpk
+
- name: Build
id: build
shell: bash
@@ -271,6 +292,14 @@ jobs:
path: |
${{ steps.build.outputs.viewer_app }}
+ # Upload Velopack Releases directory (contains nupkg and RELEASES for updates)
+ - name: Upload Velopack releases
+ if: steps.build.outputs.velopack_releases
+ uses: actions/upload-artifact@v4
+ with:
+ name: "${{ steps.build.outputs.artifact }}-releases"
+ path: ${{ steps.build.outputs.velopack_releases }}
+
# The other upload of nontrivial size is the symbol file. Use a distinct
# artifact for that too.
- name: Upload symbol file
@@ -310,13 +339,14 @@ jobs:
steps:
- name: Sign and package Windows viewer
if: env.AZURE_KEY_VAULT_URI && env.AZURE_CERT_NAME && env.AZURE_CLIENT_ID && env.AZURE_CLIENT_SECRET && env.AZURE_TENANT_ID
- uses: secondlife/viewer-build-util/sign-pkg-windows@v2.0.4
+ uses: secondlife/viewer-build-util/sign-pkg-windows@geenz/velopack
with:
vault_uri: "${{ env.AZURE_KEY_VAULT_URI }}"
cert_name: "${{ env.AZURE_CERT_NAME }}"
client_id: "${{ env.AZURE_CLIENT_ID }}"
client_secret: "${{ env.AZURE_CLIENT_SECRET }}"
tenant_id: "${{ env.AZURE_TENANT_ID }}"
+ installer_type: "${{ github.event.inputs.installer_type || 'velopack' }}"
sign-and-package-mac:
env:
@@ -439,6 +469,10 @@ jobs:
with:
pattern: "*-metadata"
+ - uses: actions/download-artifact@v4
+ with:
+ pattern: "*-releases"
+
- name: Rename metadata
run: |
cp Windows-metadata/autobuild-package.xml Windows-autobuild-package.xml
@@ -464,12 +498,14 @@ jobs:
generate_release_notes: true
target_commitish: ${{ github.sha }}
append_body: true
- fail_on_unmatched_files: true
+ fail_on_unmatched_files: false
files: |
macOS-installer/*.dmg
Windows-installer/*.exe
*-autobuild-package.xml
*-viewer_version.txt
+ Windows-releases/*
+ macOS-releases/*
- name: post release URL
run: |
diff --git a/autobuild.xml b/autobuild.xml
index 7333583415a..1abb87ec222 100644
--- a/autobuild.xml
+++ b/autobuild.xml
@@ -2914,6 +2914,56 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors
description
Voxelized Hierarchical Approximate Convex Decomposition
+ velopack
+
package_description
diff --git a/indra/cmake/CMakeLists.txt b/indra/cmake/CMakeLists.txt
index 2ba282bdb78..c10f6ec934b 100644
--- a/indra/cmake/CMakeLists.txt
+++ b/indra/cmake/CMakeLists.txt
@@ -62,6 +62,7 @@ set(cmake_SOURCE_FILES
UI.cmake
UnixInstall.cmake
Variables.cmake
+ Velopack.cmake
VHACD.cmake
ViewerMiscLibs.cmake
VisualLeakDetector.cmake
diff --git a/indra/cmake/Python.cmake b/indra/cmake/Python.cmake
index 7cce190f6a5..39fd21c33f8 100644
--- a/indra/cmake/Python.cmake
+++ b/indra/cmake/Python.cmake
@@ -13,7 +13,7 @@ elseif (WINDOWS)
foreach(hive HKEY_CURRENT_USER HKEY_LOCAL_MACHINE)
# prefer more recent Python versions to older ones, if multiple versions
# are installed
- foreach(pyver 3.13 3.12 3.11 3.10 3.9 3.8 3.7)
+ foreach(pyver 3.14 3.13 3.12 3.11 3.10 3.9 3.8 3.7)
list(APPEND regpaths "[${hive}\\SOFTWARE\\Python\\PythonCore\\${pyver}\\InstallPath]")
endforeach()
endforeach()
diff --git a/indra/cmake/Velopack.cmake b/indra/cmake/Velopack.cmake
new file mode 100644
index 00000000000..a1dbe2cbe92
--- /dev/null
+++ b/indra/cmake/Velopack.cmake
@@ -0,0 +1,68 @@
+# -*- cmake -*-
+# Velopack installer and update framework integration
+# https://velopack.io/
+
+include_guard()
+
+# USE_VELOPACK controls whether to use Velopack for installer packaging (instead of NSIS/DMG)
+option(USE_VELOPACK "Use Velopack for installer packaging" OFF)
+
+if (WINDOWS)
+ include(Prebuilt)
+ use_prebuilt_binary(velopack)
+
+ add_library(ll::velopack INTERFACE IMPORTED)
+
+ target_include_directories(ll::velopack SYSTEM INTERFACE
+ ${LIBS_PREBUILT_DIR}/include/velopack
+ )
+
+ target_link_libraries(ll::velopack INTERFACE
+ ${ARCH_PREBUILT_DIRS_RELEASE}/velopack_libc.lib
+ )
+
+ # Windows system libraries required by Velopack
+ target_link_libraries(ll::velopack INTERFACE
+ winhttp
+ ole32
+ shell32
+ shlwapi
+ version
+ userenv
+ ws2_32
+ bcrypt
+ ntdll
+ )
+
+ target_compile_definitions(ll::velopack INTERFACE LL_VELOPACK=1)
+
+elseif (DARWIN)
+ include(Prebuilt)
+ use_prebuilt_binary(velopack)
+
+ add_library(ll::velopack INTERFACE IMPORTED)
+
+ target_include_directories(ll::velopack SYSTEM INTERFACE
+ ${LIBS_PREBUILT_DIR}/include/velopack
+ )
+
+ target_link_libraries(ll::velopack INTERFACE
+ ${ARCH_PREBUILT_DIRS_RELEASE}/libvelopack_libc.a
+ )
+
+ # macOS system frameworks required by Velopack (Rust static library dependencies)
+ target_link_libraries(ll::velopack INTERFACE
+ "-framework Foundation"
+ "-framework Security"
+ "-framework SystemConfiguration"
+ "-framework AppKit"
+ "-framework CoreFoundation"
+ "-framework CoreServices"
+ "-framework IOKit"
+ "-liconv"
+ "-lresolv"
+ )
+
+ target_compile_definitions(ll::velopack INTERFACE LL_VELOPACK=1)
+
+endif()
diff --git a/indra/lib/python/indra/util/llmanifest.py b/indra/lib/python/indra/util/llmanifest.py
index 1bd65eb57df..0ad0b6b1a90 100755
--- a/indra/lib/python/indra/util/llmanifest.py
+++ b/indra/lib/python/indra/util/llmanifest.py
@@ -157,7 +157,8 @@ def get_default_platform(dummy):
for use by a .bat file.""",
default=None),
dict(name='versionfile',
- description="""The name of a file containing the full version number."""),
+ description="""The name of a file containing the full version number.""",
+ default=None),
]
def usage(arguments, srctree=""):
diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt
index 0949a3b59f2..670450fd6b1 100644
--- a/indra/newview/CMakeLists.txt
+++ b/indra/newview/CMakeLists.txt
@@ -43,6 +43,7 @@ include(TinyEXR)
include(ThreeJS)
include(Tracy)
include(UI)
+include(Velopack)
include(ViewerMiscLibs)
include(ViewerManager)
include(VisualLeakDetector)
@@ -659,6 +660,7 @@ set(viewer_SOURCE_FILES
llurllineeditorctrl.cpp
llurlwhitelist.cpp
llversioninfo.cpp
+ llvvmquery.cpp
llviewchildren.cpp
llviewerassetstats.cpp
llviewerassetstorage.cpp
@@ -1337,6 +1339,7 @@ set(viewer_HEADER_FILES
llurllineeditorctrl.h
llurlwhitelist.h
llversioninfo.h
+ llvvmquery.h
llviewchildren.h
llviewerassetstats.h
llviewerassetstorage.h
@@ -1456,6 +1459,8 @@ if (DARWIN)
LIST(APPEND viewer_SOURCE_FILES llappviewermacosx-objc.h)
LIST(APPEND viewer_SOURCE_FILES llfilepicker_mac.mm)
LIST(APPEND viewer_HEADER_FILES llfilepicker_mac.h)
+ LIST(APPEND viewer_SOURCE_FILES llvelopack.cpp)
+ LIST(APPEND viewer_HEADER_FILES llvelopack.h)
set_source_files_properties(
llappviewermacosx-objc.mm
@@ -1518,16 +1523,19 @@ if (WINDOWS)
list(APPEND viewer_SOURCE_FILES
llappviewerwin32.cpp
+ llvelopack.cpp
llwindebug.cpp
)
set_source_files_properties(
llappviewerwin32.cpp
+ llvelopack.cpp
PROPERTIES
COMPILE_DEFINITIONS "${VIEWER_CHANNEL_VERSION_DEFINES}"
)
list(APPEND viewer_HEADER_FILES
llappviewerwin32.h
+ llvelopack.h
llwindebug.h
)
@@ -1941,6 +1949,7 @@ if (WINDOWS)
"--discord=${USE_DISCORD}"
"--openal=${USE_OPENAL}"
"--tracy=${USE_TRACY}"
+ "--velopack=${USE_VELOPACK}"
--build=${CMAKE_CURRENT_BINARY_DIR}
--buildtype=$
"--channel=${VIEWER_CHANNEL}"
@@ -2064,6 +2073,10 @@ if (USE_DISCORD)
target_link_libraries(${VIEWER_BINARY_NAME} ll::discord_sdk )
endif ()
+if (TARGET ll::velopack)
+ target_link_libraries(${VIEWER_BINARY_NAME} ll::velopack )
+endif ()
+
if( TARGET ll::intel_memops )
target_link_libraries(${VIEWER_BINARY_NAME} ll::intel_memops )
endif()
@@ -2261,9 +2274,11 @@ if (DARWIN)
--arch=${ARCH}
--artwork=${ARTWORK_DIR}
"--bugsplat=${BUGSPLAT_DB}"
+ --bundleid=${MACOSX_BUNDLE_GUI_IDENTIFIER}
"--discord=${USE_DISCORD}"
"--openal=${USE_OPENAL}"
"--tracy=${USE_TRACY}"
+ "--velopack=${USE_VELOPACK}"
--build=${CMAKE_CURRENT_BINARY_DIR}
--buildtype=$
"--channel=${VIEWER_CHANNEL}"
diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml
index 90582208201..3e85a5afe23 100644
--- a/indra/newview/app_settings/settings.xml
+++ b/indra/newview/app_settings/settings.xml
@@ -4237,7 +4237,17 @@
Value
0.0.0
-
+ LastInstallVersion
+
+ Comment
+ Version number of last instance of the viewer that you installed
+ Persist
+ 1
+ Type
+ String
+ Value
+
+
LimitDragDistance
Comment
diff --git a/indra/newview/installers/windows/installer_template.nsi b/indra/newview/installers/windows/installer_template.nsi
index 0e366980185..ae40e8830fb 100644
--- a/indra/newview/installers/windows/installer_template.nsi
+++ b/indra/newview/installers/windows/installer_template.nsi
@@ -767,8 +767,21 @@ Function un.UserSettingsFiles
StrCmp $DO_UNINSTALL_V2 "true" Keep # Don't remove user's settings files on auto upgrade
-# Ask if user wants to keep data files or not
-MessageBox MB_YESNO|MB_ICONQUESTION $(RemoveDataFilesMB) IDYES Remove IDNO Keep
+ClearErrors
+Push $0
+${GetParameters} $COMMANDLINE
+${GetOptionsS} $COMMANDLINE "/clrusrfiles" $0
+# GetOptionsS returns an error if option does not exist, jump past Goto.
+IfErrors +3 0
+ Pop $0
+ Goto Remove
+
+Pop $0
+ClearErrors
+
+ifSilent Keep 0
+ # Ask if user wants to keep data files or not
+ MessageBox MB_YESNO|MB_ICONQUESTION $(RemoveDataFilesMB) IDYES Remove IDNO Keep
Remove:
Push $0
@@ -864,11 +877,25 @@ RMDir "$INSTDIR"
IfFileExists "$INSTDIR" FOLDERFOUND NOFOLDER
FOLDERFOUND:
+ifSilent NOFOLDER 0
MessageBox MB_OK $(DeleteProgramFilesMB) /SD IDOK IDOK NOFOLDER
NOFOLDER:
-MessageBox MB_YESNO $(DeleteRegistryKeysMB) IDYES DeleteKeys IDNO NoDelete
+ClearErrors
+Push $0
+${GetParameters} $COMMANDLINE
+${GetOptionsS} $COMMANDLINE "/clearreg" $0
+# GetOptionsS returns an error if option does not exist, jump past Goto.
+IfErrors +3 0
+ Pop $0
+ Goto DeleteKeys
+
+Pop $0
+ClearErrors
+
+ifSilent NoDelete 0
+ MessageBox MB_YESNO $(DeleteRegistryKeysMB) IDYES DeleteKeys IDNO NoDelete
DeleteKeys:
DeleteRegKey SHELL_CONTEXT "SOFTWARE\Classes\x-grid-location-info"
diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp
index 9a421972e58..47c517eb5b5 100644
--- a/indra/newview/llappviewer.cpp
+++ b/indra/newview/llappviewer.cpp
@@ -98,6 +98,11 @@
#include "llurlmatch.h"
#include "lltextutil.h"
#include "lllogininstance.h"
+#include "llvvmquery.h"
+
+#if LL_VELOPACK
+#include "llvelopack.h"
+#endif
#include "llprogressview.h"
#include "llvocache.h"
#include "lldiskcache.h"
@@ -1112,68 +1117,18 @@ bool LLAppViewer::init()
gGLActive = false;
-#if LL_RELEASE_FOR_DOWNLOAD
- // Skip updater if this is a non-interactive instance
+//#if LL_RELEASE_FOR_DOWNLOAD
+ // Launch VVM update check (replaces SLVersionChecker)
if (!gSavedSettings.getBOOL("CmdLineSkipUpdater") && !gNonInteractive)
{
- LLProcess::Params updater;
- updater.desc = "updater process";
- // Because it's the updater, it MUST persist beyond the lifespan of the
- // viewer itself.
- updater.autokill = false;
- std::string updater_file;
-#if LL_WINDOWS
- updater_file = "SLVersionChecker.exe";
- updater.executable = gDirUtilp->getExpandedFilename(LL_PATH_EXECUTABLE, updater_file);
-#elif LL_DARWIN
- updater_file = "SLVersionChecker";
- updater.executable = gDirUtilp->add(gDirUtilp->getAppRODataDir(), "updater", updater_file);
-#else
- updater_file = "SLVersionChecker";
- updater.executable = gDirUtilp->getExpandedFilename(LL_PATH_EXECUTABLE, updater_file);
-#endif
- // add LEAP mode command-line argument to whichever of these we selected
- updater.args.add("leap");
- // UpdaterServiceSettings
- if (gSavedSettings.getBOOL("FirstLoginThisInstall"))
- {
- // Befor first login, treat this as 'manual' updates,
- // updater won't install anything, but required updates
- updater.args.add("0");
- }
- else
- {
- updater.args.add(stringize(gSavedSettings.getU32("UpdaterServiceSetting")));
- }
- // channel
- updater.args.add(LLVersionInfo::instance().getChannel());
- // testok
- updater.args.add(stringize(gSavedSettings.getBOOL("UpdaterWillingToTest")));
- // ForceAddressSize
- updater.args.add(stringize(gSavedSettings.getU32("ForceAddressSize")));
-
- try
- {
- // Run the updater. An exception from launching the updater should bother us.
- LLLeap::create(updater, true);
- mUpdaterNotFound = false;
- }
- catch (...)
- {
- LLUIString details = LLNotifications::instance().getGlobalString("LLLeapUpdaterFailure");
- details.setArg("[UPDATER_APP]", updater_file);
- OSMessageBox(
- details.getString(),
- LLStringUtil::null,
- OSMB_OK);
- mUpdaterNotFound = true;
- }
+ initVVMUpdateCheck();
+ mUpdaterNotFound = false;
}
else
{
LL_WARNS("InitInfo") << "Skipping updater check." << LL_ENDL;
}
-#endif //LL_RELEASE_FOR_DOWNLOAD
+//#endif //LL_RELEASE_FOR_DOWNLOAD
{
// Iterate over --leap command-line options. But this is a bit tricky: if
@@ -1711,6 +1666,15 @@ void LLAppViewer::flushLFSIO()
bool LLAppViewer::cleanup()
{
+#if LL_VELOPACK
+ // Apply any pending Velopack update before shutdown
+ if (velopack_is_update_pending())
+ {
+ LL_INFOS("AppInit") << "Applying pending Velopack update on shutdown..." << LL_ENDL;
+ velopack_apply_pending_update(true); // restart=true
+ }
+#endif
+
//ditch LLVOAvatarSelf instance
gAgentAvatarp = NULL;
diff --git a/indra/newview/llappviewerwin32.cpp b/indra/newview/llappviewerwin32.cpp
index 0620b625d9b..a02311b74c2 100644
--- a/indra/newview/llappviewerwin32.cpp
+++ b/indra/newview/llappviewerwin32.cpp
@@ -72,6 +72,11 @@
#include
#include
+// Velopack installer and update framework
+#if LL_VELOPACK
+#include "llvelopack.h"
+#endif
+
// Bugsplat (http://bugsplat.com) crash reporting tool
#ifdef LL_BUGSPLAT
#include "BugSplat.h"
@@ -424,6 +429,16 @@ int APIENTRY WINMAIN(HINSTANCE hInstance,
PWSTR pCmdLine,
int nCmdShow)
{
+#if LL_VELOPACK
+ // Velopack MUST be initialized first - it may handle install/uninstall
+ // commands and exit the process before we do anything else.
+ if (!velopack_initialize())
+ {
+ // Velopack handled the invocation (install/uninstall hook)
+ return 0;
+ }
+#endif
+
// Call Tracy first thing to have it allocate memory
// https://github.com/wolfpld/tracy/issues/196
LL_PROFILER_FRAME_END;
diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp
index 59d97943e3c..675693045e4 100644
--- a/indra/newview/llstartup.cpp
+++ b/indra/newview/llstartup.cpp
@@ -29,6 +29,11 @@
#include "llappviewer.h"
#include "llstartup.h"
+#if LL_VELOPACK && LL_WINDOWS
+#include "llvelopack.h"
+#include
+#endif
+
#if LL_WINDOWS
# include // _spawnl()
#else
@@ -266,6 +271,7 @@ std::unique_ptr LLStartUp::sPhases(new LLViewerStats::P
void login_show();
void login_callback(S32 option, void* userdata);
+void uninstall_nsis_if_required();
void show_release_notes_if_required();
void show_first_run_dialog();
bool first_run_dialog_callback(const LLSD& notification, const LLSD& response);
@@ -921,6 +927,7 @@ bool idle_startup()
LL_DEBUGS("AppInit") << "PeekMessage processed" << LL_ENDL;
#endif
do_startup_frame();
+ uninstall_nsis_if_required();
timeout.reset();
return false;
}
@@ -2605,6 +2612,75 @@ void release_notes_coro(const std::string url)
LLWeb::loadURLInternal(url);
}
+/**
+* Check if this is a fresh velopack install and
+* if uninstallation of old viewer is needed.
+*/
+void uninstall_nsis_if_required()
+{
+#if LL_VELOPACK && LL_WINDOWS
+ // Todo: perhaps use marker files?
+ // Debug variable isn't specific to one channel
+ // and something channel specific is needed.
+ std::string last_install_ver = gSavedSettings.getString("LastInstallVersion");
+ LLVersionInfo* ver_inst = LLVersionInfo::getInstance();
+ if (ver_inst->getChannelAndVersion() == last_install_ver)
+ {
+ return;
+ }
+ gSavedSettings.setString("LastInstallVersion",
+ ver_inst->getChannelAndVersion());
+
+ LL_INFOS() << "Looking for previous NSIS installs" << LL_ENDL;
+
+ wchar_t buffer[MAX_PATH];
+ if (!get_nsis_uninstaller_path( buffer,
+ MAX_PATH,
+ ver_inst->getMajor(),
+ ver_inst->getMinor(),
+ ver_inst->getPatch(),
+ ver_inst->getBuild())
+ )
+ {
+ return;
+ }
+
+ // Compose command line: "" /S /clearreg
+ std::wstring params = L"\"";
+ params += buffer;
+ params += L"\"";
+ // params += L" /S /clearreg"; // silent uninstall and clear registry entries
+
+ LLNotificationsUtil::add("PromptRemoveNsisInstallation", LLSD(), LLSD(),
+ [params](const LLSD& notification, const LLSD& response)
+ {
+ S32 option = LLNotificationsUtil::getSelectedOption(notification, response);
+ if (option == 1) // cancel
+ {
+ return;
+ }
+
+ LL_INFOS() << "Triggering NSIS uninstall from " << ll_convert_wide_to_string(params) << LL_ENDL;
+
+ // Launch uninstaller using explorer.exe
+ SHELLEXECUTEINFOW sei = { 0 };
+ sei.cbSize = sizeof(sei);
+ sei.fMask = SEE_MASK_DEFAULT;
+ sei.hwnd = NULL;
+ sei.lpVerb = L"runas"; // Request elevation
+ sei.lpFile = L"explorer.exe";
+ sei.lpParameters = params.c_str();
+
+ sei.nShow = SW_HIDE;
+
+ if (!ShellExecuteExW(&sei))
+ {
+ LL_WARNS("AppInit") << "Failed to launch NSIS uninstaller, error code: " << GetLastError() << LL_ENDL;
+ }
+ });
+#endif
+}
+
void validate_release_notes_coro(const std::string url)
{
LLVersionInfo& versionInfo(LLVersionInfo::instance());
diff --git a/indra/newview/llvelopack.cpp b/indra/newview/llvelopack.cpp
new file mode 100644
index 00000000000..694b75960b9
--- /dev/null
+++ b/indra/newview/llvelopack.cpp
@@ -0,0 +1,537 @@
+/**
+ * @file llvelopack.cpp
+ * @brief Velopack installer and update framework integration
+ *
+ * $LicenseInfo:firstyear=2025&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2025, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ */
+
+#if LL_VELOPACK
+
+#include "llviewerprecompiledheaders.h"
+#include "llvelopack.h"
+#include "llstring.h"
+
+#include "Velopack.h"
+
+#if LL_WINDOWS
+#include
+#include
+#include
+#include
+#include
+#include
+
+#pragma comment(lib, "shlwapi.lib")
+#pragma comment(lib, "ole32.lib")
+#pragma comment(lib, "shell32.lib")
+#endif // LL_WINDOWS
+
+// Common state
+static std::string sUpdateUrl;
+static std::function sProgressCallback;
+static vpkc_update_manager_t* sUpdateManager = nullptr;
+static vpkc_update_info_t* sPendingUpdate = nullptr;
+
+//
+// Platform-specific helpers and hooks
+//
+
+#if LL_WINDOWS
+
+static const wchar_t* PROTOCOL_SECONDLIFE = L"secondlife";
+static const wchar_t* PROTOCOL_GRID_INFO = L"x-grid-location-info";
+static std::wstring get_viewer_exe_name()
+{
+ return ll_convert(gDirUtilp->getExecutableFilename());
+}
+
+static std::wstring get_install_dir()
+{
+ wchar_t path[MAX_PATH];
+ GetModuleFileNameW(NULL, path, MAX_PATH);
+ PathRemoveFileSpecW(path);
+ return path;
+}
+
+static std::wstring get_app_name()
+{
+ // LL_VIEWER_CHANNEL is defined at compile time via CMake (e.g., "Second Life Test")
+ return LL_TO_WSTRING(LL_VIEWER_CHANNEL);
+}
+
+static std::wstring get_app_name_oneword()
+{
+ std::wstring name = get_app_name();
+ name.erase(std::remove(name.begin(), name.end(), L' '), name.end());
+ return name;
+}
+
+static std::wstring get_start_menu_path()
+{
+ wchar_t path[MAX_PATH];
+ if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_PROGRAMS, NULL, 0, path)))
+ {
+ return path;
+ }
+ return L"";
+}
+
+static std::wstring get_desktop_path()
+{
+ wchar_t path[MAX_PATH];
+ if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_DESKTOPDIRECTORY, NULL, 0, path)))
+ {
+ return path;
+ }
+ return L"";
+}
+
+static bool create_shortcut(const std::wstring& shortcut_path,
+ const std::wstring& target_path,
+ const std::wstring& arguments,
+ const std::wstring& description,
+ const std::wstring& icon_path)
+{
+ HRESULT hr = CoInitialize(NULL);
+ if (FAILED(hr)) return false;
+
+ IShellLinkW* shell_link = nullptr;
+ hr = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER,
+ IID_IShellLinkW, (void**)&shell_link);
+ if (SUCCEEDED(hr))
+ {
+ shell_link->SetPath(target_path.c_str());
+ shell_link->SetArguments(arguments.c_str());
+ shell_link->SetDescription(description.c_str());
+ shell_link->SetIconLocation(icon_path.c_str(), 0);
+
+ wchar_t work_dir[MAX_PATH];
+ wcscpy_s(work_dir, target_path.c_str());
+ PathRemoveFileSpecW(work_dir);
+ shell_link->SetWorkingDirectory(work_dir);
+
+ IPersistFile* persist_file = nullptr;
+ hr = shell_link->QueryInterface(IID_IPersistFile, (void**)&persist_file);
+ if (SUCCEEDED(hr))
+ {
+ hr = persist_file->Save(shortcut_path.c_str(), TRUE);
+ persist_file->Release();
+ }
+ shell_link->Release();
+ }
+
+ CoUninitialize();
+ return SUCCEEDED(hr);
+}
+
+static void register_protocol_handler(const std::wstring& protocol,
+ const std::wstring& description,
+ const std::wstring& exe_path)
+{
+ std::wstring key_path = L"SOFTWARE\\Classes\\" + protocol;
+ HKEY hkey;
+
+ if (RegCreateKeyExW(HKEY_CURRENT_USER, key_path.c_str(), 0, NULL,
+ REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hkey, NULL) == ERROR_SUCCESS)
+ {
+ RegSetValueExW(hkey, NULL, 0, REG_SZ,
+ (BYTE*)description.c_str(), (DWORD)((description.size() + 1) * sizeof(wchar_t)));
+ RegSetValueExW(hkey, L"URL Protocol", 0, REG_SZ, (BYTE*)L"", sizeof(wchar_t));
+ RegCloseKey(hkey);
+ }
+
+ std::wstring icon_key_path = key_path + L"\\DefaultIcon";
+ if (RegCreateKeyExW(HKEY_CURRENT_USER, icon_key_path.c_str(), 0, NULL,
+ REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hkey, NULL) == ERROR_SUCCESS)
+ {
+ std::wstring icon_value = L"\"" + exe_path + L"\"";
+ RegSetValueExW(hkey, NULL, 0, REG_SZ,
+ (BYTE*)icon_value.c_str(), (DWORD)((icon_value.size() + 1) * sizeof(wchar_t)));
+ RegCloseKey(hkey);
+ }
+
+ std::wstring cmd_key_path = key_path + L"\\shell\\open\\command";
+ if (RegCreateKeyExW(HKEY_CURRENT_USER, cmd_key_path.c_str(), 0, NULL,
+ REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hkey, NULL) == ERROR_SUCCESS)
+ {
+ std::wstring cmd_value = L"\"" + exe_path + L"\" -url \"%1\"";
+ RegSetValueExW(hkey, NULL, 0, REG_EXPAND_SZ,
+ (BYTE*)cmd_value.c_str(), (DWORD)((cmd_value.size() + 1) * sizeof(wchar_t)));
+ RegCloseKey(hkey);
+ }
+}
+
+static void parse_version(const wchar_t* version_str, int& major, int& minor, int& patch, uint64_t& build)
+{
+ major = minor = patch = 0;
+ build = 0;
+ if (!version_str) return;
+ // Use swscanf for wide strings
+ swscanf(version_str, L"%d.%d.%d.%llu", &major, &minor, &patch, &build);
+}
+
+bool get_nsis_uninstaller_path(wchar_t* path_buffer, DWORD bufSize, S32 cur_major_ver, S32 cur_minor_ver, S32 cur_patch_ver, U64 cur_build_ver)
+{
+ // Test for presence of NSIS viewer registration, then
+ // attempt to read uninstall info
+ std::wstring app_name_oneword = get_app_name_oneword();
+ std::wstring uninstall_key_path = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" + app_name_oneword;
+ HKEY hkey;
+ LONG result = RegOpenKeyExW(HKEY_LOCAL_MACHINE, uninstall_key_path.c_str(), 0, KEY_READ, &hkey);
+ if (result != ERROR_SUCCESS)
+ {
+ return false;
+ }
+
+ // Read DisplayVersion
+ wchar_t version_buf[64] = { 0 };
+ DWORD version_buf_size = sizeof(version_buf);
+ DWORD type = 0;
+ LONG ver_rv = RegGetValueW(hkey, nullptr, L"DisplayVersion", RRF_RT_REG_SZ, &type, version_buf, &version_buf_size);
+
+ if (ver_rv != ERROR_SUCCESS)
+ {
+ RegCloseKey(hkey);
+ return false;
+ }
+
+ int nsis_major = 0, nsis_minor = 0, nsis_patch = 0;
+ uint64_t nsis_build = 0;
+ parse_version(version_buf, nsis_major, nsis_minor, nsis_patch, nsis_build);
+
+ // Compare numerically
+ if ((nsis_major > cur_major_ver) ||
+ (nsis_major == cur_major_ver && nsis_minor > cur_minor_ver) ||
+ (nsis_major == cur_major_ver && nsis_minor == cur_minor_ver && nsis_patch > cur_patch_ver) ||
+ // Assume that bigger build number means newer version, which is not always true but works for our purposes
+ (nsis_major == cur_major_ver && nsis_minor == cur_minor_ver && nsis_patch == cur_patch_ver && nsis_build > cur_build_ver))
+ {
+ LL_INFOS() << "Found installed nsis version that is newer" << nsis_major << "." << nsis_minor << "." << nsis_patch << LL_ENDL;
+ RegCloseKey(hkey);
+ return false;
+ }
+
+ LONG rv = RegGetValueW(hkey, nullptr, L"UninstallString", RRF_RT_REG_SZ, &type, path_buffer, &bufSize);
+ RegCloseKey(hkey);
+ if (rv != ERROR_SUCCESS)
+ {
+ return false;
+ }
+ size_t len = wcslen(path_buffer);
+ if (len > 0)
+ {
+ if (path_buffer[0] == L'\"')
+ {
+ // Likely to contain leading "
+ memmove(path_buffer, path_buffer + 1, len * sizeof(wchar_t));
+ }
+ wchar_t* pos = wcsstr(path_buffer, L"uninst.exe");
+ if (pos)
+ {
+ // Likely to contain trailing "
+ pos[wcslen(L"uninst.exe")] = L'\0';
+ }
+ }
+ std::error_code ec;
+ std::filesystem::path path(path_buffer);
+ if (!std::filesystem::exists(path, ec))
+ {
+ return false;
+ }
+
+ // Todo: check codesigning?
+
+ return true;
+}
+
+static void unregister_protocol_handler(const std::wstring& protocol)
+{
+ std::wstring key_path = L"SOFTWARE\\Classes\\" + protocol;
+ RegDeleteTreeW(HKEY_CURRENT_USER, key_path.c_str());
+}
+
+static void register_uninstall_info(const std::wstring& install_dir,
+ const std::wstring& app_name,
+ const std::wstring& version)
+{
+ std::wstring app_name_oneword = get_app_name_oneword();
+ std::wstring key_path = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" + app_name_oneword;
+ HKEY hkey;
+
+ if (RegCreateKeyExW(HKEY_CURRENT_USER, key_path.c_str(), 0, NULL,
+ REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hkey, NULL) == ERROR_SUCCESS)
+ {
+ std::wstring exe_path = install_dir + L"\\" + get_viewer_exe_name();
+ std::wstring uninstall_cmd = L"\"" + install_dir + L"\\Update.exe\" --uninstall";
+
+ RegSetValueExW(hkey, L"DisplayName", 0, REG_SZ,
+ (BYTE*)app_name.c_str(), (DWORD)((app_name.size() + 1) * sizeof(wchar_t)));
+ RegSetValueExW(hkey, L"DisplayVersion", 0, REG_SZ,
+ (BYTE*)version.c_str(), (DWORD)((version.size() + 1) * sizeof(wchar_t)));
+ RegSetValueExW(hkey, L"Publisher", 0, REG_SZ,
+ (BYTE*)L"Linden Research, Inc.", 44);
+ RegSetValueExW(hkey, L"UninstallString", 0, REG_SZ,
+ (BYTE*)uninstall_cmd.c_str(), (DWORD)((uninstall_cmd.size() + 1) * sizeof(wchar_t)));
+ RegSetValueExW(hkey, L"DisplayIcon", 0, REG_SZ,
+ (BYTE*)exe_path.c_str(), (DWORD)((exe_path.size() + 1) * sizeof(wchar_t)));
+
+ std::wstring link_url = L"https://support.secondlife.com/contact-support/";
+ RegSetValueExW(hkey, L"HelpLink", 0, REG_SZ,
+ (BYTE*)link_url.c_str(), (DWORD)((link_url.size() + 1) * sizeof(wchar_t)));
+
+ link_url = L"https://secondlife.com/whatis/";
+ RegSetValueExW(hkey, L"URLInfoAbout", 0, REG_SZ,
+ (BYTE*)link_url.c_str(), (DWORD)((link_url.size() + 1) * sizeof(wchar_t)));
+
+ link_url = L"http://secondlife.com/support/downloads/";
+ RegSetValueExW(hkey, L"URLUpdateInfo", 0, REG_SZ,
+ (BYTE*)link_url.c_str(), (DWORD)((link_url.size() + 1) * sizeof(wchar_t)));
+
+ DWORD no_modify = 1;
+ RegSetValueExW(hkey, L"NoModify", 0, REG_DWORD, (BYTE*)&no_modify, sizeof(DWORD));
+ RegSetValueExW(hkey, L"NoRepair", 0, REG_DWORD, (BYTE*)&no_modify, sizeof(DWORD));
+
+ DWORD estimated_size = 120000;
+ RegSetValueExW(hkey, L"EstimatedSize", 0, REG_DWORD, (BYTE*)&estimated_size, sizeof(DWORD));
+
+ RegCloseKey(hkey);
+ }
+}
+
+static void unregister_uninstall_info()
+{
+ std::wstring app_name_oneword = get_app_name_oneword();
+ std::wstring key_path = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" + app_name_oneword;
+ RegDeleteTreeW(HKEY_CURRENT_USER, key_path.c_str());
+}
+
+static void create_shortcuts(const std::wstring& install_dir, const std::wstring& app_name)
+{
+ std::wstring exe_path = install_dir + L"\\" + get_viewer_exe_name();
+ std::wstring start_menu_dir = get_start_menu_path() + L"\\" + app_name;
+ std::wstring desktop_path = get_desktop_path();
+
+ CreateDirectoryW(start_menu_dir.c_str(), NULL);
+
+ create_shortcut(start_menu_dir + L"\\" + app_name + L".lnk",
+ exe_path, L"", app_name, exe_path);
+
+ create_shortcut(desktop_path + L"\\" + app_name + L".lnk",
+ exe_path, L"", app_name, exe_path);
+}
+
+static void remove_shortcuts(const std::wstring& app_name)
+{
+ std::wstring start_menu_dir = get_start_menu_path() + L"\\" + app_name;
+ std::wstring desktop_path = get_desktop_path();
+
+ DeleteFileW((start_menu_dir + L"\\" + app_name + L".lnk").c_str());
+ RemoveDirectoryW(start_menu_dir.c_str());
+ DeleteFileW((desktop_path + L"\\" + app_name + L".lnk").c_str());
+}
+
+static void on_after_install(void* user_data, const char* app_version)
+{
+ std::wstring install_dir = get_install_dir();
+ std::wstring app_name = get_app_name();
+ std::wstring exe_path = install_dir + L"\\" + get_viewer_exe_name();
+
+ int len = MultiByteToWideChar(CP_UTF8, 0, app_version, -1, NULL, 0);
+ std::wstring version(len, 0);
+ MultiByteToWideChar(CP_UTF8, 0, app_version, -1, &version[0], len);
+
+ register_protocol_handler(PROTOCOL_SECONDLIFE, L"URL:Second Life", exe_path);
+ register_protocol_handler(PROTOCOL_GRID_INFO, L"URL:Second Life", exe_path);
+ register_uninstall_info(install_dir, app_name, version);
+ create_shortcuts(install_dir, app_name);
+}
+
+static void on_before_uninstall(void* user_data, const char* app_version)
+{
+ std::wstring app_name = get_app_name();
+
+ unregister_protocol_handler(PROTOCOL_SECONDLIFE);
+ unregister_protocol_handler(PROTOCOL_GRID_INFO);
+ unregister_uninstall_info();
+ remove_shortcuts(app_name);
+}
+
+static void on_log_message(void* user_data, const char* level, const char* message)
+{
+ OutputDebugStringA("[Velopack] ");
+ OutputDebugStringA(level);
+ OutputDebugStringA(": ");
+ OutputDebugStringA(message);
+ OutputDebugStringA("\n");
+}
+
+#elif LL_DARWIN
+
+// macOS-specific hooks
+// TODO: Implement protocol handler registration via Launch Services
+// TODO: Implement app bundle management
+
+static void on_after_install(void* user_data, const char* app_version)
+{
+ // macOS handles protocol registration via Info.plist CFBundleURLTypes
+ // No additional registration needed at runtime
+ LL_INFOS("Velopack") << "macOS post-install hook called for version: " << app_version << LL_ENDL;
+}
+
+static void on_before_uninstall(void* user_data, const char* app_version)
+{
+ LL_INFOS("Velopack") << "macOS pre-uninstall hook called for version: " << app_version << LL_ENDL;
+}
+
+static void on_log_message(void* user_data, const char* level, const char* message)
+{
+ LL_INFOS("Velopack") << "[" << level << "] " << message << LL_ENDL;
+}
+
+#endif // LL_WINDOWS / LL_DARWIN
+
+//
+// Common progress callback
+//
+
+static void on_progress(void* user_data, size_t progress)
+{
+ if (sProgressCallback)
+ {
+ sProgressCallback(static_cast(progress));
+ }
+}
+
+//
+// Public API - Cross-platform
+//
+
+bool velopack_initialize()
+{
+ vpkc_set_logger(on_log_message, nullptr);
+
+#if LL_WINDOWS || LL_DARWIN
+ vpkc_app_set_hook_after_install(on_after_install);
+ vpkc_app_set_hook_before_uninstall(on_before_uninstall);
+#endif
+
+ vpkc_app_run(nullptr);
+ return true;
+}
+
+void velopack_check_for_updates()
+{
+ if (sUpdateUrl.empty())
+ {
+ LL_DEBUGS("Velopack") << "No update URL set, skipping update check" << LL_ENDL;
+ return;
+ }
+
+ if (!sUpdateManager)
+ {
+ vpkc_update_options_t options = {};
+ options.AllowVersionDowngrade = false;
+ options.ExplicitChannel = nullptr;
+
+ if (!vpkc_new_update_manager(sUpdateUrl.c_str(), &options, nullptr, &sUpdateManager))
+ {
+ LL_WARNS("Velopack") << "Failed to create update manager" << LL_ENDL;
+ return;
+ }
+ }
+
+ vpkc_update_info_t* update_info = nullptr;
+ vpkc_update_check_t result = vpkc_check_for_updates(sUpdateManager, &update_info);
+
+ if (result == UPDATE_AVAILABLE && update_info)
+ {
+ LL_INFOS("Velopack") << "Update available, downloading..." << LL_ENDL;
+ if (vpkc_download_updates(sUpdateManager, update_info, on_progress, nullptr))
+ {
+ if (sPendingUpdate)
+ {
+ vpkc_free_update_info(sPendingUpdate);
+ }
+ sPendingUpdate = update_info;
+ LL_INFOS("Velopack") << "Update downloaded and pending" << LL_ENDL;
+ }
+ else
+ {
+ LL_WARNS("Velopack") << "Failed to download update" << LL_ENDL;
+ vpkc_free_update_info(update_info);
+ }
+ }
+ else
+ {
+ LL_DEBUGS("Velopack") << "No update available (result=" << result << ")" << LL_ENDL;
+ }
+}
+
+std::string velopack_get_current_version()
+{
+ if (!sUpdateManager)
+ {
+ return "";
+ }
+
+ char version[64];
+ size_t len = vpkc_get_current_version(sUpdateManager, version, sizeof(version));
+ if (len > 0)
+ {
+ return std::string(version, len);
+ }
+ return "";
+}
+
+bool velopack_is_update_pending()
+{
+ return sPendingUpdate != nullptr;
+}
+
+void velopack_apply_pending_update(bool restart)
+{
+ if (!sUpdateManager || !sPendingUpdate || !sPendingUpdate->TargetFullRelease)
+ {
+ LL_WARNS("Velopack") << "Cannot apply update: no pending update or manager" << LL_ENDL;
+ return;
+ }
+
+ LL_INFOS("Velopack") << "Applying pending update (restart=" << restart << ")" << LL_ENDL;
+ vpkc_wait_exit_then_apply_updates(sUpdateManager,
+ sPendingUpdate->TargetFullRelease,
+ false,
+ restart,
+ nullptr, 0);
+}
+
+void velopack_set_update_url(const std::string& url)
+{
+ sUpdateUrl = url;
+ LL_INFOS("Velopack") << "Update URL set to: " << url << LL_ENDL;
+}
+
+void velopack_set_progress_callback(std::function callback)
+{
+ sProgressCallback = callback;
+}
+
+#endif // LL_VELOPACK
diff --git a/indra/newview/llvelopack.h b/indra/newview/llvelopack.h
new file mode 100644
index 00000000000..93d1ca4776d
--- /dev/null
+++ b/indra/newview/llvelopack.h
@@ -0,0 +1,49 @@
+/**
+ * @file llvelopack.h
+ * @brief Velopack installer and update framework integration
+ *
+ * $LicenseInfo:firstyear=2025&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2025, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ */
+
+#ifndef LL_LLVELOPACK_H
+#define LL_LLVELOPACK_H
+
+#if LL_VELOPACK
+
+#include
+#include
+
+bool velopack_initialize();
+void velopack_check_for_updates();
+std::string velopack_get_current_version();
+bool velopack_is_update_pending();
+void velopack_apply_pending_update(bool restart = true);
+void velopack_set_update_url(const std::string& url);
+void velopack_set_progress_callback(std::function callback);
+
+#if LL_WINDOWS
+bool get_nsis_uninstaller_path(wchar_t* path_buffer, DWORD bufSize, S32 cur_major_ver, S32 cur_minor_ver, S32 cur_patch_ver, U64 cur_build_ver);
+#endif
+
+#endif // LL_VELOPACK
+#endif
+// EOF
diff --git a/indra/newview/llvvmquery.cpp b/indra/newview/llvvmquery.cpp
new file mode 100644
index 00000000000..b1f964b1e45
--- /dev/null
+++ b/indra/newview/llvvmquery.cpp
@@ -0,0 +1,165 @@
+/**
+ * @file llvvmquery.cpp
+ * @brief Query the Viewer Version Manager (VVM) for update information
+ *
+ * $LicenseInfo:firstyear=2025&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2025, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ */
+
+#include "llviewerprecompiledheaders.h"
+#include "llvvmquery.h"
+
+#include "llcorehttputil.h"
+#include "llcoros.h"
+#include "llevents.h"
+#include "llviewernetwork.h"
+#include "llversioninfo.h"
+#include "llviewercontrol.h"
+#include "llhasheduniqueid.h"
+#include "lluri.h"
+#include "llsys.h"
+
+#if LL_VELOPACK
+#include "llvelopack.h"
+#endif
+
+namespace
+{
+ std::string get_platform_string()
+ {
+#if LL_WINDOWS
+ return "win64";
+#elif LL_DARWIN
+ return "mac64";
+#elif LL_LINUX
+ return "lnx64";
+#else
+ return "unknown";
+#endif
+ }
+
+ std::string get_platform_version()
+ {
+ return LLOSInfo::instance().getOSVersionString();
+ }
+
+ std::string get_machine_id()
+ {
+ unsigned char id[MD5HEX_STR_SIZE];
+ if (llHashedUniqueID(id))
+ {
+ return std::string(reinterpret_cast(id));
+ }
+ return "unknown";
+ }
+
+ void query_vvm_coro()
+ {
+ // Get base URL from grid manager
+ std::string base_url = LLGridManager::getInstance()->getUpdateServiceURL();
+ if (base_url.empty())
+ {
+ LL_WARNS("VVM") << "No update service URL configured" << LL_ENDL;
+ return;
+ }
+
+ // Gather parameters for VVM query
+ std::string channel = LLVersionInfo::instance().getChannel();
+ std::string version = LLVersionInfo::instance().getVersion();
+ std::string platform = get_platform_string();
+ std::string platform_version = get_platform_version();
+ std::string test_ok = gSavedSettings.getBOOL("UpdaterWillingToTest") ? "testok" : "testno";
+ std::string machine_id = get_machine_id();
+
+ // Build URL: {base}/v1.2/{channel}/{version}/{platform}/{platform_version}/{testok}/{uuid}
+ std::string url = base_url + "/v1.2/" +
+ LLURI::escape(channel) + "/" +
+ LLURI::escape(version) + "/" +
+ platform + "/" +
+ LLURI::escape(platform_version) + "/" +
+ test_ok + "/" +
+ machine_id;
+
+ LL_INFOS("VVM") << "Querying VVM: " << url << LL_ENDL;
+
+ // Make HTTP GET request
+ LLCore::HttpRequest::policy_t httpPolicy(LLCore::HttpRequest::DEFAULT_POLICY_ID);
+ LLCoreHttpUtil::HttpCoroutineAdapter::ptr_t adapter =
+ std::make_shared("VVMQuery", httpPolicy);
+ LLCore::HttpRequest::ptr_t request = std::make_shared();
+
+ LLSD result = adapter->getAndSuspend(request, url);
+
+ // Check HTTP status
+ LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS];
+ LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults);
+
+ if (!status)
+ {
+ if (status.getType() == 404)
+ {
+ LL_INFOS("VVM") << "Unmanaged channel, no updates available" << LL_ENDL;
+ return;
+ }
+ LL_WARNS("VVM") << "VVM query failed: " << status.toString() << LL_ENDL;
+ return;
+ }
+
+ // Extract update URL for current platform
+ LLSD platforms = result["platforms"];
+ if (platforms.has(platform))
+ {
+ std::string update_url = platforms[platform]["url"].asString();
+#if LL_VELOPACK
+ std::string velopack_url = platforms[platform]["velopack_url"].asString();
+ if (!velopack_url.empty())
+ {
+ LL_INFOS("VVM") << "Velopack update URL: " << velopack_url << LL_ENDL;
+ velopack_set_update_url(velopack_url);
+ velopack_check_for_updates();
+ }
+ else
+#endif
+ if (!update_url.empty())
+ {
+ LL_INFOS("VVM") << "Update available at: " << update_url << LL_ENDL;
+ }
+ }
+ else
+ {
+ LL_INFOS("VVM") << "No update available for platform: " << platform << LL_ENDL;
+ }
+
+ // Post release notes URL to the relnotes event pump
+ std::string relnotes = result["more_info"].asString();
+ if (!relnotes.empty())
+ {
+ LL_INFOS("VVM") << "Release notes URL: " << relnotes << LL_ENDL;
+ LLEventPumps::instance().obtain("relnotes").post(relnotes);
+ }
+ }
+}
+
+void initVVMUpdateCheck()
+{
+ LL_INFOS("VVM") << "Initializing VVM update check" << LL_ENDL;
+ LLCoros::instance().launch("VVMUpdateCheck", &query_vvm_coro);
+}
diff --git a/indra/newview/llvvmquery.h b/indra/newview/llvvmquery.h
new file mode 100644
index 00000000000..977d82af643
--- /dev/null
+++ b/indra/newview/llvvmquery.h
@@ -0,0 +1,42 @@
+/**
+ * @file llvvmquery.h
+ * @brief Query the Viewer Version Manager (VVM) for update information
+ *
+ * $LicenseInfo:firstyear=2025&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2025, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ */
+
+#ifndef LL_LLVVMQUERY_H
+#define LL_LLVVMQUERY_H
+
+/**
+ * Initialize the VVM update check.
+ *
+ * This launches a coroutine that queries the Viewer Version Manager (VVM)
+ * to check for available updates. If an update is available, it configures
+ * Velopack with the update URL and initiates the update check/download.
+ *
+ * The release notes URL from the VVM response is posted to the "relnotes"
+ * event pump for display.
+ */
+void initVVMUpdateCheck();
+
+#endif // LL_LLVVMQUERY_H
diff --git a/indra/newview/skins/default/xui/en/notifications.xml b/indra/newview/skins/default/xui/en/notifications.xml
index 82e2229d76d..9b1d47ac9d6 100644
--- a/indra/newview/skins/default/xui/en/notifications.xml
+++ b/indra/newview/skins/default/xui/en/notifications.xml
@@ -200,6 +200,20 @@ No tutorial is currently available.
yestext="OK"/>
+
+[APP_NAME] found an installation from an older version. Do you want to uninstall the previous version now?
+
+The uninstaller may display additional prompts requesting permission to access or modify files on your disk.
+ confirm
+
+
+