Merge branch 'project/one-click' of https://github.com/secondlife/viewer

# Conflicts:
#	indra/cmake/Velopack.cmake
#	indra/newview/CMakeLists.txt
#	indra/newview/app_settings/settings.xml
#	indra/newview/installers/windows/installer_template.nsi
#	indra/newview/llappviewer.cpp
master
Ansariel 2026-02-17 17:14:21 +01:00
commit f95833ee6a
17 changed files with 743 additions and 120 deletions

View File

@ -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@v6
@ -136,8 +136,14 @@ 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' && (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 +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
@ -488,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: |

View File

@ -3190,11 +3190,11 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors</string>
<key>creds</key>
<string>github</string>
<key>hash</key>
<string>77d82909367f116c60edd073f584d0c53e6ea591</string>
<string>81a97ec4fc491011726097e6dc8359f537fa7935</string>
<key>hash_algorithm</key>
<string>sha1</string>
<key>url</key>
<string>https://api.github.com/repos/secondlife-3p/3p-velopack/releases/assets/330718186</string>
<string>https://api.github.com/repos/secondlife-3p/3p-velopack/releases/assets/343914203</string>
</map>
<key>name</key>
<string>windows64</string>
@ -3206,11 +3206,11 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors</string>
<key>creds</key>
<string>github</string>
<key>hash</key>
<string>3cabca6840973bbb3a90bf0cc3390bc86e745379</string>
<string>17b1373f15b33cd86ca30743a383bf4de63d5c09</string>
<key>hash_algorithm</key>
<string>sha1</string>
<key>url</key>
<string>https://api.github.com/repos/secondlife-3p/3p-velopack/releases/assets/330718181</string>
<string>https://api.github.com/repos/secondlife-3p/3p-velopack/releases/assets/343914198</string>
</map>
<key>name</key>
<string>darwin64</string>
@ -3223,7 +3223,7 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors</string>
<key>copyright</key>
<string>Velopack Ltd.</string>
<key>version</key>
<string>a2592d1.20376699173</string>
<string>a2592d1.21222737470</string>
<key>name</key>
<string>velopack</string>
<key>description</key>

View File

