From 4dc59a87d15e0d8b5e89f4d6358bb0af5ee453fe Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Fri, 19 Dec 2025 12:50:25 -0500 Subject: [PATCH 01/17] Integrate Velopack installer and update framework Note: Updates don't quite work yet. --- autobuild.xml | 50 +++ indra/cmake/Python.cmake | 2 +- indra/cmake/Velopack.cmake | 37 +++ indra/lib/python/indra/util/llmanifest.py | 3 +- indra/newview/CMakeLists.txt | 7 + indra/newview/llappviewerwin32.cpp | 15 + indra/newview/llvelopack.cpp | 379 ++++++++++++++++++++++ indra/newview/llvelopack.h | 45 +++ indra/newview/viewer_manifest.py | 73 +++++ 9 files changed, 609 insertions(+), 2 deletions(-) create mode 100644 indra/cmake/Velopack.cmake create mode 100644 indra/newview/llvelopack.cpp create mode 100644 indra/newview/llvelopack.h diff --git a/autobuild.xml b/autobuild.xml index d58a785b6b6..0586bf91c33 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 + + platforms + + windows64 + + archive + + creds + github + hash + 77d82909367f116c60edd073f584d0c53e6ea591 + hash_algorithm + sha1 + url + https://api.github.com/repos/secondlife-3p/3p-velopack/releases/assets/330718186 + + name + windows64 + + darwin64 + + archive + + creds + github + hash + 3cabca6840973bbb3a90bf0cc3390bc86e745379 + hash_algorithm + sha1 + url + https://api.github.com/repos/secondlife-3p/3p-velopack/releases/assets/330718181 + + name + darwin64 + + + license + MIT + license_file + LICENSES/velopack.txt + copyright + Velopack Ltd. + version + a2592d1.20376699173 + name + velopack + description + Velopack C/C++ Library + package_description 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..ce1f8299939 --- /dev/null +++ b/indra/cmake/Velopack.cmake @@ -0,0 +1,37 @@ +# -*- cmake -*- +# Velopack installer and update framework integration +# https://velopack.io/ + +include_guard() + +# TODO: Add Mac and Linux support +# Only available on Windows (so far) +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) +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 0c5f3f3fd91..05710c8be2b 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) @@ -1520,6 +1521,7 @@ if (WINDOWS) list(APPEND viewer_SOURCE_FILES llappviewerwin32.cpp + llvelopack.cpp llwindebug.cpp ) set_source_files_properties( @@ -1530,6 +1532,7 @@ if (WINDOWS) list(APPEND viewer_HEADER_FILES llappviewerwin32.h + llvelopack.h llwindebug.h ) @@ -2066,6 +2069,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() 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/llvelopack.cpp b/indra/newview/llvelopack.cpp new file mode 100644 index 00000000000..0d91d7c6863 --- /dev/null +++ b/indra/newview/llvelopack.cpp @@ -0,0 +1,379 @@ +/** + * @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 +#include +#include +#include +#include + +#include "Velopack.h" + +#pragma comment(lib, "shlwapi.lib") +#pragma comment(lib, "ole32.lib") +#pragma comment(lib, "shell32.lib") + +static const wchar_t* PROTOCOL_SECONDLIFE = L"secondlife"; +static const wchar_t* PROTOCOL_GRID_INFO = L"x-grid-location-info"; +static const wchar_t* VIEWER_EXE_NAME = L"SecondLifeViewer.exe"; + +static std::string sUpdateUrl; +static std::function sProgressCallback; +static vpkc_update_manager_t* sUpdateManager = nullptr; +static vpkc_update_info_t* sPendingUpdate = nullptr; + +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() +{ + // TODO: Read from build_data.json + return L"Second Life"; +} + +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 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"\\" + 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))); + + 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"\\" + 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"\\" + 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"); +} + +static void on_progress(void* user_data, size_t progress) +{ + if (sProgressCallback) + { + sProgressCallback(static_cast(progress)); + } +} + +bool velopack_initialize() +{ + vpkc_set_logger(on_log_message, nullptr); + vpkc_app_set_hook_after_install(on_after_install); + vpkc_app_set_hook_before_uninstall(on_before_uninstall); + vpkc_app_run(nullptr); + return true; +} + +void velopack_check_for_updates() +{ + if (sUpdateUrl.empty()) + { + 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)) + { + 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) + { + if (vpkc_download_updates(sUpdateManager, update_info, on_progress, nullptr)) + { + if (sPendingUpdate) + { + vpkc_free_update_info(sPendingUpdate); + } + sPendingUpdate = update_info; + } + else + { + vpkc_free_update_info(update_info); + } + } +} + +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) + { + return; + } + + vpkc_wait_exit_then_apply_updates(sUpdateManager, + sPendingUpdate->TargetFullRelease, + false, + restart, + nullptr, 0); +} + +void velopack_set_update_url(const std::string& url) +{ + sUpdateUrl = url; +} + +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..430ea9e5183 --- /dev/null +++ b/indra/newview/llvelopack.h @@ -0,0 +1,45 @@ +/** + * @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); + +#endif // LL_VELOPACK +#endif +// EOF diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py index 94d234686a2..7d204797618 100755 --- a/indra/newview/viewer_manifest.py +++ b/indra/newview/viewer_manifest.py @@ -764,6 +764,78 @@ def INSTDIR(path): return '\n'.join(result) def package_finish(self): + # Check if we should use Velopack instead of NSIS + # Note: as of 2026.01's release, we will be building with Velopack's one click install. + # We maintain the legacy NSIS packaging mainly for TPVs at this point. + if self.args.get('velopack', 'OFF') == 'ON': + self.velopack_package_finish() + return + + # NSIS packaging (legacy) + self.nsis_package_finish() + + def velopack_package_finish(self): + # packId determines install folder: %LocalAppData%\{packId} + # Uses same naming as NSIS INSTNAME for channel separation + pack_id = self.app_name_oneword() # "SecondLife", "SecondLifeBeta", etc. + # Velopack requires SemVer2 (3-part: major.minor.patch), viewer has 4 parts + # TODO: Treat patch as build number. + pack_version = '.'.join(self.args['version'][:3]) + pack_title = self.app_name() # Display name with spaces + pack_dir = self.get_dst_prefix() + main_exe = self.final_exe() + + # Icon path - use ll_icon.ico which has PNG-embedded icons that Velopack can parse + # (install_icon.ico causes parsing errors in Velopack's ICO library) + icon_path = os.path.join(self.get_src_prefix(), 'res', 'll_icon.ico') + + # Splash image (we should probably add one - uncomment later when we have one) + # splash_path = os.path.join(self.get_src_prefix(), 'installers', 'windows', 'splash.png') + + # Build vpk command + vpk_args = [ + 'vpk', 'pack', + '--packId', pack_id, + '--packVersion', pack_version, + '--packDir', pack_dir, + '--mainExe', main_exe, + '--packTitle', pack_title, + ] + + # Add icon if exists + # TODO: Convert all of our icons into something vpk works better with. + # We have some bitmap data in our icons that it really doesn't like. + if os.path.exists(icon_path): + vpk_args.extend(['--icon', icon_path]) + + # Add splash image when it exists + # if os.path.exists(splash_path): + # vpk_args.extend(['--splashImage', splash_path]) + + print("Running Velopack packaging: %s" % ' '.join(vpk_args)) + + # Run vpk command + import subprocess + result = subprocess.run(vpk_args, cwd=os.path.dirname(pack_dir), capture_output=True, text=True) + if result.returncode != 0: + print("vpk stdout: %s" % result.stdout) + print("vpk stderr: %s" % result.stderr) + raise ManifestError("Velopack packaging failed with code %d" % result.returncode) + + # Set output file name - Velopack uses format: {packId}-{version}-win-setup.exe + # But for compatibility with existing build system, we use our naming + self.package_file = self.installer_base_name() + '_Setup.exe' + + # Velopack outputs to a Releases directory, we may need to move/rename the setup + # Velopack format: {packId}-win-Setup.exe + velopack_setup = os.path.join(os.path.dirname(pack_dir), 'Releases', '%s-win-Setup.exe' % pack_id) + our_setup = os.path.join(os.path.dirname(pack_dir), self.package_file) + if os.path.exists(velopack_setup) and velopack_setup != our_setup: + shutil.move(velopack_setup, our_setup) + print("Moved %s to %s" % (velopack_setup, our_setup)) + + def nsis_package_finish(self): + """Package the viewer using NSIS installer (legacy)""" # a standard map of strings for replacing in the templates substitution_strings = { 'version' : '.'.join(self.args['version']), @@ -1323,6 +1395,7 @@ def construct(self): dict(name='discord', description="""Indication discord social sdk libraries are needed""", default='OFF'), dict(name='openal', description="""Indication openal libraries are needed""", default='OFF'), dict(name='tracy', description="""Indication tracy profiler is enabled""", default='OFF'), + dict(name='velopack', description="""Use Velopack installer instead of NSIS""", default='OFF'), ] try: main(extra=extra_arguments) From 386316cbbde695e8b1855fe5c60a2059c3916709 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Fri, 19 Dec 2025 17:50:43 -0500 Subject: [PATCH 02/17] Try to get CI/CD working with velopack. --- .github/workflows/build.yaml | 21 ++++++++++++++++++++- indra/cmake/Velopack.cmake | 4 ++++ indra/newview/CMakeLists.txt | 2 ++ indra/newview/llvelopack.cpp | 4 ++-- indra/newview/viewer_manifest.py | 23 ++++++++++++++--------- 5 files changed, 42 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 4c8d98ce2f6..e2f2200fac8 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) + autobuild_configure_parameters: ${{ (github.event.inputs.installer_type || 'velopack') == 'velopack' && '-DUSE_VELOPACK:BOOL=ON' || '' }} steps: - name: Checkout code uses: actions/checkout@v5 @@ -271,6 +281,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 +328,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: diff --git a/indra/cmake/Velopack.cmake b/indra/cmake/Velopack.cmake index ce1f8299939..653120f8826 100644 --- a/indra/cmake/Velopack.cmake +++ b/indra/cmake/Velopack.cmake @@ -4,6 +4,10 @@ include_guard() +# USE_VELOPACK controls whether to use Velopack for Windows installer packaging (instead of NSIS) +# This is separate from the ll::velopack library which is always linked on Windows +option(USE_VELOPACK "Use Velopack for Windows installer packaging" OFF) + # TODO: Add Mac and Linux support # Only available on Windows (so far) if (WINDOWS) diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 05710c8be2b..227aec4b22c 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -1526,6 +1526,7 @@ if (WINDOWS) ) set_source_files_properties( llappviewerwin32.cpp + llvelopack.cpp PROPERTIES COMPILE_DEFINITIONS "${VIEWER_CHANNEL_VERSION_DEFINES}" ) @@ -1946,6 +1947,7 @@ if (WINDOWS) "--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/llvelopack.cpp b/indra/newview/llvelopack.cpp index 0d91d7c6863..5b1eaae541e 100644 --- a/indra/newview/llvelopack.cpp +++ b/indra/newview/llvelopack.cpp @@ -60,8 +60,8 @@ static std::wstring get_install_dir() static std::wstring get_app_name() { - // TODO: Read from build_data.json - return L"Second Life"; + // 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() diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py index 7d204797618..98400ff850c 100755 --- a/indra/newview/viewer_manifest.py +++ b/indra/newview/viewer_manifest.py @@ -822,18 +822,23 @@ def velopack_package_finish(self): print("vpk stderr: %s" % result.stderr) raise ManifestError("Velopack packaging failed with code %d" % result.returncode) - # Set output file name - Velopack uses format: {packId}-{version}-win-setup.exe - # But for compatibility with existing build system, we use our naming - self.package_file = self.installer_base_name() + '_Setup.exe' - - # Velopack outputs to a Releases directory, we may need to move/rename the setup - # Velopack format: {packId}-win-Setup.exe - velopack_setup = os.path.join(os.path.dirname(pack_dir), 'Releases', '%s-win-Setup.exe' % pack_id) - our_setup = os.path.join(os.path.dirname(pack_dir), self.package_file) - if os.path.exists(velopack_setup) and velopack_setup != our_setup: + # Velopack outputs to a Releases directory + releases_dir = os.path.join(os.path.dirname(pack_dir), 'Releases') + + # Move the setup exe INTO pack_dir so it's included in the Windows-app artifact + # Use hyphen format (-Setup.exe) to avoid the *_Setup.exe exclusion pattern in viewer_app + # Velopack creates: {packId}-win-Setup.exe + velopack_setup = os.path.join(releases_dir, '%s-win-Setup.exe' % pack_id) + # Keep Velopack naming convention (hyphen, not underscore) + self.package_file = '%s-Setup.exe' % pack_id + our_setup = os.path.join(pack_dir, self.package_file) + if os.path.exists(velopack_setup): shutil.move(velopack_setup, our_setup) print("Moved %s to %s" % (velopack_setup, our_setup)) + # Output the Releases directory path for artifact upload (contains nupkg, RELEASES for updates) + self.set_github_output('velopack_releases', releases_dir) + def nsis_package_finish(self): """Package the viewer using NSIS installer (legacy)""" # a standard map of strings for replacing in the templates From d4432c995e6db6f482abcfd8d178faad11df686f Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Fri, 19 Dec 2025 17:53:49 -0500 Subject: [PATCH 03/17] Update build.yaml --- .github/workflows/build.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index e2f2200fac8..019afbb4aa0 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -92,8 +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) - autobuild_configure_parameters: ${{ (github.event.inputs.installer_type || 'velopack') == 'velopack' && '-DUSE_VELOPACK:BOOL=ON' || '' }} + # Pass USE_VELOPACK to CMake when using Velopack installer (default) - Windows only + autobuild_configure_parameters: ${{ contains(matrix.runner, 'windows') && (github.event.inputs.installer_type || 'velopack') == 'velopack' && '-DUSE_VELOPACK:BOOL=ON' || '' }} steps: - name: Checkout code uses: actions/checkout@v5 From 2bdd1c2a3aa34fdd241ba7cf0d7f968d111a7846 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Fri, 19 Dec 2025 17:58:08 -0500 Subject: [PATCH 04/17] Update build.yaml --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 019afbb4aa0..9cd9ddd32c9 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -93,7 +93,7 @@ jobs: # over variants, let GitHub distribute variants over multiple hosts. variants: ${{ matrix.configuration }} # Pass USE_VELOPACK to CMake when using Velopack installer (default) - Windows only - autobuild_configure_parameters: ${{ contains(matrix.runner, 'windows') && (github.event.inputs.installer_type || 'velopack') == 'velopack' && '-DUSE_VELOPACK:BOOL=ON' || '' }} + autobuild_configure_parameters: ${{ contains(matrix.runner, 'windows') && (github.event.inputs.installer_type || 'velopack') == 'velopack' && '-- -DUSE_VELOPACK:BOOL=ON' || '' }} steps: - name: Checkout code uses: actions/checkout@v5 From 5baae4ab17a23857b67e460b545c82f739816801 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Fri, 19 Dec 2025 19:15:36 -0500 Subject: [PATCH 05/17] Update build.yaml --- .github/workflows/build.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 9cd9ddd32c9..458910a4785 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -136,6 +136,11 @@ jobs: with: token: ${{ github.token }} + - name: Install Velopack CLI + if: runner.os == 'Windows' && (github.event.inputs.installer_type || 'velopack') == 'velopack' + shell: bash + run: dotnet tool install -g vpk + - name: Build id: build shell: bash From ad82d46f7ffa3855b68315ee64ae432a4f9d15d1 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Wed, 21 Jan 2026 11:51:34 -0500 Subject: [PATCH 06/17] Add Velopack update support for macOS and VVM integration --- .github/workflows/build.yaml | 14 ++- indra/cmake/Velopack.cmake | 32 ++++++- indra/newview/CMakeLists.txt | 4 + indra/newview/llappviewer.cpp | 74 ++++---------- indra/newview/llvelopack.cpp | 70 ++++++++++++-- indra/newview/llvvmquery.cpp | 159 +++++++++++++++++++++++++++++++ indra/newview/llvvmquery.h | 42 ++++++++ indra/newview/viewer_manifest.py | 55 +++++++++++ 8 files changed, 380 insertions(+), 70 deletions(-) create mode 100644 indra/newview/llvvmquery.cpp create mode 100644 indra/newview/llvvmquery.h diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 458910a4785..b274499ce55 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -92,8 +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 only - autobuild_configure_parameters: ${{ contains(matrix.runner, 'windows') && (github.event.inputs.installer_type || 'velopack') == 'velopack' && '-- -DUSE_VELOPACK:BOOL=ON' || '' }} + # 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 @@ -137,7 +137,7 @@ jobs: token: ${{ github.token }} - name: Install Velopack CLI - if: runner.os == 'Windows' && (github.event.inputs.installer_type || 'velopack') == 'velopack' + if: (runner.os == 'Windows' || runner.os == 'macOS') && (github.event.inputs.installer_type || 'velopack') == 'velopack' shell: bash run: dotnet tool install -g vpk @@ -463,6 +463,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 @@ -488,12 +492,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/indra/cmake/Velopack.cmake b/indra/cmake/Velopack.cmake index 653120f8826..fa37553e22f 100644 --- a/indra/cmake/Velopack.cmake +++ b/indra/cmake/Velopack.cmake @@ -4,12 +4,9 @@ include_guard() -# USE_VELOPACK controls whether to use Velopack for Windows installer packaging (instead of NSIS) -# This is separate from the ll::velopack library which is always linked on Windows -option(USE_VELOPACK "Use Velopack for Windows installer packaging" OFF) +# USE_VELOPACK controls whether to use Velopack for installer packaging (instead of NSIS/DMG) +option(USE_VELOPACK "Use Velopack for installer packaging" OFF) -# TODO: Add Mac and Linux support -# Only available on Windows (so far) if (WINDOWS) include(Prebuilt) use_prebuilt_binary(velopack) @@ -38,4 +35,29 @@ if (WINDOWS) ) 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 + target_link_libraries(ll::velopack INTERFACE + "-framework Foundation" + "-framework Security" + "-framework SystemConfiguration" + "-framework AppKit" + ) + + target_compile_definitions(ll::velopack INTERFACE LL_VELOPACK=1) + endif() diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 227aec4b22c..242bef6e3c8 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -660,6 +660,7 @@ set(viewer_SOURCE_FILES llurllineeditorctrl.cpp llurlwhitelist.cpp llversioninfo.cpp + llvvmquery.cpp llviewchildren.cpp llviewerassetstats.cpp llviewerassetstorage.cpp @@ -1339,6 +1340,7 @@ set(viewer_HEADER_FILES llurllineeditorctrl.h llurlwhitelist.h llversioninfo.h + llvvmquery.h llviewchildren.h llviewerassetstats.h llviewerassetstorage.h @@ -1459,6 +1461,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 diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 569fd30b210..701ae189bdb 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 @@ -1699,6 +1654,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/llvelopack.cpp b/indra/newview/llvelopack.cpp index 5b1eaae541e..32bad18f380 100644 --- a/indra/newview/llvelopack.cpp +++ b/indra/newview/llvelopack.cpp @@ -29,27 +29,36 @@ #include "llviewerprecompiledheaders.h" #include "llvelopack.h" +#include "Velopack.h" + +#if LL_WINDOWS #include #include #include #include #include -#include "Velopack.h" - #pragma comment(lib, "shlwapi.lib") #pragma comment(lib, "ole32.lib") #pragma comment(lib, "shell32.lib") +#endif // LL_WINDOWS -static const wchar_t* PROTOCOL_SECONDLIFE = L"secondlife"; -static const wchar_t* PROTOCOL_GRID_INFO = L"x-grid-location-info"; -static const wchar_t* VIEWER_EXE_NAME = L"SecondLifeViewer.exe"; - +// 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 const wchar_t* VIEWER_EXE_NAME = L"SecondLifeViewer.exe"; + static std::wstring get_install_dir() { wchar_t path[MAX_PATH]; @@ -275,6 +284,35 @@ static void on_log_message(void* user_data, const char* level, const char* messa 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) @@ -283,11 +321,19 @@ static void on_progress(void* user_data, size_t 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; } @@ -296,6 +342,7 @@ void velopack_check_for_updates() { if (sUpdateUrl.empty()) { + LL_DEBUGS("Velopack") << "No update URL set, skipping update check" << LL_ENDL; return; } @@ -307,6 +354,7 @@ void velopack_check_for_updates() if (!vpkc_new_update_manager(sUpdateUrl.c_str(), &options, nullptr, &sUpdateManager)) { + LL_WARNS("Velopack") << "Failed to create update manager" << LL_ENDL; return; } } @@ -316,6 +364,7 @@ void velopack_check_for_updates() 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) @@ -323,12 +372,18 @@ void velopack_check_for_updates() 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() @@ -356,9 +411,11 @@ 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, @@ -369,6 +426,7 @@ void velopack_apply_pending_update(bool restart) 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) diff --git a/indra/newview/llvvmquery.cpp b/indra/newview/llvvmquery.cpp new file mode 100644 index 00000000000..49fb0bfbf67 --- /dev/null +++ b/indra/newview/llvvmquery.cpp @@ -0,0 +1,159 @@ +/** + * @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 (!update_url.empty()) + { + LL_INFOS("VVM") << "Update available at: " << update_url << LL_ENDL; +#if LL_VELOPACK + velopack_set_update_url(update_url); + velopack_check_for_updates(); +#endif + } + } + 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/viewer_manifest.py b/indra/newview/viewer_manifest.py index 98400ff850c..f1ff5b210e1 100755 --- a/indra/newview/viewer_manifest.py +++ b/indra/newview/viewer_manifest.py @@ -1203,6 +1203,61 @@ def package_finish(self): arcname=self.app_name() + ".app") self.set_github_output_path('viewer_app', tarpath) + # Generate Velopack update packages if enabled + # This creates the nupkg and RELEASES files needed for auto-updates + # Distribution is still via DMG, but updates use Velopack + if self.args.get('velopack', 'OFF') == 'ON': + self.velopack_package_finish() + + def velopack_package_finish(self): + """Generate Velopack update packages for macOS""" + # packId determines install identification + pack_id = self.app_name_oneword() # "SecondLife", "SecondLifeBeta", etc. + # Velopack requires SemVer2 (3-part: major.minor.patch), viewer has 4 parts + pack_version = '.'.join(self.args['version'][:3]) + pack_title = self.app_name() # Display name with spaces + # The .app bundle path + pack_dir = self.get_dst_prefix() + + # Icon path for macOS + icon_path = os.path.join(self.get_src_prefix(), 'res', 'secondlife.icns') + + # Build vpk command for macOS + vpk_args = [ + 'vpk', 'pack', + '--packId', pack_id, + '--packVersion', pack_version, + '--packDir', pack_dir, + '--packTitle', pack_title, + '--mainExe', self.channel(), # The executable inside the .app bundle + ] + + # Add icon if exists + if os.path.exists(icon_path): + vpk_args.extend(['--icon', icon_path]) + + print("Running Velopack packaging for macOS: %s" % ' '.join(vpk_args)) + + # Run vpk command + import subprocess + result = subprocess.run(vpk_args, cwd=os.path.dirname(pack_dir), capture_output=True, text=True) + if result.returncode != 0: + print("vpk stdout: %s" % result.stdout) + print("vpk stderr: %s" % result.stderr) + # Don't fail the build - Velopack packaging is supplementary to DMG + print("Warning: Velopack packaging failed with code %d" % result.returncode) + return + + # Velopack outputs to a Releases directory + releases_dir = os.path.join(os.path.dirname(pack_dir), 'Releases') + + # Output the Releases directory path for artifact upload (contains nupkg, RELEASES for updates) + if os.path.exists(releases_dir): + self.set_github_output('velopack_releases', releases_dir) + print("Velopack releases directory: %s" % releases_dir) + else: + print("Warning: Velopack releases directory not found: %s" % releases_dir) + class LinuxManifest(ViewerManifest): build_data_json_platform = 'lnx' From a9d125fa16e8887e5fa2ab6be76d6c1ab51a4c29 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Wed, 21 Jan 2026 14:27:14 -0500 Subject: [PATCH 07/17] Update Velopack version and dependencies --- autobuild.xml | 10 +++++----- indra/cmake/Velopack.cmake | 7 ++++++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/autobuild.xml b/autobuild.xml index 0586bf91c33..c71d93d6d8a 100644 --- a/autobuild.xml +++ b/autobuild.xml @@ -2925,11 +2925,11 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors creds github hash - 77d82909367f116c60edd073f584d0c53e6ea591 + 81a97ec4fc491011726097e6dc8359f537fa7935 hash_algorithm sha1 url - https://api.github.com/repos/secondlife-3p/3p-velopack/releases/assets/330718186 + https://api.github.com/repos/secondlife-3p/3p-velopack/releases/assets/343914203 name windows64 @@ -2941,11 +2941,11 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors creds github hash - 3cabca6840973bbb3a90bf0cc3390bc86e745379 + 17b1373f15b33cd86ca30743a383bf4de63d5c09 hash_algorithm sha1 url - https://api.github.com/repos/secondlife-3p/3p-velopack/releases/assets/330718181 + https://api.github.com/repos/secondlife-3p/3p-velopack/releases/assets/343914198 name darwin64 @@ -2958,7 +2958,7 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors copyright Velopack Ltd. version - a2592d1.20376699173 + a2592d1.21222737470 name velopack description diff --git a/indra/cmake/Velopack.cmake b/indra/cmake/Velopack.cmake index fa37553e22f..a1dbe2cbe92 100644 --- a/indra/cmake/Velopack.cmake +++ b/indra/cmake/Velopack.cmake @@ -50,12 +50,17 @@ elseif (DARWIN) ${ARCH_PREBUILT_DIRS_RELEASE}/libvelopack_libc.a ) - # macOS system frameworks required by Velopack + # 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) From b2d42851a8032df134ddbcc0e1e6fed186fc2fc4 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Wed, 21 Jan 2026 15:50:19 -0500 Subject: [PATCH 08/17] Update viewer_manifest.py --- indra/newview/viewer_manifest.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py index f1ff5b210e1..a7a21d970cd 100755 --- a/indra/newview/viewer_manifest.py +++ b/indra/newview/viewer_manifest.py @@ -1211,13 +1211,20 @@ def package_finish(self): def velopack_package_finish(self): """Generate Velopack update packages for macOS""" - # packId determines install identification + # packId determines install identification - same as Windows for consistency pack_id = self.app_name_oneword() # "SecondLife", "SecondLifeBeta", etc. # Velopack requires SemVer2 (3-part: major.minor.patch), viewer has 4 parts pack_version = '.'.join(self.args['version'][:3]) pack_title = self.app_name() # Display name with spaces - # The .app bundle path - pack_dir = self.get_dst_prefix() + + # The .app bundle path (e.g., "Second Life Release.app") + app_bundle = self.get_dst_prefix() + # Parent directory containing the .app bundle + pack_dir = os.path.dirname(app_bundle) + # The executable inside Contents/MacOS/ has the same name as the channel + main_exe = self.channel() + # Bundle ID from args (e.g., "com.secondlife.viewer") + bundle_id = self.args.get('bundleid', 'com.secondlife.indra.viewer') # Icon path for macOS icon_path = os.path.join(self.get_src_prefix(), 'res', 'secondlife.icns') @@ -1227,9 +1234,10 @@ def velopack_package_finish(self): 'vpk', 'pack', '--packId', pack_id, '--packVersion', pack_version, - '--packDir', pack_dir, + '--packDir', app_bundle, '--packTitle', pack_title, - '--mainExe', self.channel(), # The executable inside the .app bundle + '--mainExe', main_exe, + '--bundleId', bundle_id, ] # Add icon if exists @@ -1240,23 +1248,21 @@ def velopack_package_finish(self): # Run vpk command import subprocess - result = subprocess.run(vpk_args, cwd=os.path.dirname(pack_dir), capture_output=True, text=True) + result = subprocess.run(vpk_args, cwd=pack_dir, capture_output=True, text=True) if result.returncode != 0: print("vpk stdout: %s" % result.stdout) print("vpk stderr: %s" % result.stderr) - # Don't fail the build - Velopack packaging is supplementary to DMG - print("Warning: Velopack packaging failed with code %d" % result.returncode) - return + raise ManifestError("Velopack packaging failed with code %d" % result.returncode) # Velopack outputs to a Releases directory - releases_dir = os.path.join(os.path.dirname(pack_dir), 'Releases') + releases_dir = os.path.join(pack_dir, 'Releases') - # Output the Releases directory path for artifact upload (contains nupkg, RELEASES for updates) + # Output the Releases directory path for artifact upload (contains nupkg, releases.json for updates) if os.path.exists(releases_dir): self.set_github_output('velopack_releases', releases_dir) print("Velopack releases directory: %s" % releases_dir) else: - print("Warning: Velopack releases directory not found: %s" % releases_dir) + raise ManifestError("Velopack releases directory not found: %s" % releases_dir) class LinuxManifest(ViewerManifest): From 158fc844cf52ae4d628c11f52cae7666ce1a3f92 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Thu, 22 Jan 2026 15:31:43 -0500 Subject: [PATCH 09/17] Improve Velopack packaging for macOS --- indra/newview/CMakeLists.txt | 2 + indra/newview/viewer_manifest.py | 73 +++++++++++++++++++++++--------- 2 files changed, 54 insertions(+), 21 deletions(-) diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 242bef6e3c8..ed3fbed5948 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -2276,9 +2276,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/viewer_manifest.py b/indra/newview/viewer_manifest.py index a7a21d970cd..6be73e063aa 100755 --- a/indra/newview/viewer_manifest.py +++ b/indra/newview/viewer_manifest.py @@ -1210,60 +1210,91 @@ def package_finish(self): self.velopack_package_finish() def velopack_package_finish(self): - """Generate Velopack update packages for macOS""" + """Generate Velopack update packages for macOS. + + This creates the nupkg and releases.json files needed for auto-updates. + Distribution is still via DMG - Velopack only handles the update infrastructure. + """ # packId determines install identification - same as Windows for consistency pack_id = self.app_name_oneword() # "SecondLife", "SecondLifeBeta", etc. # Velopack requires SemVer2 (3-part: major.minor.patch), viewer has 4 parts pack_version = '.'.join(self.args['version'][:3]) pack_title = self.app_name() # Display name with spaces - # The .app bundle path (e.g., "Second Life Release.app") + # The .app bundle path (e.g., "/path/to/Second Life Release.app") app_bundle = self.get_dst_prefix() - # Parent directory containing the .app bundle - pack_dir = os.path.dirname(app_bundle) - # The executable inside Contents/MacOS/ has the same name as the channel - main_exe = self.channel() + # Parent directory containing the .app bundle - this is where we run vpk from + # and where the Releases directory will be created + work_dir = os.path.dirname(app_bundle) # Bundle ID from args (e.g., "com.secondlife.viewer") bundle_id = self.args.get('bundleid', 'com.secondlife.indra.viewer') + # Output directory for releases + releases_dir = os.path.join(work_dir, 'Releases') + # Icon path for macOS - icon_path = os.path.join(self.get_src_prefix(), 'res', 'secondlife.icns') + icon_path = os.path.join(self.get_src_prefix(), self.icon_path(), 'secondlife.icns') # Build vpk command for macOS + # See: https://docs.velopack.io/reference/cli/content/vpk-osx vpk_args = [ 'vpk', 'pack', '--packId', pack_id, '--packVersion', pack_version, '--packDir', app_bundle, '--packTitle', pack_title, - '--mainExe', main_exe, '--bundleId', bundle_id, + '--outputDir', releases_dir, + '--noInst', # Don't generate .pkg installer - we use DMG for distribution + '--noPortable', # Don't generate portable zip + '--verbose', # Show detailed output ] # Add icon if exists if os.path.exists(icon_path): vpk_args.extend(['--icon', icon_path]) - print("Running Velopack packaging for macOS: %s" % ' '.join(vpk_args)) + print("Running Velopack packaging for macOS:") + print(" Command: %s" % ' '.join(vpk_args)) + print(" Working directory: %s" % work_dir) + print(" App bundle: %s" % app_bundle) # Run vpk command - import subprocess - result = subprocess.run(vpk_args, cwd=pack_dir, capture_output=True, text=True) + result = subprocess.run(vpk_args, cwd=work_dir, capture_output=True, text=True) + + # Always print output for debugging + if result.stdout: + print("vpk stdout:\n%s" % result.stdout) + if result.stderr: + print("vpk stderr:\n%s" % result.stderr) + if result.returncode != 0: - print("vpk stdout: %s" % result.stdout) - print("vpk stderr: %s" % result.stderr) raise ManifestError("Velopack packaging failed with code %d" % result.returncode) - # Velopack outputs to a Releases directory - releases_dir = os.path.join(pack_dir, 'Releases') - - # Output the Releases directory path for artifact upload (contains nupkg, releases.json for updates) - if os.path.exists(releases_dir): - self.set_github_output('velopack_releases', releases_dir) - print("Velopack releases directory: %s" % releases_dir) - else: + # Verify the Releases directory was created and contains expected files + if not os.path.exists(releases_dir): raise ManifestError("Velopack releases directory not found: %s" % releases_dir) + # List what was created + releases_contents = os.listdir(releases_dir) + print("Velopack releases directory contents: %s" % releases_contents) + + # Verify we have the expected files (nupkg and releases JSON) + nupkg_files = [f for f in releases_contents if f.endswith('.nupkg')] + json_files = [f for f in releases_contents if f.endswith('.json')] + + if not nupkg_files: + raise ManifestError("No .nupkg files found in releases directory") + if not json_files: + raise ManifestError("No releases JSON files found in releases directory") + + print("Generated %d nupkg file(s): %s" % (len(nupkg_files), nupkg_files)) + print("Generated %d JSON file(s): %s" % (len(json_files), json_files)) + + # Output the Releases directory path for artifact upload + self.set_github_output('velopack_releases', releases_dir) + print("Velopack releases directory: %s" % releases_dir) + class LinuxManifest(ViewerManifest): build_data_json_platform = 'lnx' From e6d2890408a0a378eea33fdbefd31bc7c827adf3 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Thu, 22 Jan 2026 16:27:01 -0500 Subject: [PATCH 10/17] Update build.yaml --- .github/workflows/build.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index b274499ce55..ef867d99109 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -136,6 +136,12 @@ 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 From 2906ad7083f2046546dd2c0a82a2ce482ccaa7c3 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Thu, 22 Jan 2026 17:17:27 -0500 Subject: [PATCH 11/17] Update viewer_manifest.py --- indra/newview/viewer_manifest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py index 6be73e063aa..10ee422951d 100755 --- a/indra/newview/viewer_manifest.py +++ b/indra/newview/viewer_manifest.py @@ -1246,7 +1246,6 @@ def velopack_package_finish(self): '--bundleId', bundle_id, '--outputDir', releases_dir, '--noInst', # Don't generate .pkg installer - we use DMG for distribution - '--noPortable', # Don't generate portable zip '--verbose', # Show detailed output ] From 8fccef6ab1d90e1679596690f8626ec6bda5637b Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Thu, 22 Jan 2026 19:07:39 -0500 Subject: [PATCH 12/17] Update viewer_manifest.py --- indra/newview/viewer_manifest.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py index 10ee422951d..3a5cfae48cd 100755 --- a/indra/newview/viewer_manifest.py +++ b/indra/newview/viewer_manifest.py @@ -1235,6 +1235,9 @@ def velopack_package_finish(self): # Icon path for macOS icon_path = os.path.join(self.get_src_prefix(), self.icon_path(), 'secondlife.icns') + # The main executable inside Contents/MacOS/ is named after the channel + main_exe = self.channel() + # Build vpk command for macOS # See: https://docs.velopack.io/reference/cli/content/vpk-osx vpk_args = [ @@ -1243,6 +1246,7 @@ def velopack_package_finish(self): '--packVersion', pack_version, '--packDir', app_bundle, '--packTitle', pack_title, + '--mainExe', main_exe, # Executable name inside Contents/MacOS/ '--bundleId', bundle_id, '--outputDir', releases_dir, '--noInst', # Don't generate .pkg installer - we use DMG for distribution @@ -1257,6 +1261,7 @@ def velopack_package_finish(self): print(" Command: %s" % ' '.join(vpk_args)) print(" Working directory: %s" % work_dir) print(" App bundle: %s" % app_bundle) + print(" Main executable: %s" % main_exe) # Run vpk command result = subprocess.run(vpk_args, cwd=work_dir, capture_output=True, text=True) From 68be5c7acd28f0ec39fabee38dcf46eedc7b71f9 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Thu, 22 Jan 2026 20:12:28 -0500 Subject: [PATCH 13/17] Update viewer_manifest.py --- indra/newview/viewer_manifest.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py index 3a5cfae48cd..70a59058a65 100755 --- a/indra/newview/viewer_manifest.py +++ b/indra/newview/viewer_manifest.py @@ -1229,8 +1229,11 @@ def velopack_package_finish(self): # Bundle ID from args (e.g., "com.secondlife.viewer") bundle_id = self.args.get('bundleid', 'com.secondlife.indra.viewer') - # Output directory for releases + # Output directory for releases - clean it first to avoid version conflicts releases_dir = os.path.join(work_dir, 'Releases') + if os.path.exists(releases_dir): + print("Cleaning existing Releases directory: %s" % releases_dir) + shutil.rmtree(releases_dir) # Icon path for macOS icon_path = os.path.join(self.get_src_prefix(), self.icon_path(), 'secondlife.icns') From 3acb53165cffd08482f12ce67d136e064a8f7eeb Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Fri, 23 Jan 2026 11:39:46 -0500 Subject: [PATCH 14/17] Update viewer_manifest.py --- indra/newview/viewer_manifest.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py index 70a59058a65..b108ecc6f17 100755 --- a/indra/newview/viewer_manifest.py +++ b/indra/newview/viewer_manifest.py @@ -829,8 +829,9 @@ def velopack_package_finish(self): # Use hyphen format (-Setup.exe) to avoid the *_Setup.exe exclusion pattern in viewer_app # Velopack creates: {packId}-win-Setup.exe velopack_setup = os.path.join(releases_dir, '%s-win-Setup.exe' % pack_id) - # Keep Velopack naming convention (hyphen, not underscore) - self.package_file = '%s-Setup.exe' % pack_id + # Use same naming convention as NSIS installer for consistency + # Format: Second_Life_26_1_0_53294_x86_64_Setup.exe + self.package_file = self.installer_base_name() + '_Setup.exe' our_setup = os.path.join(pack_dir, self.package_file) if os.path.exists(velopack_setup): shutil.move(velopack_setup, our_setup) From 85298ebb341c6038dba92bc21311566ff5e86708 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Fri, 23 Jan 2026 12:52:12 -0500 Subject: [PATCH 15/17] Update viewer_manifest.py --- indra/newview/viewer_manifest.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py index b108ecc6f17..648860553c9 100755 --- a/indra/newview/viewer_manifest.py +++ b/indra/newview/viewer_manifest.py @@ -826,12 +826,13 @@ def velopack_package_finish(self): releases_dir = os.path.join(os.path.dirname(pack_dir), 'Releases') # Move the setup exe INTO pack_dir so it's included in the Windows-app artifact - # Use hyphen format (-Setup.exe) to avoid the *_Setup.exe exclusion pattern in viewer_app + # IMPORTANT: Use hyphen format (-Setup.exe) to avoid the *_Setup.exe exclusion pattern + # in viewer_app output (line ~538). The underscore pattern excludes NSIS installers + # which are rebuilt during signing, but Velopack installers are created here. # Velopack creates: {packId}-win-Setup.exe velopack_setup = os.path.join(releases_dir, '%s-win-Setup.exe' % pack_id) - # Use same naming convention as NSIS installer for consistency - # Format: Second_Life_26_1_0_53294_x86_64_Setup.exe - self.package_file = self.installer_base_name() + '_Setup.exe' + # Use versioned name with hyphen: Second_Life_26_1_0_53294_x86_64-Setup.exe + self.package_file = self.installer_base_name() + '-Setup.exe' our_setup = os.path.join(pack_dir, self.package_file) if os.path.exists(velopack_setup): shutil.move(velopack_setup, our_setup) From a6b978b3e00f2e7e186b97d8a4997374adecdafc Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Tue, 10 Feb 2026 22:29:52 +0200 Subject: [PATCH 16/17] #5346 Uninstall older non-velopack viewer (#5363) * #5335 Fix silent uninstall asking about registry * #5346 Uninstall older non-velopack viewer if of the same channel --- indra/cmake/CMakeLists.txt | 1 + indra/newview/app_settings/settings.xml | 12 ++- .../installers/windows/installer_template.nsi | 33 ++++++- indra/newview/llstartup.cpp | 76 +++++++++++++++ indra/newview/llvelopack.cpp | 97 +++++++++++++++++++ indra/newview/llvelopack.h | 4 + .../skins/default/xui/en/notifications.xml | 14 +++ 7 files changed, 233 insertions(+), 4 deletions(-) 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/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index fe31a00ba33..2f2d9f3ece0 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/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 index 32bad18f380..14be2b0cae0 100644 --- a/indra/newview/llvelopack.cpp +++ b/indra/newview/llvelopack.cpp @@ -28,6 +28,7 @@ #include "llviewerprecompiledheaders.h" #include "llvelopack.h" +#include "llstring.h" #include "Velopack.h" @@ -37,6 +38,7 @@ #include #include #include +#include #pragma comment(lib, "shlwapi.lib") #pragma comment(lib, "ole32.lib") @@ -175,6 +177,89 @@ static void register_protocol_handler(const std::wstring& protocol, } } +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; @@ -206,6 +291,18 @@ static void register_uninstall_info(const std::wstring& install_dir, 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)); diff --git a/indra/newview/llvelopack.h b/indra/newview/llvelopack.h index 430ea9e5183..93d1ca4776d 100644 --- a/indra/newview/llvelopack.h +++ b/indra/newview/llvelopack.h @@ -40,6 +40,10 @@ 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/skins/default/xui/en/notifications.xml b/indra/newview/skins/default/xui/en/notifications.xml index d0261a930c1..fe77fbb5c5b 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 + + + Date: Tue, 24 Feb 2026 11:45:41 -0500 Subject: [PATCH 17/17] Use runtime viewer exe name, handle Velopack URL --- indra/newview/llvelopack.cpp | 11 +++++++---- indra/newview/llvvmquery.cpp | 14 ++++++++++---- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/indra/newview/llvelopack.cpp b/indra/newview/llvelopack.cpp index 14be2b0cae0..694b75960b9 100644 --- a/indra/newview/llvelopack.cpp +++ b/indra/newview/llvelopack.cpp @@ -59,7 +59,10 @@ static vpkc_update_info_t* sPendingUpdate = nullptr; static const wchar_t* PROTOCOL_SECONDLIFE = L"secondlife"; static const wchar_t* PROTOCOL_GRID_INFO = L"x-grid-location-info"; -static const wchar_t* VIEWER_EXE_NAME = L"SecondLifeViewer.exe"; +static std::wstring get_viewer_exe_name() +{ + return ll_convert(gDirUtilp->getExecutableFilename()); +} static std::wstring get_install_dir() { @@ -277,7 +280,7 @@ static void register_uninstall_info(const std::wstring& install_dir, 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"\\" + VIEWER_EXE_NAME; + 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, @@ -323,7 +326,7 @@ static void unregister_uninstall_info() static void create_shortcuts(const std::wstring& install_dir, const std::wstring& app_name) { - std::wstring exe_path = install_dir + L"\\" + VIEWER_EXE_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(); @@ -350,7 +353,7 @@ 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"\\" + VIEWER_EXE_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); diff --git a/indra/newview/llvvmquery.cpp b/indra/newview/llvvmquery.cpp index 49fb0bfbf67..b1f964b1e45 100644 --- a/indra/newview/llvvmquery.cpp +++ b/indra/newview/llvvmquery.cpp @@ -128,13 +128,19 @@ namespace if (platforms.has(platform)) { std::string update_url = platforms[platform]["url"].asString(); - if (!update_url.empty()) - { - LL_INFOS("VVM") << "Update available at: " << update_url << LL_ENDL; #if LL_VELOPACK - velopack_set_update_url(update_url); + 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