@ -64,6 +64,7 @@ set(cmake_SOURCE_FILES
UI.cmake
UnixInstall.cmake
Variables.cmake
Velopack.cmake
VHACD.cmake
ViewerMiscLibs.cmake
VisualLeakDetector.cmake

View File

@ -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 (USE_VELOPACK)
if (WINDOWS)
include(Prebuilt)
@ -40,4 +37,34 @@ if (USE_VELOPACK)
target_compile_definitions(ll::velopack INTERFACE LL_VELOPACK=1)
endif()
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()

View File

@ -787,6 +787,7 @@ set(viewer_SOURCE_FILES
llurllineeditorctrl.cpp
llurlwhitelist.cpp
llversioninfo.cpp
llvvmquery.cpp
llviewchildren.cpp
llviewerassetstats.cpp
llviewerassetstorage.cpp
@ -1639,6 +1640,7 @@ set(viewer_HEADER_FILES
llurllineeditorctrl.h
llurlwhitelist.h
llversioninfo.h
llvvmquery.h
llviewchildren.h
llviewerassetstats.h
llviewerassetstorage.h
@ -1845,6 +1847,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
@ -2878,6 +2882,7 @@ if (DARWIN)
--arch=${ARCH}
--artwork=${ARTWORK_DIR}
"--bugsplat=${BUGSPLAT_DB}"
--bundleid=${MACOSX_BUNDLE_GUI_IDENTIFIER}
"--discord=${USE_DISCORD}"
"--fmodstudio=${USE_FMODSTUDIO}"
"--openal=${USE_OPENAL}"

View File

@ -6899,7 +6899,17 @@
<key>Backup</key>
<integer>0</integer>
</map>
<key>LastInstallVersion</key>
<map>
<key>Comment</key>
<string>Version number of last instance of the viewer that you installed</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>String</string>
<key>Value</key>
<string></string>
</map>
<key>LastSnapshotToProfileHeight</key>
<map>
<key>Comment</key>

View File

@ -197,7 +197,7 @@ void LLGridManager::initGrids()
}
// <FS:ND> FIRE-20112 in case grid was set bt settings/cmd line, set it now
if (mStartupGrid.size())
if (!mStartupGrid.empty())
setGridChoice(mStartupGrid);
mStartupGrid = "";
// </FS:ND>
@ -1340,7 +1340,7 @@ std::string LLGridManager::getSLURLBase(const std::string& grid)
// deal with hand edited entries
std::string grid_norm = grid;
if (grid_norm.size())
if (!grid_norm.empty())
{
size_t pos = grid_norm.find_last_of("/");
@ -1356,6 +1356,30 @@ std::string LLGridManager::getSLURLBase(const std::string& grid)
return ret;
}
std::string LLGridManager::getUpdateServiceURL()
{
std::string update_url_base = gSavedSettings.getString("CmdLineUpdateService");;
if (!update_url_base.empty())
{
LL_INFOS("UpdaterService", "GridManager")
<< "Update URL base overridden from command line: " << update_url_base
<< LL_ENDL;
}
else if (mGridList[mGrid].has(GRID_UPDATE_SERVICE_URL))
{
update_url_base = mGridList[mGrid][GRID_UPDATE_SERVICE_URL].asString();
}
else
{
LL_WARNS("UpdaterService", "GridManager")
<< "The grid property '" << GRID_UPDATE_SERVICE_URL
<< "' is not defined for the grid '" << mGrid << "'"
<< LL_ENDL;
}
return update_url_base;
}
// get app slurl base for the given region
// within the selected grid (LL comment was misleading)
std::string LLGridManager::getAppSLURLBase(const std::string& grid)

View File

@ -242,6 +242,8 @@ public:
void setDirectoryFee(const S32 directory_fee) { mDirectoryFee = directory_fee; }
S32 getDirectoryFee() const { return mDirectoryFee; }
std::string getUpdateServiceURL();
private:
friend class GridInfoRequestResponder;

View File

@ -976,9 +976,22 @@ 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
Push $1
@ -1085,14 +1098,28 @@ RMDir "$INSTDIR"
IfFileExists "$INSTDIR" FOLDERFOUND NOFOLDER
FOLDERFOUND:
# Silent uninstall always removes all files (/SD IDYES)
MessageBox MB_YESNO $(DeleteProgramFilesMB) /SD IDYES IDNO NOFOLDER
RMDir /r "$INSTDIR"
# Silent uninstall always removes all files (/SD IDYES)
ifSilent 2 0
MessageBox MB_YESNO $(DeleteProgramFilesMB) /SD IDYES IDNO NOFOLDER
RMDir /r "$INSTDIR"
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-info" # <FS:Ansariel> FIRE-30446: Register x-grid-info hypergrid protocol
DeleteRegKey SHELL_CONTEXT "SOFTWARE\Classes\x-grid-location-info"

View File

@ -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"
@ -1313,103 +1318,53 @@ bool LLAppViewer::init()
gGLActive = false;
// <FS:Ansariel> Disable updater
//#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
//
// {
// // Iterate over --leap command-line options. But this is a bit tricky: if
// // there's only one, it won't be an array at all.
// LLSD LeapCommand(gSavedSettings.getLLSD("LeapCommand"));
// LL_DEBUGS("InitInfo") << "LeapCommand: " << LeapCommand << LL_ENDL;
// if (LeapCommand.isDefined() && !LeapCommand.isArray())
// {
// // If LeapCommand is actually a scalar value, make an array of it.
// // Have to do it in two steps because LeapCommand.append(LeapCommand)
// // trashes content! :-P
// LLSD item(LeapCommand);
// LeapCommand.append(item);
// }
// for (const auto& leap : llsd::inArray(LeapCommand))
// {
// LL_INFOS("InitInfo") << "processing --leap \"" << leap << '"' << LL_ENDL;
// // We don't have any better description of this plugin than the
// // user-specified command line. Passing "" causes LLLeap to derive a
// // description from the command line itself.
// // Suppress LLLeap::Error exception: trust LLLeap's own logging. We
// // don't consider any one --leap command mission-critical, so if one
// // fails, log it, shrug and carry on.
// LLLeap::create("", leap, false); // exception=false
// }
// }
//
// if (gSavedSettings.getBOOL("QAMode") && gSavedSettings.getS32("QAModeEventHostPort") > 0)
// {
// LL_WARNS("InitInfo") << "QAModeEventHostPort DEPRECATED: "
// << "lleventhost no longer supported as a dynamic library"
// << LL_ENDL;
// }
////#endif //LL_RELEASE_FOR_DOWNLOAD
// </FS:Ansariel>
{
// Iterate over --leap command-line options. But this is a bit tricky: if
// there's only one, it won't be an array at all.
LLSD LeapCommand(gSavedSettings.getLLSD("LeapCommand"));
LL_DEBUGS("InitInfo") << "LeapCommand: " << LeapCommand << LL_ENDL;
if (LeapCommand.isDefined() && !LeapCommand.isArray())
{
// If LeapCommand is actually a scalar value, make an array of it.
// Have to do it in two steps because LeapCommand.append(LeapCommand)
// trashes content! :-P
LLSD item(LeapCommand);
LeapCommand.append(item);
}
for (const auto& leap : llsd::inArray(LeapCommand))
{
LL_INFOS("InitInfo") << "processing --leap \"" << leap << '"' << LL_ENDL;
// We don't have any better description of this plugin than the
// user-specified command line. Passing "" causes LLLeap to derive a
// description from the command line itself.
// Suppress LLLeap::Error exception: trust LLLeap's own logging. We
// don't consider any one --leap command mission-critical, so if one
// fails, log it, shrug and carry on.
LLLeap::create("", leap, false); // exception=false
}
}
if (gSavedSettings.getBOOL("QAMode") && gSavedSettings.getS32("QAModeEventHostPort") > 0)
{
LL_WARNS("InitInfo") << "QAModeEventHostPort DEPRECATED: "
<< "lleventhost no longer supported as a dynamic library"
<< LL_ENDL;
}
LLTextUtil::TextHelpers::iconCallbackCreationFunction = create_text_segment_icon_from_url_match;
//EXT-7013 - On windows for some locale (Japanese) standard
@ -1998,6 +1953,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;

View File

@ -29,6 +29,11 @@
#include "llappviewer.h"
#include "llstartup.h"
#if LL_VELOPACK && LL_WINDOWS
#include "llvelopack.h"
#include <shellapi.h>
#endif
#if LL_WINDOWS
# include <process.h> // _spawnl()
#else
@ -314,6 +319,7 @@ std::unique_ptr<LLViewerStats::PhaseMap> 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(); // <FS:CR> Unused in Firestorm
bool first_run_dialog_callback(const LLSD& notification, const LLSD& response);
@ -1392,6 +1398,7 @@ bool idle_startup()
LL_DEBUGS("AppInit") << "PeekMessage processed" << LL_ENDL;
#endif
do_startup_frame();
uninstall_nsis_if_required();
timeout.reset();
return false;
}
@ -3546,6 +3553,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: "<uninstaller_path>" /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());

View File

@ -28,28 +28,39 @@
#include "llviewerprecompiledheaders.h"
#include "llvelopack.h"
#include "llstring.h"
#include "Velopack.h"
#if LL_WINDOWS
#include <windows.h>
#include <shlobj.h>
#include <shobjidl.h>
#include <shlwapi.h>
#include <objbase.h>
#include "Velopack.h"
#include <filesystem>
#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<void(int)> 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];
@ -166,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;
@ -197,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));
@ -275,6 +381,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 +418,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 +439,7 @@ void velopack_check_for_updates()
{
if (sUpdateUrl.empty())
{
LL_DEBUGS("Velopack") << "No update URL set, skipping update check" << LL_ENDL;
return;
}
@ -307,6 +451,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 +461,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 +469,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 +508,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 +523,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<void(int)> callback)

View File

@ -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<void(int)> 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

View File

@ -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<char*>(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<LLCoreHttpUtil::HttpCoroutineAdapter>("VVMQuery", httpPolicy);
LLCore::HttpRequest::ptr_t request = std::make_shared<LLCore::HttpRequest>();
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);
}

View File

@ -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

View File

@ -200,6 +200,20 @@ No tutorial is currently available.
yestext="OK"/>
</notification>
<notification
icon="alertmodal.tga"
name="PromptRemoveNsisInstallation"
type="alertmodal">
[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.
<tag>confirm</tag>
<usetemplate
name="okcancelbuttons"
notext="Cancel"
yestext="Uninstall"/>
</notification>
<notification
icon="alertmodal.tga"
name="LoginFailedNoNetwork"

View File

@ -1036,11 +1036,13 @@ class Windows_x86_64_Manifest(ViewerManifest):
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)
# Keep Velopack naming convention (hyphen, not underscore)
self.package_file = '%s-Setup.exe' % pack_id
# 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)
@ -1949,6 +1951,105 @@ class Darwin_x86_64_Manifest(ViewerManifest):
self.remove(sparsename)
self.fs_save_osx_symbols()
# 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.
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., "/path/to/Second Life Release.app")
app_bundle = self.get_dst_prefix()
# 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 - 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')
# 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 = [
'vpk', 'pack',
'--packId', pack_id,
'--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
'--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:")
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)
# 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:
raise ManifestError("Velopack packaging failed with code %d" % result.returncode)
# 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'