diff --git a/.hgtags b/.hgtags
index 105abb4fa5e17dc6d93d3c22a6ed54b910b1ad9a..b117cb3016f65a56416acc85a690ad6c62bfde54 100755
--- a/.hgtags
+++ b/.hgtags
@@ -541,5 +541,6 @@ ad0e15543836d64d6399d28b32852510435e344a 5.1.0-release
 ac3b1332ad4f55b7182a8cbcc1254535a0069f75 5.1.7-release
 23ea0fe36fadf009a60c080392ce80e4bf8af8d9 5.1.8-release
 52422540bfe54b71155aa455360bee6e3ef1fd96 5.1.9-release
+1cfa567caf5088ae299271be08cc2d9f0801ff6a pre-Poseidon
 821edfcd14919c0e95c590866171c61fb57e8623 6.0.0-release
 21b7604680ef6b6ea67f8bebaaa588d6e263bdc1 6.0.1-release
diff --git a/BuildParams b/BuildParams
index cb908f1532b850cc22bac928a3e7e13ffeec8009..c5f96d5ee32e3116da93061f55b9e2cbbdccb5bc 100755
--- a/BuildParams
+++ b/BuildParams
@@ -62,11 +62,6 @@ Linux.additional_packages = ""
 EDU_sourceid = ""
 EDU_viewer_channel_suffix = "edu"
 
-# The EDU package allows us to create a separate release channel whose expirations
-# are synchronized as much as possible with the academic year
-EDU_sourceid = ""
-EDU_viewer_channel_suffix = "edu"
-
 # Notifications - to configure email notices use the TeamCity parameter
 # setting screen for your project or build configuration to set the
 # environment variable 'email' to a space-separated list of email addresses
diff --git a/autobuild.xml b/autobuild.xml
index 513728623ba744cfdb0ac06db5a4eeb4775ab82b..9b7ff39334746dc61cfb4f6ea043b025e7db6b0b 100644
--- a/autobuild.xml
+++ b/autobuild.xml
@@ -225,18 +225,18 @@
         <key>version</key>
         <string>1.57</string>
       </map>
-      <key>chardet</key>
+      <key>bugsplat</key>
       <map>
         <key>copyright</key>
-        <string>Contributors to charset (see https://github.com/chardet/chardet)</string>
+        <string>Copyright 2003-2017, BugSplat</string>
         <key>description</key>
-        <string>Python Character Encoding Library</string>
+        <string>Bugsplat crash reporting package</string>
         <key>license</key>
-        <string>LGPL</string>
+        <string>Proprietary</string>
         <key>license_file</key>
-        <string>LICENSES/chardet.txt</string>
+        <string>LICENSES/BUGSPLAT_LICENSE.txt</string>
         <key>name</key>
-        <string>chardet</string>
+        <string>bugsplat</string>
         <key>platforms</key>
         <map>
           <key>darwin64</key>
@@ -244,16 +244,40 @@
             <key>archive</key>
             <map>
               <key>hash</key>
-              <string>0124862b6a1b88455c78a68f8b823d21</string>
+              <string>c3b5e8c57bd1c92bc9e0956586908b99</string>
               <key>url</key>
-              <string>http://automated-builds-secondlife-com.s3.amazonaws.com/ct2/6662/23578/chardet-3.0.4-darwin64-506651.tar.bz2</string>
+              <string>http://s3-proxy.lindenlab.com/private-builds-secondlife-com/ct2/26330/207568/bugsplat-1.0.7.520791-darwin64-520791.tar.bz2</string>
             </map>
             <key>name</key>
             <string>darwin64</string>
           </map>
+          <key>windows</key>
+          <map>
+            <key>archive</key>
+            <map>
+              <key>hash</key>
+              <string>766dfde65a5b42ea5691d41df79c43e0</string>
+              <key>url</key>
+              <string>http://s3-proxy.lindenlab.com/private-builds-secondlife-com/ct2/26332/207582/bugsplat-3.6.0.4.520791-windows-520791.tar.bz2</string>
+            </map>
+            <key>name</key>
+            <string>windows</string>
+          </map>
+          <key>windows64</key>
+          <map>
+            <key>archive</key>
+            <map>
+              <key>hash</key>
+              <string>afd01285e22f27d473fac6f88fac9a3b</string>
+              <key>url</key>
+              <string>http://s3-proxy.lindenlab.com/private-builds-secondlife-com/ct2/26331/207576/bugsplat-3.6.0.4.520791-windows64-520791.tar.bz2</string>
+            </map>
+            <key>name</key>
+            <string>windows64</string>
+          </map>
         </map>
         <key>version</key>
-        <string>3.0.4</string>
+        <string>1.0.7.520791</string>
       </map>
       <key>colladadom</key>
       <map>
@@ -1465,36 +1489,6 @@
         <key>version</key>
         <string>2012.1-2</string>
       </map>
-      <key>idna</key>
-      <map>
-        <key>copyright</key>
-        <string>Copyright (c) 2013-2017, Kim Davies. All rights reserved.</string>
-        <key>description</key>
-        <string>Python Internationalized Domain Names in Applications (IDNA) Library</string>
-        <key>license</key>
-        <string>see idna.rst</string>
-        <key>license_file</key>
-        <string>LICENSES/idna.rst</string>
-        <key>name</key>
-        <string>idna</string>
-        <key>platforms</key>
-        <map>
-          <key>darwin64</key>
-          <map>
-            <key>archive</key>
-            <map>
-              <key>hash</key>
-              <string>7dfe9fc4023d7d4f511dd9fac7258266</string>
-              <key>url</key>
-              <string>http://automated-builds-secondlife-com.s3.amazonaws.com/ct2/6663/23584/idna-2.5-darwin64-506652.tar.bz2</string>
-            </map>
-            <key>name</key>
-            <string>darwin64</string>
-          </map>
-        </map>
-        <key>version</key>
-        <string>2.5</string>
-      </map>
       <key>jpeglib</key>
       <map>
         <key>copyright</key>
@@ -2173,46 +2167,6 @@
         <key>version</key>
         <string>0.0.1</string>
       </map>
-      <key>llbase</key>
-      <map>
-        <key>copyright</key>
-        <string>Copyright (c) 2010, Linden Research, Inc.</string>
-        <key>license</key>
-        <string>mit</string>
-        <key>license_file</key>
-        <string>LICENSES/llbase-license.txt</string>
-        <key>name</key>
-        <string>llbase</string>
-        <key>platforms</key>
-        <map>
-          <key>darwin64</key>
-          <map>
-            <key>archive</key>
-            <map>
-              <key>hash</key>
-              <string>e18eeb0691af053b83bd46b76c6ee86a</string>
-              <key>url</key>
-              <string>http://automated-builds-secondlife-com.s3.amazonaws.com/ct2/6299/21982/llbase-0.9.3.506286-darwin64-506286.tar.bz2</string>
-            </map>
-            <key>name</key>
-            <string>darwin64</string>
-          </map>
-          <key>windows</key>
-          <map>
-            <key>archive</key>
-            <map>
-              <key>hash</key>
-              <string>e6865670f9bca1c82fb8b91db3ea515c</string>
-              <key>url</key>
-              <string>http://automated-builds-secondlife-com.s3.amazonaws.com/ct2/6301/21994/llbase-0.9.3.506286-windows-506286.tar.bz2</string>
-            </map>
-            <key>name</key>
-            <string>windows</string>
-          </map>
-        </map>
-        <key>version</key>
-        <string>0.9.3.506286</string>
-      </map>
       <key>llca</key>
       <map>
         <key>copyright</key>
@@ -2955,52 +2909,6 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors</string>
         <key>version</key>
         <string>8.35.500898</string>
       </map>
-      <key>requests</key>
-      <map>
-        <key>copyright</key>
-        <string>Copyright 2016 Kenneth Reitz</string>
-        <key>description</key>
-        <string>Python HTTP Library</string>
-        <key>license</key>
-        <string>Apache</string>
-        <key>license_file</key>
-        <string>LICENSES/requests.txt</string>
-        <key>name</key>
-        <string>requests</string>
-        <key>platforms</key>
-        <map>
-          <key>darwin64</key>
-          <map>
-            <key>archive</key>
-            <map>
-              <key>hash</key>
-              <string>b8d134a970261b445a3f376ba4e05ff7</string>
-              <key>url</key>
-              <string>http://automated-builds-secondlife-com.s3.amazonaws.com/ct2/6693/23788/requests-2.18.1-darwin64-506681.tar.bz2</string>
-            </map>
-            <key>name</key>
-            <string>darwin64</string>
-          </map>
-          <key>linux64</key>
-          <map>
-            <key>archive</key>
-            <map>
-              <key>hash</key>
-              <string>a92f2235991871c3d601a73cfef9b2af</string>
-              <key>url</key>
-              <string>http://automated-builds-secondlife-com.s3.amazonaws.com/ct2/4105/11530/requests-1.0-linux64-504094.tar.bz2</string>
-            </map>
-            <key>name</key>
-            <string>linux64</string>
-          </map>
-        </map>
-        <key>source</key>
-        <string>https://bitbucket.org/lindenlab/p64_python-requests</string>
-        <key>source_type</key>
-        <string>hg</string>
-        <key>version</key>
-        <string>2.18.1</string>
-      </map>
       <key>slvoice</key>
       <map>
         <key>copyright</key>
@@ -3211,36 +3119,6 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors</string>
         <key>version</key>
         <string>0.8.0.1</string>
       </map>
-      <key>urllib3</key>
-      <map>
-        <key>copyright</key>
-        <string>Copyright 2008-2016 Andrey Petrov and contributors (see CONTRIBUTORS.txt)</string>
-        <key>description</key>
-        <string>Python HTTP Library</string>
-        <key>license</key>
-        <string>MIT</string>
-        <key>license_file</key>
-        <string>LICENSES/urllib3.txt</string>
-        <key>name</key>
-        <string>urllib3</string>
-        <key>platforms</key>
-        <map>
-          <key>darwin64</key>
-          <map>
-            <key>archive</key>
-            <map>
-              <key>hash</key>
-              <string>22f64c7fbb6704d2e9519fd1cca8e49b</string>
-              <key>url</key>
-              <string>http://automated-builds-secondlife-com.s3.amazonaws.com/ct2/6659/23560/urllib3-1.21.1-darwin64-506648.tar.bz2</string>
-            </map>
-            <key>name</key>
-            <string>darwin64</string>
-          </map>
-        </map>
-        <key>version</key>
-        <string>1.21.1</string>
-      </map>
       <key>viewer-manager</key>
       <map>
         <key>copyright</key>
@@ -3248,7 +3126,7 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors</string>
         <key>description</key>
         <string>Linden Lab Viewer Management Process suite.</string>
         <key>license</key>
-        <string>Proprietary</string>
+        <string>viewerlgpl</string>
         <key>license_file</key>
         <string>LICENSE</string>
         <key>name</key>
@@ -3260,9 +3138,9 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors</string>
             <key>archive</key>
             <map>
               <key>hash</key>
-              <string>ce95944fb842849108102263a25fc794</string>
+              <string>aaf04f5ed1d28477781d976dc04871ee</string>
               <key>url</key>
-              <string>http://automated-builds-secondlife-com.s3.amazonaws.com/ct2/23237/178332/viewer_manager-1.0.518840-darwin64-518840.tar.bz2</string>
+              <string>http://automated-builds-secondlife-com.s3.amazonaws.com/ct2/31904/266163/viewer_manager-2.0.524157-darwin64-524157.tar.bz2</string>
             </map>
             <key>name</key>
             <string>darwin64</string>
@@ -3284,9 +3162,9 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors</string>
             <key>archive</key>
             <map>
               <key>hash</key>
-              <string>642f847a9ac45551af65a55826974334</string>
+              <string>4604624f11b215b052f4a840f4da4bf8</string>
               <key>url</key>
-              <string>http://automated-builds-secondlife-com.s3.amazonaws.com/ct2/23236/178338/viewer_manager-1.0.518840-windows-518840.tar.bz2</string>
+              <string>http://automated-builds-secondlife-com.s3.amazonaws.com/ct2/31906/266177/viewer_manager-2.0.524157-windows-524157.tar.bz2</string>
             </map>
             <key>name</key>
             <string>windows</string>
@@ -3297,7 +3175,7 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors</string>
         <key>source_type</key>
         <string>hg</string>
         <key>version</key>
-        <string>1.0.518840</string>
+        <string>2.0.524157</string>
       </map>
       <key>vlc-bin</key>
       <map>
diff --git a/build.sh b/build.sh
index 1fc578e95623f622c75810dd9033d5f80e342449..1f9daa78b2029a8ac80db2a1d0c07dd4e0c73882 100755
--- a/build.sh
+++ b/build.sh
@@ -95,23 +95,54 @@ pre_build()
     && [ -r "$master_message_template_checkout/message_template.msg" ] \
     && template_verifier_master_url="-DTEMPLATE_VERIFIER_MASTER_URL=file://$master_message_template_checkout/message_template.msg"
 
-    # nat 2016-12-20: disable HAVOK on Mac until we get a 64-bit Mac build.
     RELEASE_CRASH_REPORTING=ON
     HAVOK=ON
     SIGNING=()
-    if [ "$arch" == "Darwin" ]
+    if [ "$arch" == "Darwin" -a "$variant" == "Release" ]
+    then SIGNING=("-DENABLE_SIGNING:BOOL=YES" \
+                  "-DSIGNING_IDENTITY:STRING=Developer ID Application: Linden Research, Inc.")
+    fi
+
+    if [ "${RELEASE_CRASH_REPORTING:-}" != "OFF" ]
     then
-         if [ "$variant" == "Release" ]
-         then SIGNING=("-DENABLE_SIGNING:BOOL=YES" \
-                       "-DSIGNING_IDENTITY:STRING=Developer ID Application: Linden Research, Inc.")
+        case "$arch" in
+            CYGWIN)
+                symplat="windows"
+                ;;
+            Darwin)
+                symplat="darwin"
+                ;;
+            Linux)
+                symplat="linux"
+                ;;
+        esac
+        # This name is consumed by indra/newview/CMakeLists.txt. Make it
+        # absolute because we've had troubles with relative pathnames.
+        abs_build_dir="$(cd "$build_dir"; pwd)"
+        VIEWER_SYMBOL_FILE="$(native_path "$abs_build_dir/newview/$variant/secondlife-symbols-$symplat-${AUTOBUILD_ADDRSIZE}.tar.bz2")"
+    fi
+
+    # don't spew credentials into build log
+    bugsplat_sh="$build_secrets_checkout/bugsplat/bugsplat.sh"
+    set +x
+    if [ -r "$bugsplat_sh" ]
+    then # show that we're doing this, just not the contents
+         echo source "$bugsplat_sh"
+         source "$bugsplat_sh"
+         # important: we test this and use its value in [grand-]child processes
+         if [ -n "${BUGSPLAT_DB:-}" ]
+         then echo export BUGSPLAT_DB
+              export BUGSPLAT_DB
          fi
     fi
+    set -x
 
     "$autobuild" configure --quiet -c $variant -- \
      -DPACKAGE:BOOL=ON \
-     -DUNATTENDED:BOOL=ON \
      -DHAVOK:BOOL="$HAVOK" \
      -DRELEASE_CRASH_REPORTING:BOOL="$RELEASE_CRASH_REPORTING" \
+     -DVIEWER_SYMBOL_FILE:STRING="${VIEWER_SYMBOL_FILE:-}" \
+     -DBUGSPLAT_DB:STRING="${BUGSPLAT_DB:-}" \
      -DVIEWER_CHANNEL:STRING="${viewer_channel}" \
      -DGRID:STRING="\"$viewer_grid\"" \
      -DTEMPLATE_VERIFIER_OPTIONS:STRING="$template_verifier_options" $template_verifier_master_url \
@@ -193,6 +224,8 @@ then
     exit 1
 fi
 
+shopt -s nullglob # if nothing matches a glob, expand to nothing
+
 initialize_build # provided by master buildscripts build.sh
 
 begin_section "autobuild initialize"
@@ -232,7 +265,6 @@ initialize_version # provided by buildscripts build.sh; sets version id
 
 # Now run the build
 succeeded=true
-build_processes=
 last_built_variant=
 for variant in $variants
 do
@@ -240,7 +272,6 @@ do
   last_built_variant="$variant"
 
   build_dir=`build_dir_$arch $variant`
-  build_dir_stubs="$build_dir/win_setup/$variant"
 
   begin_section "Initialize $variant Build Directory"
   rm -rf "$build_dir"
@@ -417,19 +448,7 @@ then
           if [ "${RELEASE_CRASH_REPORTING:-}" != "OFF" ]
           then
               # Upload crash reporter file
-              # These names must match the set of VIEWER_SYMBOL_FILE in indra/newview/CMakeLists.txt
-              case "$arch" in
-                  CYGWIN)
-                      symbolfile="$build_dir/newview/Release/secondlife-symbols-windows-${AUTOBUILD_ADDRSIZE}.tar.bz2"
-                      ;;
-                  Darwin)
-                      symbolfile="$build_dir/newview/Release/secondlife-symbols-darwin-${AUTOBUILD_ADDRSIZE}.tar.bz2"
-                      ;;
-                  Linux)
-                      symbolfile="$build_dir/newview/Release/secondlife-symbols-linux-${AUTOBUILD_ADDRSIZE}.tar.bz2"
-                      ;;
-              esac
-              python_cmd "$helpers/codeticket.py" addoutput "Symbolfile" "$symbolfile" \
+              python_cmd "$helpers/codeticket.py" addoutput "Symbolfile" "$VIEWER_SYMBOL_FILE" \
                   || fatal "Upload of symbolfile failed"
           fi
 
diff --git a/indra/CMakeLists.txt b/indra/CMakeLists.txt
index a40b2c084623870294b6f607df2fb0a54a700b7a..6c20a813bad182415bf5ca7665a1055c9bf0717a 100644
--- a/indra/CMakeLists.txt
+++ b/indra/CMakeLists.txt
@@ -44,6 +44,7 @@ if (WINDOWS AND EXISTS ${LIBS_CLOSED_DIR}copy_win_scripts)
 endif (WINDOWS AND EXISTS ${LIBS_CLOSED_DIR}copy_win_scripts)
 
 add_custom_target(viewer)
+
 add_subdirectory(${LIBS_OPEN_PREFIX}llcrashlogger)
 add_subdirectory(${LIBS_OPEN_PREFIX}llplugin)
 add_subdirectory(${LIBS_OPEN_PREFIX}llui)
diff --git a/indra/cmake/CMakeLists.txt b/indra/cmake/CMakeLists.txt
index 4a3ebe4835bff370129a9409b3d93a78dcb77496..84e1c5d6fdee0b417b144d9b314a47817eb8cbac 100644
--- a/indra/cmake/CMakeLists.txt
+++ b/indra/cmake/CMakeLists.txt
@@ -12,6 +12,7 @@ set(cmake_SOURCE_FILES
     Audio.cmake
     BerkeleyDB.cmake
     Boost.cmake
+    bugsplat.cmake
     BuildVersion.cmake
     CEFPlugin.cmake
     CEFPlugin.cmake
diff --git a/indra/cmake/Copy3rdPartyLibs.cmake b/indra/cmake/Copy3rdPartyLibs.cmake
index 09a97fc03e979b17bc68553aad0515f5c3c9d82a..dde53835fb5d770ddd954fb0138e902197d28efd 100644
--- a/indra/cmake/Copy3rdPartyLibs.cmake
+++ b/indra/cmake/Copy3rdPartyLibs.cmake
@@ -49,6 +49,20 @@ if(WINDOWS)
         libhunspell.dll
         )
 
+    # Filenames are different for 32/64 bit BugSplat file and we don't
+    # have any control over them so need to branch.
+    if (BUGSPLAT_DB)
+      if(ADDRESS_SIZE EQUAL 32)
+        set(release_files ${release_files} BugSplat.dll)
+        set(release_files ${release_files} BugSplatRc.dll)
+        set(release_files ${release_files} BsSndRpt.exe)
+      else(ADDRESS_SIZE EQUAL 32)
+        set(release_files ${release_files} BugSplat64.dll)
+        set(release_files ${release_files} BugSplatRc64.dll)
+        set(release_files ${release_files} BsSndRpt64.exe)
+      endif(ADDRESS_SIZE EQUAL 32)
+    endif (BUGSPLAT_DB)
+
     if (FMODEX)
 
         if(ADDRESS_SIZE EQUAL 32)
diff --git a/indra/cmake/LLAddBuildTest.cmake b/indra/cmake/LLAddBuildTest.cmake
index 024bfe14a104e8814f52c5cc5cf4fd06fa98e605..b3f42c1a5e93048fae218d570e780b79595758f6 100644
--- a/indra/cmake/LLAddBuildTest.cmake
+++ b/indra/cmake/LLAddBuildTest.cmake
@@ -1,4 +1,5 @@
 # -*- cmake -*-
+include(00-Common)
 include(LLTestCommand)
 include(GoogleMock)
 include(Tut)
diff --git a/indra/cmake/LLBase.cmake b/indra/cmake/LLBase.cmake
deleted file mode 100644
index 76e3c688a3a0bf941d2f250c53f1d1f07375b08b..0000000000000000000000000000000000000000
--- a/indra/cmake/LLBase.cmake
+++ /dev/null
@@ -1,4 +0,0 @@
-# -*- cmake -*-
-include(Prebuilt)
-
-use_prebuilt_binary(llbase)
diff --git a/indra/cmake/Linking.cmake b/indra/cmake/Linking.cmake
index 74fe3f11377d096bc3e57fdaba85360168cb9a65..3cb235a9d58775166c57cb0f70878829a824dae8 100644
--- a/indra/cmake/Linking.cmake
+++ b/indra/cmake/Linking.cmake
@@ -66,6 +66,7 @@ if (WINDOWS)
       wldap32
       gdi32
       user32
+      ole32
       dbghelp
       )
 else (WINDOWS)
diff --git a/indra/cmake/Requests.cmake b/indra/cmake/Requests.cmake
deleted file mode 100644
index b9c729d6970a1d160b1e60f8a4e949607eaf1223..0000000000000000000000000000000000000000
--- a/indra/cmake/Requests.cmake
+++ /dev/null
@@ -1,7 +0,0 @@
-if (DARWIN)
-   include (Prebuilt)
-   use_prebuilt_binary(requests)
-   use_prebuilt_binary(urllib3)
-   use_prebuilt_binary(chardet)
-   use_prebuilt_binary(idna)
-endif (DARWIN)
diff --git a/indra/cmake/Variables.cmake b/indra/cmake/Variables.cmake
index bd69c4985644a8e5d4e642341f9b01f5f8c49c5d..2b54cd41552485e882411ad79608b2b8b4933057 100644
--- a/indra/cmake/Variables.cmake
+++ b/indra/cmake/Variables.cmake
@@ -33,6 +33,8 @@ set(INTEGRATION_TESTS_PREFIX)
 set(LL_TESTS ON CACHE BOOL "Build and run unit and integration tests (disable for build timing runs to reduce variation")
 set(INCREMENTAL_LINK OFF CACHE BOOL "Use incremental linking on win32 builds (enable for faster links on some machines)")
 set(ENABLE_MEDIA_PLUGINS ON CACHE BOOL "Turn off building media plugins if they are imported by third-party library mechanism")
+set(VIEWER_SYMBOL_FILE "" CACHE STRING "Name of tarball into which to place symbol files")
+set(BUGSPLAT_DB "" CACHE STRING "BugSplat database name, if BugSplat crash reporting is desired")
 
 if(LIBS_CLOSED_DIR)
   file(TO_CMAKE_PATH "${LIBS_CLOSED_DIR}" LIBS_CLOSED_DIR)
@@ -209,7 +211,6 @@ set(SIGNING_IDENTITY "" CACHE STRING "Specifies the signing identity to use, if
 
 set(VERSION_BUILD "0" CACHE STRING "Revision number passed in from the outside")
 set(USESYSTEMLIBS OFF CACHE BOOL "Use libraries from your system rather than Linden-supplied prebuilt libraries.")
-set(UNATTENDED OFF CACHE BOOL "Should be set to ON for building with VC Express editions.")
 
 set(USE_PRECOMPILED_HEADERS ON CACHE BOOL "Enable use of precompiled header directives where supported.")
 
diff --git a/indra/cmake/bugsplat.cmake b/indra/cmake/bugsplat.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..59644b73ce13f2bdbf63f56d2113e2fbb8c5bd16
--- /dev/null
+++ b/indra/cmake/bugsplat.cmake
@@ -0,0 +1,25 @@
+# BugSplat is engaged by setting BUGSPLAT_DB to the target BugSplat database
+# name.
+if (BUGSPLAT_DB)
+  if (USESYSTEMLIBS)
+    message(STATUS "Looking for system BugSplat")
+    set(BUGSPLAT_FIND_QUIETLY ON)
+    set(BUGSPLAT_FIND_REQUIRED ON)
+    include(FindBUGSPLAT)
+  else (USESYSTEMLIBS)
+    message(STATUS "Engaging autobuild BugSplat")
+    include(Prebuilt)
+    use_prebuilt_binary(bugsplat)
+    if (WINDOWS)
+      set(BUGSPLAT_LIBRARIES 
+        ${ARCH_PREBUILT_DIRS_RELEASE}/bugsplat.lib
+        )
+    elseif (DARWIN)
+      find_library(BUGSPLAT_LIBRARIES BugsplatMac
+        PATHS "${ARCH_PREBUILT_DIRS_RELEASE}")
+    else (WINDOWS)
+
+    endif (WINDOWS)
+    set(BUGSPLAT_INCLUDE_DIR ${LIBS_PREBUILT_DIR}/include/bugsplat)
+  endif (USESYSTEMLIBS)
+endif (BUGSPLAT_DB)
diff --git a/indra/integration_tests/llimage_libtest/CMakeLists.txt b/indra/integration_tests/llimage_libtest/CMakeLists.txt
index 13cf1f7bde9014b85d0ff31272e38b7867a9f796..d9353f904c2c23588a1b22bdb1876702c0664eea 100644
--- a/indra/integration_tests/llimage_libtest/CMakeLists.txt
+++ b/indra/integration_tests/llimage_libtest/CMakeLists.txt
@@ -40,7 +40,7 @@ add_executable(llimage_libtest
     WIN32
     MACOSX_BUNDLE
     ${llimage_libtest_SOURCE_FILES}
-)
+    )
 
 set_target_properties(llimage_libtest
     PROPERTIES
diff --git a/indra/lib/python/indra/util/llmanifest.py b/indra/lib/python/indra/util/llmanifest.py
index 598261e3d7000772b1fc57d7c3c09671db44a108..2e6cf53912c2cd0e06440a4346aaaf03ba5c06da 100755
--- a/indra/lib/python/indra/util/llmanifest.py
+++ b/indra/lib/python/indra/util/llmanifest.py
@@ -33,13 +33,14 @@
 import fnmatch
 import getopt
 import glob
+import itertools
+import operator
 import os
 import re
 import shutil
+import subprocess
 import sys
 import tarfile
-import errno
-import subprocess
 
 class ManifestError(RuntimeError):
     """Use an exception more specific than generic Python RuntimeError"""
@@ -49,7 +50,9 @@ def __init__(self, msg):
 
 class MissingError(ManifestError):
     """You specified a file that doesn't exist"""
-    pass
+    def __init__(self, msg):
+        self.msg = msg
+        super(MissingError, self).__init__(self.msg)
 
 def path_ancestors(path):
     drive, path = os.path.splitdrive(os.path.normpath(path))
@@ -90,7 +93,7 @@ def get_default_platform(dummy):
 CHANNEL_VENDOR_BASE = 'Second Life'
 RELEASE_CHANNEL = CHANNEL_VENDOR_BASE + ' Release'
 
-ARGUMENTS=[
+BASE_ARGUMENTS=[
     dict(name='actions',
          description="""This argument specifies the actions that are to be taken when the
         script is run.  The meaningful actions are currently:
@@ -108,8 +111,19 @@ def get_default_platform(dummy):
         Example use: %(name)s --arch=i686
         On Linux this would try to use Linux_i686Manifest.""",
          default=""),
+    dict(name='artwork', description='Artwork directory.', default=DEFAULT_SRCTREE),
     dict(name='build', description='Build directory.', default=DEFAULT_SRCTREE),
     dict(name='buildtype', description='Build type (i.e. Debug, Release, RelWithDebInfo).', default=None),
+    dict(name='bundleid',
+         description="""The Mac OS X Bundle identifier.""",
+         default="com.secondlife.indra.viewer"),
+    dict(name='channel',
+         description="""The channel to use for updates, packaging, settings name, etc.""",
+         default='CHANNEL UNSET'),
+    dict(name='channel_suffix',
+         description="""Addition to the channel for packaging and channel value,
+         but not application name (used internally)""",
+         default=None),
     dict(name='configuration',
          description="""The build configuration used.""",
          default="Release"),
@@ -117,12 +131,6 @@ def get_default_platform(dummy):
     dict(name='grid',
          description="""Which grid the client will try to connect to.""",
          default=None),
-    dict(name='channel',
-         description="""The channel to use for updates, packaging, settings name, etc.""",
-         default='CHANNEL UNSET'),
-    dict(name='channel_suffix',
-         description="""Addition to the channel for packaging and channel value, but not application name (used internally)""",
-         default=None),
     dict(name='installer_name',
          description=""" The name of the file that the installer should be
         packaged up into. Only used on Linux at the moment.""",
@@ -134,10 +142,14 @@ def get_default_platform(dummy):
          description="""The current platform, to be used for looking up which
         manifest class to run.""",
          default=get_default_platform),
+    dict(name='signature',
+         description="""This specifies an identity to sign the viewer with, if any.
+        If no value is supplied, the default signature will be used, if any. Currently
+        only used on Mac OS X.""",
+         default=None),
     dict(name='source',
          description='Source directory.',
          default=DEFAULT_SRCTREE),
-    dict(name='artwork', description='Artwork directory.', default=DEFAULT_SRCTREE),
     dict(name='touch',
          description="""File to touch when action is finished. Touch file will
         contain the name of the final package in a form suitable
@@ -145,23 +157,15 @@ def get_default_platform(dummy):
          default=None),
     dict(name='versionfile',
          description="""The name of a file containing the full version number."""),
-    dict(name='bundleid',
-         description="""The Mac OS X Bundle identifier.""",
-         default="com.secondlife.indra.viewer"),
-    dict(name='signature',
-         description="""This specifies an identity to sign the viewer with, if any.
-        If no value is supplied, the default signature will be used, if any. Currently
-        only used on Mac OS X.""",
-         default=None)
     ]
 
-def usage(srctree=""):
+def usage(arguments, srctree=""):
     nd = {'name':sys.argv[0]}
     print """Usage:
     %(name)s [options] [destdir]
     Options:
     """ % nd
-    for arg in ARGUMENTS:
+    for arg in arguments:
         default = arg['default']
         if hasattr(default, '__call__'):
             default = "(computed value) \"" + str(default(srctree)) + '"'
@@ -172,11 +176,15 @@ def usage(srctree=""):
             default,
             arg['description'] % nd)
 
-def main():
-##  import itertools
+def main(extra=[]):
 ##  print ' '.join((("'%s'" % item) if ' ' in item else item)
 ##                 for item in itertools.chain([sys.executable], sys.argv))
-    option_names = [arg['name'] + '=' for arg in ARGUMENTS]
+    # Supplement our default command-line switches with any desired by
+    # application-specific caller.
+    arguments = list(itertools.chain(BASE_ARGUMENTS, extra))
+    # Alphabetize them by option name in case we display usage.
+    arguments.sort(key=operator.itemgetter('name'))
+    option_names = [arg['name'] + '=' for arg in arguments]
     option_names.append('help')
     options, remainder = getopt.getopt(sys.argv[1:], "", option_names)
 
@@ -199,11 +207,11 @@ def main():
     # early out for help
     if 'help' in args:
         # *TODO: it is a huge hack to pass around the srctree like this
-        usage(args['source'])
+        usage(arguments, srctree=args['source'])
         return
 
     # defaults
-    for arg in ARGUMENTS:
+    for arg in arguments:
         if arg['name'] not in args:
             default = arg['default']
             if hasattr(default, '__call__'):
@@ -232,104 +240,68 @@ def main():
         print "Option:", opt, "=", args[opt]
 
     # pass in sourceid as an argument now instead of an environment variable
-    try:
-        args['sourceid'] = os.environ["sourceid"]
-    except KeyError:
-        args['sourceid'] = ""
+    args['sourceid'] = os.environ.get("sourceid", "")
 
     # Build base package.
     touch = args.get('touch')
     if touch:
-        print 'Creating base package'
-    args['package_id'] = "" # base package has no package ID
+        print '================ Creating base package'
+    else:
+        print '================ Starting base copy'
     wm = LLManifest.for_platform(args['platform'], args.get('arch'))(args)
     wm.do(*args['actions'])
     # Store package file for later if making touched file.
     base_package_file = ""
     if touch:
-        print 'Created base package ', wm.package_file
+        print '================ Created base package ', wm.package_file
         base_package_file = "" + wm.package_file
+    else:
+        print '================ Finished base copy'
 
     # handle multiple packages if set
-    try:
-        additional_packages = os.environ["additional_packages"]
-    except KeyError:
-        additional_packages = ""
+    # ''.split() produces empty list
+    additional_packages = os.environ.get("additional_packages", "").split()
     if additional_packages:
         # Determine destination prefix / suffix for additional packages.
-        base_dest_postfix = args['dest']
-        base_dest_prefix = ""
-        base_dest_parts = args['dest'].split(os.sep)
-        if len(base_dest_parts) > 1:
-            base_dest_postfix = base_dest_parts[len(base_dest_parts) - 1]
-            base_dest_prefix = base_dest_parts[0]
-            i = 1
-            while i < len(base_dest_parts) - 1:
-                base_dest_prefix = base_dest_prefix + os.sep + base_dest_parts[i]
-                i = i + 1
+        base_dest_parts = list(os.path.split(args['dest']))
+        base_dest_parts.insert(-1, "{}")
+        base_dest_template = os.path.join(*base_dest_parts)
         # Determine touched prefix / suffix for additional packages.
-        base_touch_postfix = ""
-        base_touch_prefix = ""
         if touch:
-            base_touch_postfix = touch
-            base_touch_parts = touch.split('/')
+            base_touch_parts = list(os.path.split(touch))
+            # Because of the special insert() logic below, we don't just want
+            # [dirpath, basename]; we want [dirpath, directory, basename].
+            # Further split the dirpath and replace it in the list.
+            base_touch_parts[0:1] = os.path.split(base_touch_parts[0])
             if "arwin" in args['platform']:
-                if len(base_touch_parts) > 1:
-                    base_touch_postfix = base_touch_parts[len(base_touch_parts) - 1]
-                    base_touch_prefix = base_touch_parts[0]
-                    i = 1
-                    while i < len(base_touch_parts) - 1:
-                        base_touch_prefix = base_touch_prefix + '/' + base_touch_parts[i]
-                        i = i + 1
+                base_touch_parts.insert(-1, "{}")
             else:
-                if len(base_touch_parts) > 2:
-                    base_touch_postfix = base_touch_parts[len(base_touch_parts) - 2] + '/' + base_touch_parts[len(base_touch_parts) - 1]
-                    base_touch_prefix = base_touch_parts[0]
-                    i = 1
-                    while i < len(base_touch_parts) - 2:
-                        base_touch_prefix = base_touch_prefix + '/' + base_touch_parts[i]
-                        i = i + 1
-        # Store base channel name.
-        base_channel_name = args['channel']
-        # Build each additional package.
-        package_id_list = additional_packages.split(" ")
-        args['channel'] = base_channel_name
-        for package_id in package_id_list:
-            try:
-                if package_id + "_viewer_channel_suffix" in os.environ:
-                    args['channel_suffix'] = os.environ[package_id + "_viewer_channel_suffix"]
-                else:
-                    args['channel_suffix'] = None
-                if package_id + "_sourceid" in os.environ:
-                    args['sourceid'] = os.environ[package_id + "_sourceid"]
-                else:
-                    args['sourceid'] = None
-                args['dest'] = base_dest_prefix + os.sep + package_id + os.sep + base_dest_postfix
-            except KeyError:
-                sys.stderr.write("Failed to create package for package_id: %s" % package_id)
-                sys.stderr.flush()
-                continue
+                base_touch_parts.insert(-2, "{}")
+            base_touch_template = os.path.join(*base_touch_parts)
+        for package_id in additional_packages:
+            args['channel_suffix'] = os.environ.get(package_id + "_viewer_channel_suffix")
+            args['sourceid']       = os.environ.get(package_id + "_sourceid")
+            args['dest'] = base_dest_template.format(package_id)
             if touch:
-                print 'Creating additional package for "', package_id, '" in ', args['dest']
+                print '================ Creating additional package for "', package_id, '" in ', args['dest']
+            else:
+                print '================ Starting additional copy for "', package_id, '" in ', args['dest']
             try:
                 wm = LLManifest.for_platform(args['platform'], args.get('arch'))(args)
                 wm.do(*args['actions'])
             except Exception as err:
                 sys.exit(str(err))
             if touch:
-                print 'Created additional package ', wm.package_file, ' for ', package_id
-                faketouch = base_touch_prefix + '/' + package_id + '/' + base_touch_postfix
-                fp = open(faketouch, 'w')
-                fp.write('set package_file=%s\n' % wm.package_file)
-                fp.close()
-    
+                print '================ Created additional package ', wm.package_file, ' for ', package_id
+                with open(base_touch_template.format(package_id), 'w') as fp:
+                    fp.write('set package_file=%s\n' % wm.package_file)
+            else:
+                print '================ Finished additional copy "', package_id, '" in ', args['dest']
     # Write out the package file in this format, so that it can easily be called
     # and used in a .bat file - yeah, it sucks, but this is the simplest...
-    touch = args.get('touch')
     if touch:
-        fp = open(touch, 'w')
-        fp.write('set package_file=%s\n' % base_package_file)
-        fp.close()
+        with open(touch, 'w') as fp:
+            fp.write('set package_file=%s\n' % base_package_file)
         print 'touched', touch
     return 0
 
@@ -375,7 +347,7 @@ def exclude(self, glob):
         in the file list by path()."""
         self.excludes.append(glob)
 
-    def prefix(self, src='', build=None, dst=None):
+    def prefix(self, src='', build='', dst='', src_dst=None):
         """
         Usage:
 
@@ -385,8 +357,21 @@ def prefix(self, src='', build=None, dst=None):
         For the duration of the 'with' block, pushes a prefix onto the stack.
         Within that block, all relevant method calls (esp. to path()) will
         prefix paths with the entire prefix stack. Source and destination
-        prefixes can be different, though if only one is provided they are
-        both equal. To specify a no-op, use an empty string, not None.
+        prefixes are independent; if omitted (or passed as the empty string),
+        the prefix has no effect. Thus:
+
+        with self.prefix(src='foo'):
+            # no effect on dst
+
+        with self.prefix(dst='bar'):
+            # no effect on src
+
+        If you want to set both at once, use src_dst:
+
+        with self.prefix(src_dst='subdir'):
+            # same as self.prefix(src='subdir', dst='subdir')
+            # Passing src_dst makes any src or dst argument in the same
+            # parameter list irrelevant.
 
         Also supports the older (pre-Python-2.5) syntax:
 
@@ -400,34 +385,42 @@ def prefix(self, src='', build=None, dst=None):
         returned True specifically so that the caller could indent the
         relevant block of code with 'if', just for aesthetic purposes.
         """
-        if dst is None:
-            dst = src
-        if build is None:
-            build = src
+        if src_dst is not None:
+            src = src_dst
+            dst = src_dst
         self.src_prefix.append(src)
         self.artwork_prefix.append(src)
         self.build_prefix.append(build)
         self.dst_prefix.append(dst)
 
+##      self.display_stacks()
+
         # The above code is unchanged from the original implementation. What's
         # new is the return value. We're going to return an instance of
         # PrefixManager that binds this LLManifest instance and Does The Right
         # Thing on exit.
         return self.PrefixManager(self)
 
+    def display_stacks(self):
+        width = 1 + max(len(stack) for stack in self.PrefixManager.stacks)
+        for stack in self.PrefixManager.stacks:
+            print "{} {}".format((stack + ':').ljust(width),
+                                 os.path.join(*getattr(self, stack)))
+
     class PrefixManager(object):
+        # stack attributes we manage in this LLManifest (sub)class
+        # instance
+        stacks = ("src_prefix", "artwork_prefix", "build_prefix", "dst_prefix")
+
         def __init__(self, manifest):
             self.manifest = manifest
-            # stack attributes we manage in this LLManifest (sub)class
-            # instance
-            stacks = ("src_prefix", "artwork_prefix", "build_prefix", "dst_prefix")
             # If the caller wrote:
             # with self.prefix(...):
             # as intended, then bind the state of each prefix stack as it was
             # just BEFORE the call to prefix(). Since prefix() appended an
             # entry to each prefix stack, capture len()-1.
             self.prevlen = { stack: len(getattr(self.manifest, stack)) - 1
-                             for stack in stacks }
+                             for stack in self.stacks }
 
         def __nonzero__(self):
             # If the caller wrote:
@@ -460,6 +453,8 @@ def __exit__(self, type, value, traceback):
                 # truncate that list back to 'prevlen'
                 del getattr(self.manifest, stack)[prevlen:]
 
+##          self.manifest.display_stacks()
+
     def end_prefix(self, descr=None):
         """Pops a prefix off the stack.  If given an argument, checks
         the argument against the top of the stack.  If the argument
@@ -505,6 +500,19 @@ def dst_path_of(self, relpath):
         relative to the destination directory."""
         return os.path.join(self.get_dst_prefix(), relpath)
 
+    def _relative_dst_path(self, dstpath):
+        """
+        Returns the path to a file or directory relative to the destination directory.
+        This should only be used for generating diagnostic output in the path method.
+        """
+        dest_root=self.dst_prefix[0]
+        if dstpath.startswith(dest_root+os.path.sep):
+            return dstpath[len(dest_root)+1:]
+        elif dstpath.startswith(dest_root):
+            return dstpath[len(dest_root):]
+        else:
+            return dstpath
+
     def ensure_src_dir(self, reldir):
         """Construct the path for a directory relative to the
         source path, and ensures that it exists.  Returns the
@@ -607,9 +615,16 @@ def cleanup_finish(self):
             # *TODO is this gonna be useful?
             print "Cleaning up " + c
 
+    def process_either(self, src, dst):
+        # If it's a real directory, recurse through it --
+        # but not a symlink! Handle those like files.
+        if os.path.isdir(src) and not os.path.islink(src):
+            return self.process_directory(src, dst)
+        else:
+            return self.process_file(src, dst)
+
     def process_file(self, src, dst):
         if self.includes(src, dst):
-#            print src, "=>", dst
             for action in self.actions:
                 methodname = action + "_action"
                 method = getattr(self, methodname, None)
@@ -634,10 +649,7 @@ def process_directory(self, src, dst):
         for name in names:
             srcname = os.path.join(src, name)
             dstname = os.path.join(dst, name)
-            if os.path.isdir(srcname):
-                count += self.process_directory(srcname, dstname)
-            else:
-                count += self.process_file(srcname, dstname)
+            count += self.process_either(srcname, dstname)
         return count
 
     def includes(self, src, dst):
@@ -677,7 +689,11 @@ def ccopyfile(self, src, dst):
         # Don't recopy file if it's up-to-date.
         # If we seem to be not not overwriting files that have been
         # updated, set the last arg to False, but it will take longer.
+##      reldst = (dst[len(self.dst_prefix[0]):]
+##                if dst.startswith(self.dst_prefix[0])
+##                else dst).lstrip(r'\/')
         if os.path.exists(dst) and filecmp.cmp(src, dst, True):
+##          print "{} (skipping, {} exists)".format(src, reldst)
             return
         # only copy if it's not excluded
         if self.includes(src, dst):
@@ -687,6 +703,7 @@ def ccopyfile(self, src, dst):
                 if err.errno != errno.ENOENT:
                     raise
 
+##          print "{} => {}".format(src, reldst)
             shutil.copy2(src, dst)
 
     def ccopytree(self, src, dst):
@@ -785,13 +802,13 @@ def path2basename(self, path, file):
         return self.path(os.path.join(path, file), file)
 
     def path(self, src, dst=None):
-        sys.stdout.write("Processing %s => %s ... " % (src, dst))
         sys.stdout.flush()
         if src == None:
             raise ManifestError("No source file, dst is " + dst)
         if dst == None:
             dst = src
         dst = os.path.join(self.get_dst_prefix(), dst)
+        sys.stdout.write("Processing %s => %s ... " % (src, self._relative_dst_path(dst)))
 
         def try_path(src):
             # expand globs
@@ -804,29 +821,21 @@ def try_path(src):
                 # if we're specifying a single path (not a glob),
                 # we should error out if it doesn't exist
                 self.check_file_exists(src)
-                # if it's a directory, recurse through it
-                if os.path.isdir(src):
-                    count += self.process_directory(src, dst)
-                else:
-                    count += self.process_file(src, dst)
+                count += self.process_either(src, dst)
             return count
 
-        for pfx in self.get_src_prefix(), self.get_artwork_prefix(), self.get_build_prefix():
+        try_prefixes = [self.get_src_prefix(), self.get_artwork_prefix(), self.get_build_prefix()]
+        tried=[]
+        count=0
+        while not count and try_prefixes: 
+            pfx = try_prefixes.pop(0)
             try:
                 count = try_path(os.path.join(pfx, src))
             except MissingError:
-                # If src isn't a wildcard, and if that file doesn't exist in
-                # this pfx, try next pfx.
-                count = 0
-                continue
-
-            # Here try_path() didn't raise MissingError. Did it process any files?
-            if count:
-                break
-            # Even though try_path() didn't raise MissingError, it returned 0
-            # files. src is probably a wildcard meant for some other pfx. Loop
-            # back to try the next.
-
+                tried.append(pfx)
+                if not try_prefixes:
+                    # no more prefixes left to try
+                    print "unable to find '%s'; looked in:\n  %s" % (src, '\n  '.join(tried))
         print "%d files" % count
 
         # Let caller check whether we processed as many files as expected. In
diff --git a/indra/linux_crash_logger/CMakeLists.txt b/indra/linux_crash_logger/CMakeLists.txt
index 029096df3732fc9ab65cecc9ec3bfaa7e98ce573..315aed8d114e2185016a0dc7d44ef67cd7cb1557 100644
--- a/indra/linux_crash_logger/CMakeLists.txt
+++ b/indra/linux_crash_logger/CMakeLists.txt
@@ -78,4 +78,4 @@ target_link_libraries(linux-crash-logger
     )
 
 add_custom_target(linux-crash-logger-target ALL
-                  DEPENDS linux-crash-logger)
+    DEPENDS linux-crash-logger)
diff --git a/indra/llappearance/lllocaltextureobject.cpp b/indra/llappearance/lllocaltextureobject.cpp
index f49cf215129a2397d4d4eb3b362e32c5e75be032..3f564ec3de305f7f77d4f2ea607bc3187b5c137c 100644
--- a/indra/llappearance/lllocaltextureobject.cpp
+++ b/indra/llappearance/lllocaltextureobject.cpp
@@ -210,4 +210,3 @@ void LLLocalTextureObject::setBakedReady(BOOL ready)
 {
 	mIsBakedReady = ready;
 }
-
diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt
index d9eb13d65a6ece0a5289a8d2d1c8a87da03aa0df..42ad56f1b0b42114ae67244159afc61f198baaad 100644
--- a/indra/llcommon/CMakeLists.txt
+++ b/indra/llcommon/CMakeLists.txt
@@ -255,6 +255,11 @@ set(llcommon_HEADER_FILES
 set_source_files_properties(${llcommon_HEADER_FILES}
                             PROPERTIES HEADER_FILE_ONLY TRUE)
 
+if (BUGSPLAT_DB)
+  set_source_files_properties(llapp.cpp
+    PROPERTIES COMPILE_DEFINITIONS "LL_BUGSPLAT")
+endif (BUGSPLAT_DB)
+
 list(APPEND llcommon_SOURCE_FILES ${llcommon_HEADER_FILES})
 
 if(LLCOMMON_LINK_SHARED)
diff --git a/indra/llcommon/llapp.cpp b/indra/llcommon/llapp.cpp
index 6cc9e804d4ac4558551247128e31980ea0decb62..421af3006e29d05aa21bb6e295b77e140113614e 100644
--- a/indra/llcommon/llapp.cpp
+++ b/indra/llcommon/llapp.cpp
@@ -392,7 +392,7 @@ void LLApp::setupErrorHandling(bool second_instance)
 
 #if LL_WINDOWS
 
-#if LL_SEND_CRASH_REPORTS
+#if LL_SEND_CRASH_REPORTS && ! defined(LL_BUGSPLAT)
 	EnableCrashingOnCrashes();
 
 	// This sets a callback to handle w32 signals to the console window.
@@ -454,8 +454,15 @@ void LLApp::setupErrorHandling(bool second_instance)
 			mExceptionHandler->set_handle_debug_exceptions(true);
 		}
 	}
-#endif
-#else
+#endif // LL_SEND_CRASH_REPORTS && ! defined(LL_BUGSPLAT)
+#else  // ! LL_WINDOWS
+
+#if defined(LL_BUGSPLAT)
+	// Don't install our own signal handlers -- BugSplat needs to hook them,
+	// or it's completely ineffectual.
+	bool installHandler = false;
+
+#else // ! LL_BUGSPLAT
 	//
 	// Start up signal handling.
 	//
@@ -463,9 +470,11 @@ void LLApp::setupErrorHandling(bool second_instance)
 	// thread, asynchronous signals can be delivered to any thread (in theory)
 	//
 	setup_signals();
-	
+
 	// Add google breakpad exception handler configured for Darwin/Linux.
 	bool installHandler = true;
+#endif // ! LL_BUGSPLAT
+
 #if LL_DARWIN
 	// For the special case of Darwin, we do not want to install the handler if
 	// the process is being debugged as the app will exit with value ABRT (6) if
@@ -498,7 +507,7 @@ void LLApp::setupErrorHandling(bool second_instance)
 		// installing the handler.
 		installHandler = true;
 	}
-	#endif
+	#endif // ! LL_RELEASE_FOR_DOWNLOAD
 
 	if(installHandler && (mExceptionHandler == 0))
 	{
@@ -514,9 +523,9 @@ void LLApp::setupErrorHandling(bool second_instance)
 		google_breakpad::MinidumpDescriptor desc(mDumpPath);
 	    mExceptionHandler = new google_breakpad::ExceptionHandler(desc, NULL, unix_minidump_callback, NULL, true, -1);
 	}
-#endif
+#endif // LL_LINUX
 
-#endif
+#endif // ! LL_WINDOWS
 	startErrorThread();
 }
 
diff --git a/indra/llcommon/llcoros.h b/indra/llcommon/llcoros.h
index 8fb27af6a474d175b3d6d49505bbb2df54e261dd..c551413811502025f52bc1d71b9580f09e97617d 100644
--- a/indra/llcommon/llcoros.h
+++ b/indra/llcommon/llcoros.h
@@ -169,6 +169,26 @@ class LL_COMMON_API LLCoros: public LLSingleton<LLCoros>
     static void set_consuming(bool consuming);
     static bool get_consuming();
 
+    /**
+     * RAII control of the consuming flag
+     */
+    class OverrideConsuming
+    {
+    public:
+        OverrideConsuming(bool consuming):
+            mPrevConsuming(get_consuming())
+        {
+            set_consuming(consuming);
+        }
+        ~OverrideConsuming()
+        {
+            set_consuming(mPrevConsuming);
+        }
+
+    private:
+        bool mPrevConsuming;
+    };
+
     /**
      * Please do NOT directly use boost::dcoroutines::future! It is essential
      * to maintain the "current" coroutine at every context switch. This
diff --git a/indra/llcommon/llerror.cpp b/indra/llcommon/llerror.cpp
index 40eb7d9bac9c496d5c49fe40ac21df7b72d03636..7cfd1409b13a6d6d2e30c866ebd5404bb4cc32b8 100644
--- a/indra/llcommon/llerror.cpp
+++ b/indra/llcommon/llerror.cpp
@@ -132,8 +132,6 @@ namespace {
                     mFile.sync_with_stdio(false);
                 }
             }
-			mWantsTime = true;
-            mWantsTags = true;
 		}
 		
 		~RecordToFile()
@@ -175,7 +173,7 @@ namespace {
 	public:
 		RecordToStderr(bool timestamp) : mUseANSI(ANSI_PROBE) 
 		{
-			mWantsTime = timestamp;
+            this->showMultiline(true);
 		}
 		
         virtual bool enabled() override
@@ -241,7 +239,13 @@ namespace {
 	class RecordToFixedBuffer : public LLError::Recorder
 	{
 	public:
-		RecordToFixedBuffer(LLLineBuffer* buffer) : mBuffer(buffer) { }
+		RecordToFixedBuffer(LLLineBuffer* buffer)
+            : mBuffer(buffer)
+            {
+                this->showMultiline(true);
+                this->showTags(false);
+                this->showLocation(false);
+            }
 		
         virtual bool enabled() override
         {
@@ -263,7 +267,11 @@ namespace {
 	{
 	public:
 		RecordToWinDebug()
-		{}
+		{
+            this->showMultiline(true);
+            this->showTags(false);
+            this->showLocation(false);
+        }
 
         virtual bool enabled() override
         {
@@ -412,6 +420,7 @@ namespace
 	public:
 		std::ostringstream messageStream;
 		bool messageStreamInUse;
+		std::string mFatalMessage;
 
 		void addCallSite(LLError::CallSite&);
 		void invalidateCallSites();
@@ -454,8 +463,6 @@ namespace LLError
 	public:
 		virtual ~SettingsConfig();
 
-		bool                                mPrintLocation;
-
 		LLError::ELevel                     mDefaultLevel;
 
         bool 								mLogAlwaysFlush;
@@ -500,7 +507,6 @@ namespace LLError
 
 	SettingsConfig::SettingsConfig()
 		: LLRefCount(),
-		mPrintLocation(false),
 		mDefaultLevel(LLError::LEVEL_DEBUG),
 		mLogAlwaysFlush(true),
 		mEnabledLogTypesMask(255),
@@ -706,23 +712,22 @@ namespace LLError
 		commonInit(user_dir, app_dir, log_to_stderr);
 	}
 
-	void setPrintLocation(bool print)
+	void setFatalFunction(const FatalFunction& f)
 	{
 		SettingsConfigPtr s = Settings::getInstance()->getSettingsConfig();
-		s->mPrintLocation = print;
+		s->mCrashFunction = f;
 	}
 
-	void setFatalFunction(const FatalFunction& f)
+	FatalFunction getFatalFunction()
 	{
 		SettingsConfigPtr s = Settings::getInstance()->getSettingsConfig();
-		s->mCrashFunction = f;
+		return s->mCrashFunction;
 	}
 
-    FatalFunction getFatalFunction()
-    {
-		SettingsConfigPtr s = Settings::getInstance()->getSettingsConfig();
-        return s->mCrashFunction;
-    }
+	std::string getFatalMessage()
+	{
+		return Globals::getInstance()->mFatalMessage;
+	}
 
 	void setTimeFunction(TimeFunction f)
 	{
@@ -845,7 +850,6 @@ namespace LLError
 		s->mTagLevelMap.clear();
 		s->mUniqueLogMessages.clear();
 		
-		setPrintLocation(config["print-location"]);
 		setDefaultLevel(decodeLevel(config["default-level"]));
         if (config.has("log-always-flush"))
         {
@@ -876,11 +880,12 @@ namespace LLError
 namespace LLError
 {
 	Recorder::Recorder()
-	:	mWantsTime(false),
-		mWantsTags(false),
-		mWantsLevel(true),
-		mWantsLocation(false),
-		mWantsFunctionName(true)
+    	: mWantsTime(true)
+        , mWantsTags(true)
+        , mWantsLevel(true)
+        , mWantsLocation(true)
+        , mWantsFunctionName(true)
+        , mWantsMultiline(false)
 	{
 	}
 
@@ -917,6 +922,42 @@ namespace LLError
 		return mWantsFunctionName;
 	}
 
+	// virtual 
+	bool Recorder::wantsMultiline() 
+	{ 
+		return mWantsMultiline;
+	}
+
+    void Recorder::showTime(bool show)
+    {
+        mWantsTime = show;
+    }
+    
+    void Recorder::showTags(bool show)
+    {
+        mWantsTags = show;
+    }
+
+    void Recorder::showLevel(bool show)
+    {
+        mWantsLevel = show;
+    }
+
+    void Recorder::showLocation(bool show)
+    {
+        mWantsLocation = show;
+    }
+
+    void Recorder::showFunctionName(bool show)
+    {
+        mWantsFunctionName = show;
+    }
+
+    void Recorder::showMultiline(bool show)
+    {
+        mWantsMultiline = show;
+    }
+
 	void addRecorder(RecorderPtr recorder)
 	{
 		if (!recorder)
@@ -949,17 +990,15 @@ namespace LLError
 		s->mFileRecorder.reset();
 		s->mFileRecorderFileName.clear();
 		
-		if (file_name.empty())
+		if (!file_name.empty())
 		{
-			return;
-		}
-		
-		RecorderPtr recordToFile(new RecordToFile(file_name));
-		if (boost::dynamic_pointer_cast<RecordToFile>(recordToFile)->okay())
-		{
-			s->mFileRecorderFileName = file_name;
-			s->mFileRecorder = recordToFile;
-			addRecorder(recordToFile);
+            RecorderPtr recordToFile(new RecordToFile(file_name));
+            if (boost::dynamic_pointer_cast<RecordToFile>(recordToFile)->okay())
+            {
+                s->mFileRecorderFileName = file_name;
+                s->mFileRecorder = recordToFile;
+                addRecorder(recordToFile);
+            }
 		}
 	}
 	
@@ -970,14 +1009,12 @@ namespace LLError
 		removeRecorder(s->mFixedBufferRecorder);
 		s->mFixedBufferRecorder.reset();
 		
-		if (!fixedBuffer)
+		if (fixedBuffer)
 		{
-			return;
-		}
-		
-		RecorderPtr recordToFixedBuffer(new RecordToFixedBuffer(fixedBuffer));
-		s->mFixedBufferRecorder = recordToFixedBuffer;
-		addRecorder(recordToFixedBuffer);
+            RecorderPtr recordToFixedBuffer(new RecordToFixedBuffer(fixedBuffer));
+            s->mFixedBufferRecorder = recordToFixedBuffer;
+            addRecorder(recordToFixedBuffer);
+        }
 	}
 
 	std::string logFileName()
@@ -989,8 +1026,9 @@ namespace LLError
 
 namespace
 {
-    void addEscapedMessage(std::ostream& out, const std::string& message)
+    std::string escapedMessageLines(const std::string& message)
     {
+        std::ostringstream out;
         size_t written_out = 0;
         size_t all_content = message.length();
         size_t escape_char_index; // always relative to start of message
@@ -1026,13 +1064,16 @@ namespace
             // write whatever was left
             out << message.substr(written_out, std::string::npos);
         }
+        return out.str();
     }
 
-	void writeToRecorders(const LLError::CallSite& site, const std::string& escaped_message)
+	void writeToRecorders(const LLError::CallSite& site, const std::string& message)
 	{
 		LLError::ELevel level = site.mLevel;
 		LLError::SettingsConfigPtr s = LLError::Settings::getInstance()->getSettingsConfig();
-	
+
+        std::string escaped_message;
+        
 		for (Recorders::const_iterator i = s->mRecorders.begin();
 			i != s->mRecorders.end();
 			++i)
@@ -1064,7 +1105,7 @@ namespace
 			}
             message_stream << " ";
 
-            if (r->wantsLocation() || level == LLError::LEVEL_ERROR || s->mPrintLocation)
+            if (r->wantsLocation() || level == LLError::LEVEL_ERROR)
             {
                 message_stream << site.mLocationString;
             }
@@ -1076,7 +1117,18 @@ namespace
 			}
             message_stream << " : ";
 
-			message_stream << escaped_message;
+            if (r->wantsMultiline())
+            {
+                message_stream << message;
+            }
+            else
+            {
+                if (escaped_message.empty())
+                {
+                    escaped_message = escapedMessageLines(message);
+                }
+                message_stream << escaped_message;
+            }
 
 			r->recordMessage(level, message_stream.str());
 		}
@@ -1320,10 +1372,11 @@ namespace LLError
 			delete out;
 		}
 
-		std::ostringstream message_stream;
 
 		if (site.mPrintOnce)
 		{
+            std::ostringstream message_stream;
+
 			std::map<std::string, unsigned int>::iterator messageIter = s->mUniqueLogMessages.find(message);
 			if (messageIter != s->mUniqueLogMessages.end())
 			{
@@ -1343,15 +1396,19 @@ namespace LLError
 				message_stream << "ONCE: ";
 				s->mUniqueLogMessages[message] = 1;
 			}
+            message_stream << message;
+            message = message_stream.str();
 		}
 		
-		addEscapedMessage(message_stream, message);
+		writeToRecorders(site, message);
 
-		writeToRecorders(site, message_stream.str());
-		
-		if (site.mLevel == LEVEL_ERROR  &&  s->mCrashFunction)
+		if (site.mLevel == LEVEL_ERROR)
 		{
-			s->mCrashFunction(message_stream.str());
+			g->mFatalMessage = message;
+			if (s->mCrashFunction)
+			{
+				s->mCrashFunction(message);
+			}
 		}
 	}
 }
@@ -1656,3 +1713,4 @@ bool debugLoggingEnabled(const std::string& tag)
 }
 
 
+
diff --git a/indra/llcommon/llerrorcontrol.h b/indra/llcommon/llerrorcontrol.h
index 1730f0c640a66571760b167b410f6854acd407cf..276d22fc36122b1e831283be8563cf768b9cf9ca 100644
--- a/indra/llcommon/llerrorcontrol.h
+++ b/indra/llcommon/llerrorcontrol.h
@@ -106,6 +106,9 @@ namespace LLError
 	LL_COMMON_API FatalFunction getFatalFunction();
 		// Retrieve the previously-set FatalFunction
 
+	LL_COMMON_API std::string getFatalMessage();
+		// Retrieve the message last passed to FatalFunction, if any
+
 	/// temporarily override the FatalFunction for the duration of a
 	/// particular scope, e.g. for unit tests
 	class LL_COMMON_API OverrideFatalFunction
@@ -151,13 +154,22 @@ namespace LLError
 		bool wantsLevel();
 		bool wantsLocation(); 
 		bool wantsFunctionName();
+        bool wantsMultiline();
+
+		void showTime(bool show);
+		void showTags(bool show);
+		void showLevel(bool show);
+		void showLocation(bool show); 
+		void showFunctionName(bool show);
+		void showMultiline(bool show);
 
 	protected:
-		bool	mWantsTime,
-				mWantsTags,
-				mWantsLevel,
-				mWantsLocation,
-				mWantsFunctionName;
+		bool mWantsTime;
+        bool mWantsTags;
+        bool mWantsLevel;
+        bool mWantsLocation;
+        bool mWantsFunctionName;
+        bool mWantsMultiline;
 	};
 
 	typedef boost::shared_ptr<Recorder> RecorderPtr;
diff --git a/indra/llcommon/llevents.cpp b/indra/llcommon/llevents.cpp
index dce97b5411d8160474fd16f6de511b75a413c408..eedd8c92b5a45ba5b9d308b1078ad6d93d8c57a8 100644
--- a/indra/llcommon/llevents.cpp
+++ b/indra/llcommon/llevents.cpp
@@ -545,10 +545,8 @@ bool LLEventStream::post(const LLSD& event)
  *****************************************************************************/
 bool LLEventMailDrop::post(const LLSD& event)
 {
-    bool posted = false;
-    
-    if (!mSignal->empty())
-        posted = LLEventStream::post(event);
+    // forward the call to our base class
+    bool posted = LLEventStream::post(event);
     
     if (!posted)
     {   // if the event was not handled we will save it for later so that it can 
@@ -564,16 +562,25 @@ LLBoundListener LLEventMailDrop::listen_impl(const std::string& name,
                                     const NameList& after,
                                     const NameList& before)
 {
-    if (!mEventHistory.empty())
+    // Before actually connecting this listener for subsequent post() calls,
+    // first feed each of the saved events, in order, to the new listener.
+    // Remove any that this listener consumes -- Effective STL, Item 9.
+    for (auto hi(mEventHistory.begin()), hend(mEventHistory.end()); hi != hend; )
     {
-        if (listener(mEventHistory.front()))
+        if (listener(*hi))
         {
-            mEventHistory.pop_front();
+            // new listener consumed this event, erase it
+            hi = mEventHistory.erase(hi);
+        }
+        else
+        {
+            // listener did not consume this event, just move along
+            ++hi;
         }
     }
 
+    // let base class perform the actual connection
     return LLEventStream::listen_impl(name, listener, after, before);
-
 }
 
 
diff --git a/indra/llcommon/llevents.h b/indra/llcommon/llevents.h
index 1d51c660ed0d7990c63a4f93c875b2e61597a7f8..5d60c63810128446c02f4e6aeb0a1bf8ec185d7b 100644
--- a/indra/llcommon/llevents.h
+++ b/indra/llcommon/llevents.h
@@ -650,15 +650,21 @@ class LL_COMMON_API LLEventStream: public LLEventPump
  *   LLEventMailDrop
  *****************************************************************************/
 /**
- * LLEventMailDrop is a specialization of LLEventStream. Events are posted normally, 
- * however if no listeners return that they have handled the event it is placed in 
- * a queue. Subsequent attaching listeners will receive stored events from the queue 
- * until a listener indicates that the event has been handled.  In order to receive 
- * multiple events from a mail drop the listener must disconnect and reconnect.
+ * LLEventMailDrop is a specialization of LLEventStream. Events are posted
+ * normally, however if no listener returns that it has handled the event
+ * (returns true), it is placed in a queue. Subsequent attaching listeners
+ * will receive stored events from the queue until some listener indicates
+ * that the event has been handled.
+ *
+ * LLEventMailDrop completely decouples the timing of post() calls from
+ * listen() calls: every event posted to an LLEventMailDrop is eventually seen
+ * by all listeners, until some listener consumes it. The caveat is that each
+ * event *must* eventually reach a listener that will consume it, else the
+ * queue will grow to arbitrary length.
  * 
  * @NOTE: When using an LLEventMailDrop (or LLEventQueue) with a LLEventTimeout or
- * LLEventFilter attaching the filter downstream using Timeout's constructor will
- * cause the MailDrop to discharge any of it's stored events. The timeout should 
+ * LLEventFilter attaching the filter downstream, using Timeout's constructor will
+ * cause the MailDrop to discharge any of its stored events. The timeout should 
  * instead be connected upstream using its listen() method.  
  * See llcoro::suspendUntilEventOnWithTimeout() for an example.
  */
diff --git a/indra/llcommon/llfile.cpp b/indra/llcommon/llfile.cpp
index fc203f78e1aefc68f5db9840d6ada626e12cd69b..8355b1e797ea14946da3d3cc69b590f38fbbc8e2 100644
--- a/indra/llcommon/llfile.cpp
+++ b/indra/llcommon/llfile.cpp
@@ -30,6 +30,7 @@
 #if LL_WINDOWS
 #include "llwin32headerslean.h"
 #include <stdlib.h>                 // Windows errno
+#include <vector>
 #else
 #include <errno.h>
 #endif
@@ -134,8 +135,10 @@ int warnif(const std::string& desc, const std::string& filename, int rc, int acc
 		{
 			// Only do any of this stuff (before LL_ENDL) if it will be logged.
 			LL_DEBUGS("LLFile") << empty;
-			const char* TEMP = getenv("TEMP");
-			if (! TEMP)
+			// would be nice to use LLDir for this, but dependency goes the
+			// wrong way
+			const char* TEMP = LLFile::tmpdir();
+			if (! (TEMP && *TEMP))
 			{
 				LL_CONT << "No $TEMP, not running 'handle'";
 			}
@@ -341,17 +344,13 @@ const char *LLFile::tmpdir()
 #if LL_WINDOWS
 		sep = '\\';
 
-		DWORD len = GetTempPathW(0, L"");
-		llutf16string utf16path;
-		utf16path.resize(len + 1);
-		len = GetTempPathW(static_cast<DWORD>(utf16path.size()), &utf16path[0]);
-		utf8path = utf16str_to_utf8str(utf16path);
+		std::vector<wchar_t> utf16path(MAX_PATH + 1);
+		GetTempPathW(utf16path.size(), &utf16path[0]);
+		utf8path = ll_convert_wide_to_string(&utf16path[0]);
 #else
 		sep = '/';
 
-		char *env = getenv("TMPDIR");
-
-		utf8path = env ? env : "/tmp/";
+		utf8path = LLStringUtil::getenv("TMPDIR", "/tmp/");
 #endif
 		if (utf8path[utf8path.size() - 1] != sep)
 		{
diff --git a/indra/llcommon/llinitparam.h b/indra/llcommon/llinitparam.h
index f1f4226c409612b578803c7484ec817c03ae60da..7f5b9b4ac23e4e6afb5a236c034c0d82fc33b1a5 100644
--- a/indra/llcommon/llinitparam.h
+++ b/indra/llcommon/llinitparam.h
@@ -2115,6 +2115,9 @@ namespace LLInitParam
 			typedef typename super_t::iterator										iterator;
 			typedef typename super_t::const_iterator								const_iterator;
 
+			using super_t::operator();
+			using super_t::operator const container_t&;
+
 			explicit Multiple(const char* name = "")
 			:	super_t(DERIVED_BLOCK::getBlockDescriptor(), name, container_t(), &validate, RANGE::minCount, RANGE::maxCount)
 			{}
diff --git a/indra/llcommon/llleap.cpp b/indra/llcommon/llleap.cpp
index c87d2a3e588c87a6ee6a2e8a3dd808b1d540d116..cf8f8cc6a5ee9c8ca7c3168cbcc8f4ef6f69ed75 100644
--- a/indra/llcommon/llleap.cpp
+++ b/indra/llcommon/llleap.cpp
@@ -47,9 +47,9 @@ class LLLeapImpl: public LLLeap
     LOG_CLASS(LLLeap);
 public:
     // Called only by LLLeap::create()
-    LLLeapImpl(const std::string& desc, const std::vector<std::string>& plugin):
+    LLLeapImpl(const LLProcess::Params& cparams):
         // We might reassign mDesc in the constructor body if it's empty here.
-        mDesc(desc),
+        mDesc(cparams.desc),
         // We expect multiple LLLeapImpl instances. Definitely tweak
         // mDonePump's name for uniqueness.
         mDonePump("LLLeap", true),
@@ -67,17 +67,17 @@ class LLLeapImpl: public LLLeap
         // this class or method name.
         mListener(new LLLeapListener(boost::bind(&LLLeapImpl::connect, this, _1, _2)))
     {
-        // Rule out empty vector
-        if (plugin.empty())
+        // Rule out unpopulated Params block
+        if (! cparams.executable.isProvided())
         {
             LLTHROW(Error("no plugin command"));
         }
 
         // Don't leave desc empty either, but in this case, if we weren't
         // given one, we'll fake one.
-        if (desc.empty())
+        if (mDesc.empty())
         {
-            mDesc = LLProcess::basename(plugin[0]);
+            mDesc = LLProcess::basename(cparams.executable);
             // how about a toLower() variant that returns the transformed string?!
             std::string desclower(mDesc);
             LLStringUtil::toLower(desclower);
@@ -87,9 +87,9 @@ class LLLeapImpl: public LLLeap
             // notice Python specially: we provide Python LLSD serialization
             // support, so there's a pretty good reason to implement plugins
             // in that language.
-            if (plugin.size() >= 2 && (desclower == "python" || desclower == "python.exe"))
+            if (cparams.args.size() && (desclower == "python" || desclower == "python.exe"))
             {
-                mDesc = LLProcess::basename(plugin[1]);
+                mDesc = LLProcess::basename(cparams.args()[0]);
             }
         }
 
@@ -97,14 +97,10 @@ class LLLeapImpl: public LLLeap
         mDonePump.listen("LLLeap", boost::bind(&LLLeapImpl::bad_launch, this, _1));
 
         // Okay, launch child.
-        LLProcess::Params params;
+        // Get a modifiable copy of params block to set files and postend.
+        LLProcess::Params params(cparams);
+        // copy our deduced mDesc back into the params block
         params.desc = mDesc;
-        std::vector<std::string>::const_iterator pi(plugin.begin()), pend(plugin.end());
-        params.executable = *pi++;
-        for ( ; pi != pend; ++pi)
-        {
-            params.args.add(*pi);
-        }
         params.files.add(LLProcess::FileParam("pipe")); // stdin
         params.files.add(LLProcess::FileParam("pipe")); // stdout
         params.files.add(LLProcess::FileParam("pipe")); // stderr
@@ -429,17 +425,17 @@ class LLLeapImpl: public LLLeap
     boost::scoped_ptr<LLLeapListener> mListener;
 };
 
-// This must follow the declaration of LLLeapImpl, so it may as well be last.
-LLLeap* LLLeap::create(const std::string& desc, const std::vector<std::string>& plugin, bool exc)
+// These must follow the declaration of LLLeapImpl, so they may as well be last.
+LLLeap* LLLeap::create(const LLProcess::Params& params, bool exc)
 {
     // If caller is willing to permit exceptions, just instantiate.
     if (exc)
-        return new LLLeapImpl(desc, plugin);
+        return new LLLeapImpl(params);
 
     // Caller insists on suppressing LLLeap::Error. Very well, catch it.
     try
     {
-        return new LLLeapImpl(desc, plugin);
+        return new LLLeapImpl(params);
     }
     catch (const LLLeap::Error&)
     {
@@ -447,6 +443,23 @@ LLLeap* LLLeap::create(const std::string& desc, const std::vector<std::string>&
     }
 }
 
+LLLeap* LLLeap::create(const std::string& desc, const std::vector<std::string>& plugin, bool exc)
+{
+    LLProcess::Params params;
+    params.desc = desc;
+    std::vector<std::string>::const_iterator pi(plugin.begin()), pend(plugin.end());
+    // could validate here, but let's rely on LLLeapImpl's constructor
+    if (pi != pend)
+    {
+        params.executable = *pi++;
+    }
+    for ( ; pi != pend; ++pi)
+    {
+        params.args.add(*pi);
+    }
+    return create(params, exc);
+}
+
 LLLeap* LLLeap::create(const std::string& desc, const std::string& plugin, bool exc)
 {
     // Use LLStringUtil::getTokens() to parse the command line
diff --git a/indra/llcommon/llleap.h b/indra/llcommon/llleap.h
index 8aac8a64c578e243f2ec74bbfaa434e7d301812c..7cecdf2f8f6821e4a7e0c844c60d92ef446c18ae 100644
--- a/indra/llcommon/llleap.h
+++ b/indra/llcommon/llleap.h
@@ -14,6 +14,7 @@
 
 #include "llinstancetracker.h"
 #include "llexception.h"
+#include "llprocess.h"
 #include <string>
 #include <vector>
 
@@ -61,6 +62,19 @@ class LL_COMMON_API LLLeap: public LLInstanceTracker<LLLeap>
     static LLLeap* create(const std::string& desc, const std::string& plugin,
                           bool exc=true);
 
+    /**
+     * Pass an LLProcess::Params instance to specify desc, executable, args et al.
+     *
+     * Note that files and postend are set implicitly; any values you set in
+     * those fields will be disregarded.
+     *
+     * Pass exc=false to suppress LLLeap::Error exception. Obviously in that
+     * case the caller cannot discover the nature of the error, merely that an
+     * error of some kind occurred (because create() returned NULL). Either
+     * way, the error is logged.
+     */
+    static LLLeap* create(const LLProcess::Params& params, bool exc=true);
+
     /**
      * Exception thrown for invalid create() arguments, e.g. no plugin
      * program. This is more resiliant than an LL_ERRS failure, because the
diff --git a/indra/llcommon/llpreprocessor.h b/indra/llcommon/llpreprocessor.h
index 2879038c3652c3e54d5a9aa4e6d6559f700c439a..e8f99814375fa6f2869635554f22c738025df550 100644
--- a/indra/llcommon/llpreprocessor.h
+++ b/indra/llcommon/llpreprocessor.h
@@ -101,6 +101,9 @@
 
 #endif
 
+// Although thread_local is now a standard storage class, we can't just
+// #define LL_THREAD_LOCAL as thread_local because the *usage* is different.
+// We'll have to take the time to change LL_THREAD_LOCAL declarations by hand.
 #if LL_WINDOWS
 # define LL_THREAD_LOCAL __declspec(thread)
 #else
@@ -177,6 +180,24 @@
 #define LL_DLLIMPORT
 #endif // LL_WINDOWS
 
+#if ! defined(LL_WINDOWS)
+#define LL_WCHAR_T_NATIVE 1
+#else  // LL_WINDOWS
+// https://docs.microsoft.com/en-us/cpp/preprocessor/predefined-macros
+// _WCHAR_T_DEFINED is defined if wchar_t is provided at all.
+// Specifically, it has value 1 if wchar_t is an intrinsic type, else empty.
+// _NATIVE_WCHAR_T_DEFINED has value 1 if wchar_t is intrinsic, else undefined.
+// For years we have compiled with /Zc:wchar_t-, meaning that wchar_t is a
+// typedef for unsigned short (in stddef.h). Lore has it that one of our
+// proprietary binary-only libraries has traditionally been built that way and
+// therefore EVERYTHING ELSE requires it. Therefore, in a typical Linden
+// Windows build, _WCHAR_T_DEFINED is defined but empty, while
+// _NATIVE_WCHAR_T_DEFINED is undefined.
+# if defined(_NATIVE_WCHAR_T_DEFINED)
+#  define LL_WCHAR_T_NATIVE 1
+# endif // _NATIVE_WCHAR_T_DEFINED
+#endif // LL_WINDOWS
+
 #if LL_COMMON_LINK_SHARED
 // CMake automagically defines llcommon_EXPORTS only when building llcommon
 // sources, and only when llcommon is a shared library (i.e. when
@@ -198,6 +219,8 @@
 
 #define LL_TO_STRING_HELPER(x) #x
 #define LL_TO_STRING(x) LL_TO_STRING_HELPER(x)
+#define LL_TO_WSTRING_HELPER(x) L#x
+#define LL_TO_WSTRING(x) LL_TO_WSTRING_HELPER(x)
 #define LL_FILE_LINENO_MSG(msg) __FILE__ "(" LL_TO_STRING(__LINE__) ") : " msg
 #define LL_GLUE_IMPL(x, y) x##y
 #define LL_GLUE_TOKENS(x, y) LL_GLUE_IMPL(x, y)
diff --git a/indra/llcommon/llprocess.cpp b/indra/llcommon/llprocess.cpp
index 5753efdc592d5081853c527ce7706ac01c77c3c2..1fa53f322b1da8ca17b8bffbf11a5116349b8b8f 100644
--- a/indra/llcommon/llprocess.cpp
+++ b/indra/llcommon/llprocess.cpp
@@ -1205,30 +1205,9 @@ static LLProcess::Status interpret_status(int status)
 /// GetLastError()/FormatMessage() boilerplate
 static std::string WindowsErrorString(const std::string& operation)
 {
-	int result = GetLastError();
-
-	LPTSTR error_str = 0;
-	if (FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
-					   NULL,
-					   result,
-					   0,
-					   (LPTSTR)&error_str,
-					   0,
-					   NULL)
-		!= 0) 
-	{
-		// convert from wide-char string to multi-byte string
-		char message[256];
-		wcstombs(message, error_str, sizeof(message));
-		message[sizeof(message)-1] = 0;
-		LocalFree(error_str);
-		// convert to std::string to trim trailing whitespace
-		std::string mbsstr(message);
-		mbsstr.erase(mbsstr.find_last_not_of(" \t\r\n"));
-		return STRINGIZE(operation << " failed (" << result << "): " << mbsstr);
-	}
-	return STRINGIZE(operation << " failed (" << result
-					 << "), but FormatMessage() did not explain");
+	auto result = GetLastError();
+	return STRINGIZE(operation << " failed (" << result << "): "
+					 << windows_message<std::string>(result));
 }
 
 /*****************************************************************************
diff --git a/indra/llcommon/llstring.cpp b/indra/llcommon/llstring.cpp
index 9a02fecd7261799bcee325b3678cb149557a9f24..0174c411b4bfa062f0450817082df67d21b41cad 100644
--- a/indra/llcommon/llstring.cpp
+++ b/indra/llcommon/llstring.cpp
@@ -30,6 +30,7 @@
 #include "llerror.h"
 #include "llfasttimer.h"
 #include "llsd.h"
+#include <vector>
 
 #if LL_WINDOWS
 #include "llwin32headerslean.h"
@@ -672,6 +673,11 @@ namespace snprintf_hack
 	}
 }
 
+std::string ll_convert_wide_to_string(const wchar_t* in)
+{
+	return ll_convert_wide_to_string(in, CP_UTF8);
+}
+
 std::string ll_convert_wide_to_string(const wchar_t* in, unsigned int code_page)
 {
 	std::string out;
@@ -709,7 +715,12 @@ std::string ll_convert_wide_to_string(const wchar_t* in, unsigned int code_page)
 	return out;
 }
 
-wchar_t* ll_convert_string_to_wide(const std::string& in, unsigned int code_page)
+std::wstring ll_convert_string_to_wide(const std::string& in)
+{
+	return ll_convert_string_to_wide(in, CP_UTF8);
+}
+
+std::wstring ll_convert_string_to_wide(const std::string& in, unsigned int code_page)
 {
 	// From review:
 	// We can preallocate a wide char buffer that is the same length (in wchar_t elements) as the utf8 input,
@@ -719,28 +730,148 @@ wchar_t* ll_convert_string_to_wide(const std::string& in, unsigned int code_page
 	// but we *are* seeing string operations taking a bunch of time, especially when constructing widgets.
 //	int output_str_len = MultiByteToWideChar(code_page, 0, in.c_str(), in.length(), NULL, 0);
 
-	// reserve place to NULL terminator
-	int output_str_len = in.length();
-	wchar_t* w_out = new wchar_t[output_str_len + 1];
+	// reserve an output buffer that will be destroyed on exit, with a place
+	// to put NULL terminator
+	std::vector<wchar_t> w_out(in.length() + 1);
 
-	memset(w_out, 0, output_str_len + 1);
-	int real_output_str_len = MultiByteToWideChar (code_page, 0, in.c_str(), in.length(), w_out, output_str_len);
+	memset(&w_out[0], 0, w_out.size());
+	int real_output_str_len = MultiByteToWideChar(code_page, 0, in.c_str(), in.length(),
+												  &w_out[0], w_out.size() - 1);
 
 	//looks like MultiByteToWideChar didn't add null terminator to converted string, see EXT-4858.
 	w_out[real_output_str_len] = 0;
 
-	return w_out;
+	// construct string<wchar_t> from our temporary output buffer
+	return {&w_out[0]};
+}
+
+LLWString ll_convert_wide_to_wstring(const std::wstring& in)
+{
+    // This function, like its converse, is a placeholder, encapsulating a
+    // guilty little hack: the only "official" way nat has found to convert
+    // between std::wstring (16 bits on Windows) and LLWString (UTF-32) is
+    // by using iconv, which we've avoided so far. It kinda sorta works to
+    // just copy individual characters...
+    // The point is that if/when we DO introduce some more official way to
+    // perform such conversions, we should only have to call it here.
+    return { in.begin(), in.end() };
+}
+
+std::wstring ll_convert_wstring_to_wide(const LLWString& in)
+{
+    // See comments in ll_convert_wide_to_wstring()
+    return { in.begin(), in.end() };
 }
 
 std::string ll_convert_string_to_utf8_string(const std::string& in)
 {
-	wchar_t* w_mesg = ll_convert_string_to_wide(in, CP_ACP);
-	std::string out_utf8(ll_convert_wide_to_string(w_mesg, CP_UTF8));
-	delete[] w_mesg;
+	auto w_mesg = ll_convert_string_to_wide(in, CP_ACP);
+	std::string out_utf8(ll_convert_wide_to_string(w_mesg.c_str(), CP_UTF8));
 
 	return out_utf8;
 }
-#endif // LL_WINDOWS
+
+namespace
+{
+
+void HeapFree_deleter(void* ptr)
+{
+    // instead of LocalFree(), per https://stackoverflow.com/a/31541205
+    HeapFree(GetProcessHeap(), NULL, ptr);
+}
+
+} // anonymous namespace
+
+template<>
+std::wstring windows_message<std::wstring>(DWORD error)
+{
+    // derived from https://stackoverflow.com/a/455533
+    wchar_t* rawptr = nullptr;
+    auto okay = FormatMessageW(
+        // use system message tables for GetLastError() codes
+        FORMAT_MESSAGE_FROM_SYSTEM |
+        // internally allocate buffer and return its pointer
+        FORMAT_MESSAGE_ALLOCATE_BUFFER |
+        // you cannot pass insertion parameters (thanks Gandalf)
+        FORMAT_MESSAGE_IGNORE_INSERTS |
+        // ignore line breaks in message definition text
+        FORMAT_MESSAGE_MAX_WIDTH_MASK,
+        NULL,                       // lpSource, unused with FORMAT_MESSAGE_FROM_SYSTEM
+        error,                      // dwMessageId
+        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // dwLanguageId
+        (LPWSTR)&rawptr,         // lpBuffer: force-cast wchar_t** to wchar_t*
+        0,                // nSize, unused with FORMAT_MESSAGE_ALLOCATE_BUFFER
+        NULL);            // Arguments, unused
+
+    // make a unique_ptr from rawptr so it gets cleaned up properly
+    std::unique_ptr<wchar_t, void(*)(void*)> bufferptr(rawptr, HeapFree_deleter);
+
+    if (okay && bufferptr)
+    {
+        // got the message, return it ('okay' is length in characters)
+        return { bufferptr.get(), okay };
+    }
+
+    // did not get the message, synthesize one
+    auto format_message_error = GetLastError();
+    std::wostringstream out;
+    out << L"GetLastError() " << error << L" (FormatMessageW() failed with "
+        << format_message_error << L")";
+    return out.str();
+}
+
+boost::optional<std::wstring> llstring_getoptenv(const std::string& key)
+{
+    auto wkey = ll_convert_string_to_wide(key);
+    // Take a wild guess as to how big the buffer should be.
+    std::vector<wchar_t> buffer(1024);
+    auto n = GetEnvironmentVariableW(wkey.c_str(), &buffer[0], buffer.size());
+    // If our initial guess was too short, n will indicate the size (in
+    // wchar_t's) that buffer should have been, including the terminating nul.
+    if (n > (buffer.size() - 1))
+    {
+        // make it big enough
+        buffer.resize(n);
+        // and try again
+        n = GetEnvironmentVariableW(wkey.c_str(), &buffer[0], buffer.size());
+    }
+    // did that (ultimately) succeed?
+    if (n)
+    {
+        // great, return populated boost::optional
+        return boost::optional<std::wstring>(&buffer[0]);
+    }
+
+    // not successful
+    auto last_error = GetLastError();
+    // Don't bother warning for NOT_FOUND; that's an expected case
+    if (last_error != ERROR_ENVVAR_NOT_FOUND)
+    {
+        LL_WARNS() << "GetEnvironmentVariableW('" << key << "') failed: "
+                   << windows_message<std::string>(last_error) << LL_ENDL;
+    }
+    // return empty boost::optional
+    return {};
+}
+
+#else  // ! LL_WINDOWS
+
+boost::optional<std::string> llstring_getoptenv(const std::string& key)
+{
+    auto found = getenv(key.c_str());
+    if (found)
+    {
+        // return populated boost::optional
+        return boost::optional<std::string>(found);
+    }
+    else
+    {
+        // return empty boost::optional
+        return {};
+    }
+}
+
+#endif // ! LL_WINDOWS
 
 long LLStringOps::sPacificTimeOffset = 0;
 long LLStringOps::sLocalTimeOffset = 0;
diff --git a/indra/llcommon/llstring.h b/indra/llcommon/llstring.h
index 68ee9db46b9083e5cdb339bc19b1e93ea0c96d92..30bec3a6f89ad30985f852ce57d85ecacdfc41d9 100644
--- a/indra/llcommon/llstring.h
+++ b/indra/llcommon/llstring.h
@@ -27,6 +27,7 @@
 #ifndef LL_LLSTRING_H
 #define LL_LLSTRING_H
 
+#include <boost/optional/optional.hpp>
 #include <string>
 #include <cstdio>
 //#include <locale>
@@ -337,6 +338,19 @@ class LLStringUtilBase
 		const string_type& string,
 		const string_type& substr);
 
+	/**
+	 * get environment string value with proper Unicode handling
+	 * (key is always UTF-8)
+	 * detect absence by return value == dflt
+	 */
+	static string_type getenv(const std::string& key, const string_type& dflt="");
+	/**
+	 * get optional environment string value with proper Unicode handling
+	 * (key is always UTF-8)
+	 * detect absence by (! return value)
+	 */
+	static boost::optional<string_type> getoptenv(const std::string& key);
+
 	static void	addCRLF(string_type& string);
 	static void	removeCRLF(string_type& string);
 	static void removeWindowsCR(string_type& string);
@@ -496,6 +510,37 @@ LL_COMMON_API bool iswindividual(llwchar elem);
  * Unicode support
  */
 
+/// generic conversion aliases
+template<typename TO, typename FROM, typename Enable=void>
+struct ll_convert_impl
+{
+    // Don't even provide a generic implementation. We specialize for every
+    // combination we do support.
+    TO operator()(const FROM& in) const;
+};
+
+// Use a function template to get the nice ll_convert<TO>(from_value) API.
+template<typename TO, typename FROM>
+TO ll_convert(const FROM& in)
+{
+    return ll_convert_impl<TO, FROM>()(in);
+}
+
+// degenerate case
+template<typename T>
+struct ll_convert_impl<T, T>
+{
+    T operator()(const T& in) const { return in; }
+};
+
+// specialize ll_convert_impl<TO, FROM> to return EXPR
+#define ll_convert_alias(TO, FROM, EXPR)                    \
+template<>                                                  \
+struct ll_convert_impl<TO, FROM>                            \
+{                                                           \
+    TO operator()(const FROM& in) const { return EXPR; }    \
+}
+
 // Make the incoming string a utf8 string. Replaces any unknown glyph
 // with the UNKNOWN_CHARACTER. Once any unknown glyph is found, the rest
 // of the data may not be recovered.
@@ -503,30 +548,88 @@ LL_COMMON_API std::string rawstr_to_utf8(const std::string& raw);
 
 //
 // We should never use UTF16 except when communicating with Win32!
+// https://docs.microsoft.com/en-us/cpp/cpp/char-wchar-t-char16-t-char32-t
+// nat 2018-12-14: I consider the whole llutf16string thing a mistake, because
+// the Windows APIs we want to call are all defined in terms of wchar_t*
+// (or worse, LPCTSTR).
+// https://docs.microsoft.com/en-us/windows/desktop/winprog/windows-data-types
+
+// While there is no point coding for an ASCII-only world (! defined(UNICODE)),
+// use of U16 and llutf16string for Windows APIs locks in /Zc:wchar_t-. Going
+// forward, we should code in terms of wchar_t and std::wstring so as to
+// support either setting of /Zc:wchar_t.
+
+// The first link above states that char can be used to hold ASCII or any
+// multi-byte character set, and distinguishes wchar_t (UTF-16LE), char16_t
+// (UTF-16) and char32_t (UTF-32). Nonetheless, within this code base:
+// * char and std::string always hold UTF-8 (of which ASCII is a subset). It
+//   is a BUG if they are used to pass strings in any other multi-byte
+//   encoding.
+// * wchar_t and std::wstring should be our interface to Windows wide-string
+//   APIs, and therefore hold UTF-16LE.
+// * U16 and llutf16string are the previous but DEPRECATED UTF-16LE type. Do
+//   not introduce new uses of U16 or llutf16string for string data.
+// * llwchar and LLWString hold UTF-32 strings.
+// * Do not introduce char16_t or std::u16string.
+// * Do not introduce char32_t or std::u32string.
 //
+// This typedef may or may not be identical to std::wstring, depending on
+// LL_WCHAR_T_NATIVE.
 typedef std::basic_string<U16> llutf16string;
 
+#if ! defined(LL_WCHAR_T_NATIVE)
+// wchar_t is identical to U16, and std::wstring is identical to llutf16string.
+// Defining an ll_convert alias involving llutf16string would collide with the
+// comparable preferred alias involving std::wstring. (In this scenario, if
+// you pass llutf16string, it will engage the std::wstring specialization.)
+#define ll_convert_u16_alias(TO, FROM, EXPR) // nothing
+#else  // defined(LL_WCHAR_T_NATIVE)
+// wchar_t is a distinct native type, so llutf16string is also a distinct
+// type, and there IS a point to converting separately to/from llutf16string.
+// (But why? Windows APIs are still defined in terms of wchar_t, and
+// in this scenario llutf16string won't work for them!)
+#define ll_convert_u16_alias(TO, FROM, EXPR) ll_convert_alias(TO, FROM, EXPR)
+
+#if LL_WINDOWS
+// LL_WCHAR_T_NATIVE is defined on non-Windows systems because, in fact,
+// wchar_t is native. Everywhere but Windows, we use it for llwchar (see
+// stdtypes.h). That makes LLWString identical to std::wstring, so these
+// aliases for std::wstring would collide with those for LLWString. Only
+// define on Windows, where converting between std::wstring and llutf16string
+// means copying chars.
+ll_convert_alias(llutf16string, std::wstring, llutf16string(in.begin(), in.end()));
+ll_convert_alias(std::wstring, llutf16string,  std::wstring(in.begin(), in.end()));
+#endif // LL_WINDOWS
+#endif // defined(LL_WCHAR_T_NATIVE)
+
 LL_COMMON_API LLWString utf16str_to_wstring(const llutf16string &utf16str, S32 len);
 LL_COMMON_API LLWString utf16str_to_wstring(const llutf16string &utf16str);
+ll_convert_u16_alias(LLWString, llutf16string, utf16str_to_wstring(in));
 
 LL_COMMON_API llutf16string wstring_to_utf16str(const LLWString &utf32str, S32 len);
 LL_COMMON_API llutf16string wstring_to_utf16str(const LLWString &utf32str);
+ll_convert_u16_alias(llutf16string, LLWString, wstring_to_utf16str(in));
 
 LL_COMMON_API llutf16string utf8str_to_utf16str ( const std::string& utf8str, S32 len);
 LL_COMMON_API llutf16string utf8str_to_utf16str ( const std::string& utf8str );
+ll_convert_u16_alias(llutf16string, std::string, utf8str_to_utf16str(in));
 
 LL_COMMON_API LLWString utf8str_to_wstring(const std::string &utf8str, S32 len);
 LL_COMMON_API LLWString utf8str_to_wstring(const std::string &utf8str);
 // Same function, better name. JC
 inline LLWString utf8string_to_wstring(const std::string& utf8_string) { return utf8str_to_wstring(utf8_string); }
+// best name of all
+ll_convert_alias(LLWString, std::string, utf8string_to_wstring(in));
 
 //
 LL_COMMON_API S32 wchar_to_utf8chars(llwchar inchar, char* outchars);
 
 LL_COMMON_API std::string wstring_to_utf8str(const LLWString &utf32str, S32 len);
 LL_COMMON_API std::string wstring_to_utf8str(const LLWString &utf32str);
+ll_convert_alias(std::string, LLWString, wstring_to_utf8str(in));
 LL_COMMON_API std::string utf16str_to_utf8str(const llutf16string &utf16str, S32 len);
 LL_COMMON_API std::string utf16str_to_utf8str(const llutf16string &utf16str);
+ll_convert_u16_alias(std::string, llutf16string, utf16str_to_utf8str(in));
 
 #if LL_WINDOWS
 inline std::string wstring_to_utf8str(const llutf16string &utf16str) { return utf16str_to_utf8str(utf16str);}
@@ -635,22 +738,77 @@ using snprintf_hack::snprintf;
  * This replaces the unsafe W2A macro from ATL.
  */
 LL_COMMON_API std::string ll_convert_wide_to_string(const wchar_t* in, unsigned int code_page);
+LL_COMMON_API std::string ll_convert_wide_to_string(const wchar_t* in); // default CP_UTF8
+inline std::string ll_convert_wide_to_string(const std::wstring& in, unsigned int code_page)
+{
+    return ll_convert_wide_to_string(in.c_str(), code_page);
+}
+inline std::string ll_convert_wide_to_string(const std::wstring& in)
+{
+    return ll_convert_wide_to_string(in.c_str());
+}
+ll_convert_alias(std::string, std::wstring, ll_convert_wide_to_string(in));
 
 /**
  * Converts a string to wide string.
- *
- * It will allocate memory for result string with "new []". Don't forget to release it with "delete []".
  */
-LL_COMMON_API wchar_t* ll_convert_string_to_wide(const std::string& in, unsigned int code_page);
+LL_COMMON_API std::wstring ll_convert_string_to_wide(const std::string& in,
+                                                     unsigned int code_page);
+LL_COMMON_API std::wstring ll_convert_string_to_wide(const std::string& in);
+                                                     // default CP_UTF8
+ll_convert_alias(std::wstring, std::string, ll_convert_string_to_wide(in));
 
 /**
- * Converts incoming string into urf8 string
+ * Convert a Windows wide string to our LLWString
+ */
+LL_COMMON_API LLWString ll_convert_wide_to_wstring(const std::wstring& in);
+ll_convert_alias(LLWString, std::wstring, ll_convert_wide_to_wstring(in));
+
+/**
+ * Convert LLWString to Windows wide string
+ */
+LL_COMMON_API std::wstring ll_convert_wstring_to_wide(const LLWString& in);
+ll_convert_alias(std::wstring, LLWString, ll_convert_wstring_to_wide(in));
+
+/**
+ * Converts incoming string into utf8 string
  *
  */
 LL_COMMON_API std::string ll_convert_string_to_utf8_string(const std::string& in);
 
+/// Get Windows message string for passed GetLastError() code
+// VS 2013 doesn't let us forward-declare this template, which is what we
+// started with, so the implementation could reference the specialization we
+// haven't yet declared. Somewhat weirdly, just stating the generic
+// implementation in terms of the specialization works, even in this order...
+
+// the general case is just a conversion from the sole implementation
+// Microsoft says DWORD is a typedef for unsigned long
+// https://docs.microsoft.com/en-us/windows/desktop/winprog/windows-data-types
+// so rather than drag windows.h into everybody's include space...
+template<typename STRING>
+STRING windows_message(unsigned long error)
+{
+    return ll_convert<STRING>(windows_message<std::wstring>(error));
+}
+
+/// There's only one real implementation
+template<>
+LL_COMMON_API std::wstring windows_message<std::wstring>(unsigned long error);
+
+/// Get Windows message string, implicitly calling GetLastError()
+template<typename STRING>
+STRING windows_message() { return windows_message<STRING>(GetLastError()); }
+
 //@}
-#endif // LL_WINDOWS
+
+LL_COMMON_API boost::optional<std::wstring> llstring_getoptenv(const std::string& key);
+
+#else // ! LL_WINDOWS
+
+LL_COMMON_API boost::optional<std::string>  llstring_getoptenv(const std::string& key);
+
+#endif // ! LL_WINDOWS
 
 /**
  * Many of the 'strip' and 'replace' methods of LLStringUtilBase need
@@ -1593,6 +1751,37 @@ bool LLStringUtilBase<T>::endsWith(
 	return (idx == (string.size() - substr.size()));
 }
 
+// static
+template<class T>
+auto LLStringUtilBase<T>::getoptenv(const std::string& key) -> boost::optional<string_type>
+{
+    auto found(llstring_getoptenv(key));
+    if (found)
+    {
+        // return populated boost::optional
+        return { ll_convert<string_type>(*found) };
+    }
+    else
+    {
+        // empty boost::optional
+        return {};
+    }
+}
+
+// static
+template<class T>
+auto LLStringUtilBase<T>::getenv(const std::string& key, const string_type& dflt) -> string_type
+{
+    auto found(getoptenv(key));
+    if (found)
+    {
+        return *found;
+    }
+    else
+    {
+        return dflt;
+    }
+}
 
 template<class T> 
 BOOL LLStringUtilBase<T>::convertToBOOL(const string_type& string, BOOL& value)
diff --git a/indra/llcommon/stdtypes.h b/indra/llcommon/stdtypes.h
index bf3f3f9ee832c1609c14536ad2bf983d6def91fe..6c9871e76c9fd5ed45a9435b1faaf52f89bb877a 100644
--- a/indra/llcommon/stdtypes.h
+++ b/indra/llcommon/stdtypes.h
@@ -37,7 +37,12 @@ typedef signed int			S32;
 typedef unsigned int			U32;
 
 #if LL_WINDOWS
-// Windows wchar_t is 16-bit
+// https://docs.microsoft.com/en-us/cpp/build/reference/zc-wchar-t-wchar-t-is-native-type
+// https://docs.microsoft.com/en-us/cpp/cpp/fundamental-types-cpp
+// Windows wchar_t is 16-bit, whichever way /Zc:wchar_t is set. In effect,
+// Windows wchar_t is always a typedef, either for unsigned short or __wchar_t.
+// (__wchar_t, available either way, is Microsoft's native 2-byte wchar_t type.)
+// In any case, llwchar should be a UTF-32 type.
 typedef U32				llwchar;
 #else
 typedef wchar_t				llwchar;
diff --git a/indra/llcommon/stringize.h b/indra/llcommon/stringize.h
index a5a90d72973f291714b8153e5ea40a2ee0c3b0db..38dd198ad319760c3be12230b1dfe2975421a9d9 100644
--- a/indra/llcommon/stringize.h
+++ b/indra/llcommon/stringize.h
@@ -30,7 +30,6 @@
 #define LL_STRINGIZE_H
 
 #include <sstream>
-#include <boost/phoenix/phoenix.hpp>
 #include <llstring.h>
 
 /**
@@ -53,12 +52,7 @@ std::basic_string<CHARTYPE> gstringize(const T& item)
  */
 inline std::string stringize(const std::wstring& item)
 {
-    LL_WARNS() << "WARNING:  Possible narrowing" << LL_ENDL;
-    
-    std::string s;
-    
-    s = wstring_to_utf8str(item);
-    return gstringize<char>(s);
+    return wstring_to_utf8str(item);
 }
 
 /**
@@ -76,7 +70,10 @@ std::string stringize(const T& item)
  */
 inline std::wstring wstringize(const std::string& item)
 {
-    return gstringize<wchar_t>(item.c_str());
+    // utf8str_to_wstring() returns LLWString, which isn't necessarily the
+    // same as std::wstring
+    LLWString s(utf8str_to_wstring(item));
+    return std::wstring(s.begin(), s.end());
 }
 
 /**
@@ -91,10 +88,10 @@ std::wstring wstringize(const T& item)
 /**
  * stringize_f(functor)
  */
-template <typename Functor>
-std::string stringize_f(Functor const & f)
+template <typename CHARTYPE, typename Functor>
+std::basic_string<CHARTYPE> stringize_f(Functor const & f)
 {
-    std::ostringstream out;
+    std::basic_ostringstream<CHARTYPE> out;
     f(out);
     return out.str();
 }
@@ -108,31 +105,37 @@ std::string stringize_f(Functor const & f)
  * return out.str();
  * @endcode
  */
-#define STRINGIZE(EXPRESSION) (stringize_f(boost::phoenix::placeholders::arg1 << EXPRESSION))
+#define STRINGIZE(EXPRESSION) (stringize_f<char>([&](std::ostream& out){ out << EXPRESSION; }))
 
+/**
+ * WSTRINGIZE() is the wstring equivalent of STRINGIZE()
+ */
+#define WSTRINGIZE(EXPRESSION) (stringize_f<wchar_t>([&](std::wostream& out){ out << EXPRESSION; }))
 
 /**
  * destringize(str)
  * defined for symmetry with stringize
- * *NOTE - this has distinct behavior from boost::lexical_cast<T> regarding
+ * @NOTE - this has distinct behavior from boost::lexical_cast<T> regarding
  * leading/trailing whitespace and handling of bad_lexical_cast exceptions
+ * @NOTE - no need for dewstringize(), since passing std::wstring will Do The
+ * Right Thing
  */
-template <typename T>
-T destringize(std::string const & str)
+template <typename T, typename CHARTYPE>
+T destringize(std::basic_string<CHARTYPE> const & str)
 {
-	T val;
-    std::istringstream in(str);
-	in >> val;
+    T val;
+    std::basic_istringstream<CHARTYPE> in(str);
+    in >> val;
     return val;
 }
 
 /**
  * destringize_f(str, functor)
  */
-template <typename Functor>
-void destringize_f(std::string const & str, Functor const & f)
+template <typename CHARTYPE, typename Functor>
+void destringize_f(std::basic_string<CHARTYPE> const & str, Functor const & f)
 {
-    std::istringstream in(str);
+    std::basic_istringstream<CHARTYPE> in(str);
     f(in);
 }
 
@@ -143,8 +146,11 @@ void destringize_f(std::string const & str, Functor const & f)
  * std::istringstream in(str);
  * in >> item1 >> item2 >> item3 ... ;
  * @endcode
+ * @NOTE - once we get generic lambdas, we shouldn't need DEWSTRINGIZE() any
+ * more since DESTRINGIZE() should do the right thing with a std::wstring. But
+ * until then, the lambda we pass must accept the right std::basic_istream.
  */
-#define DESTRINGIZE(STR, EXPRESSION) (destringize_f((STR), (boost::phoenix::placeholders::arg1 >> EXPRESSION)))
-
+#define DESTRINGIZE(STR, EXPRESSION) (destringize_f((STR), [&](std::istream& in){in >> EXPRESSION;}))
+#define DEWSTRINGIZE(STR, EXPRESSION) (destringize_f((STR), [&](std::wistream& in){in >> EXPRESSION;}))
 
 #endif /* ! defined(LL_STRINGIZE_H) */
diff --git a/indra/llcommon/tests/llerror_test.cpp b/indra/llcommon/tests/llerror_test.cpp
index ce0dbce07596088c9a98e7eec1adc405e0c279ce..8e1f4c14accc5236dd50b63dc91656c45256b148 100644
--- a/indra/llcommon/tests/llerror_test.cpp
+++ b/indra/llcommon/tests/llerror_test.cpp
@@ -78,8 +78,12 @@ namespace tut
 	class TestRecorder : public LLError::Recorder
 	{
 	public:
-		TestRecorder() { mWantsTime = false; mWantsTags = true; }
-		virtual ~TestRecorder() {  }
+		TestRecorder()
+            {
+                showTime(false);
+            }
+		virtual ~TestRecorder()
+            {}
 
 		virtual void recordMessage(LLError::ELevel level,
 						   const std::string& message)
@@ -90,8 +94,6 @@ namespace tut
 		int countMessages()			{ return (int) mMessages.size(); }
 		void clearMessages()		{ mMessages.clear(); }
 
-		void setWantsTime(bool t)	{ mWantsTime = t; }
-
 		std::string message(int n)
 		{
 			std::ostringstream test_name;
@@ -139,9 +141,14 @@ namespace tut
 		}
 
 		void setWantsTime(bool t)
-		{
-			boost::dynamic_pointer_cast<TestRecorder>(mRecorder)->setWantsTime(t);
-		}
+            {
+                boost::dynamic_pointer_cast<TestRecorder>(mRecorder)->showTime(t);
+            }
+
+		void setWantsMultiline(bool t)
+            {
+                boost::dynamic_pointer_cast<TestRecorder>(mRecorder)->showMultiline(t);
+            }
 
 		std::string message(int n)
 		{
@@ -378,27 +385,6 @@ namespace
 	}
 }
 
-namespace tut
-{
-	template<> template<>
-	void ErrorTestObject::test<5>()
-		// file and line information in log messages
-	{
-		std::string location = writeReturningLocation();
-			// expecting default to not print location information
-
-		LLError::setPrintLocation(true);
-		writeReturningLocation();
-
-		LLError::setPrintLocation(false);
-		writeReturningLocation();
-
-		ensure_message_does_not_contain(0, location);
-		ensure_message_field_equals(1, LOCATION_FIELD, location);
-		ensure_message_does_not_contain(2, location);
-	}
-}
-
 /* The following helper functions and class members all log a simple message
 	from some particular function scope.  Each function takes a bool argument
 	that indicates if it should log its own name or not (in the manner that
@@ -512,6 +498,39 @@ namespace
 	}
 }
 
+namespace
+{
+    void writeMsgNeedsEscaping()
+    {
+        LL_DEBUGS("WriteTag") << "backslash\\" << LL_ENDL;
+        LL_INFOS("WriteTag") << "newline\nafternewline" << LL_ENDL;
+        LL_WARNS("WriteTag") << "return\rafterreturn" << LL_ENDL;
+
+        LL_DEBUGS("WriteTag") << "backslash\\backslash\\" << LL_ENDL;
+        LL_INFOS("WriteTag") << "backslash\\newline\nanothernewline\nafternewline" << LL_ENDL;
+        LL_WARNS("WriteTag") << "backslash\\returnnewline\r\n\\afterbackslash" << LL_ENDL;
+    }
+};
+
+namespace tut
+{
+    template<> template<>
+    void ErrorTestObject::test<5>()
+        // backslash, return, and newline are not escaped with backslashes
+    {
+        LLError::setDefaultLevel(LLError::LEVEL_DEBUG);
+        setWantsMultiline(true); 
+        writeMsgNeedsEscaping(); // but should not be now
+        ensure_message_field_equals(0, MSG_FIELD, "backslash\\");
+        ensure_message_field_equals(1, MSG_FIELD, "newline\nafternewline");
+        ensure_message_field_equals(2, MSG_FIELD, "return\rafterreturn");
+        ensure_message_field_equals(3, MSG_FIELD, "backslash\\backslash\\");
+        ensure_message_field_equals(4, MSG_FIELD, "backslash\\newline\nanothernewline\nafternewline");
+        ensure_message_field_equals(5, MSG_FIELD, "backslash\\returnnewline\r\n\\afterbackslash");
+        ensure_message_count(6);
+    }
+}
+
 namespace tut
 {
 	template<> template<>
@@ -583,7 +602,6 @@ namespace tut
 		// special handling of LL_ERRS() calls
 	void ErrorTestObject::test<8>()
 	{
-		LLError::setPrintLocation(false);
 		std::string location = errorReturningLocation();
 
 		ensure_message_field_equals(0, LOCATION_FIELD, location);
@@ -630,15 +648,15 @@ namespace tut
 		// output order
 	void ErrorTestObject::test<10>()
 	{
-		LLError::setPrintLocation(true);
 		LLError::setTimeFunction(roswell);
 		setWantsTime(true);
+
 		std::string location,
 					function;
 		writeReturningLocationAndFunction(location, function);
 
 		ensure_equals("order is time level tags location function message",
-			message(0),
+                      message(0),
                       roswell() + " INFO " + "# " /* no tag */ + location + " " + function + " : " + "apple");
 	}
 
@@ -658,7 +676,7 @@ namespace tut
 		LLError::setTimeFunction(roswell);
 
 		LLError::RecorderPtr anotherRecorder(new TestRecorder());
-		boost::dynamic_pointer_cast<TestRecorder>(anotherRecorder)->setWantsTime(true);
+		boost::dynamic_pointer_cast<TestRecorder>(anotherRecorder)->showTime(true);
 		LLError::addRecorder(anotherRecorder);
 
 		LL_INFOS() << "baz" << LL_ENDL;
@@ -835,20 +853,6 @@ namespace tut
 	}
 }
 
-namespace
-{
-    void writeMsgNeedsEscaping()
-    {
-        LL_DEBUGS("WriteTag") << "backslash\\" << LL_ENDL;
-        LL_INFOS("WriteTag") << "newline\nafternewline" << LL_ENDL;
-        LL_WARNS("WriteTag") << "return\rafterreturn" << LL_ENDL;
-
-        LL_DEBUGS("WriteTag") << "backslash\\backslash\\" << LL_ENDL;
-        LL_INFOS("WriteTag") << "backslash\\newline\nanothernewline\nafternewline" << LL_ENDL;
-        LL_WARNS("WriteTag") << "backslash\\returnnewline\r\n\\afterbackslash" << LL_ENDL;
-    }
-};
-
 namespace tut
 {
     template<> template<>
diff --git a/indra/llcommon/tests/llleap_test.cpp b/indra/llcommon/tests/llleap_test.cpp
index c387da6c4828b4806b1e913e614fcef8d5a7ff1b..45648536c4db95fac6b382abc77d5e60a8fadf8e 100644
--- a/indra/llcommon/tests/llleap_test.cpp
+++ b/indra/llcommon/tests/llleap_test.cpp
@@ -26,6 +26,7 @@
 #include "wrapllerrs.h"
 #include "llevents.h"
 #include "llprocess.h"
+#include "llstring.h"
 #include "stringize.h"
 #include "StringVec.h"
 #include <functional>
@@ -198,14 +199,12 @@ namespace tut
             // basename.
             reader_module(LLProcess::basename(
                               reader.getName().substr(0, reader.getName().length()-3))),
-            pPYTHON(getenv("PYTHON")),
-            PYTHON(pPYTHON? pPYTHON : "")
+            PYTHON(LLStringUtil::getenv("PYTHON"))
         {
-            ensure("Set PYTHON to interpreter pathname", pPYTHON);
+            ensure("Set PYTHON to interpreter pathname", !PYTHON.empty());
         }
         NamedExtTempFile reader;
         const std::string reader_module;
-        const char* pPYTHON;
         const std::string PYTHON;
     };
     typedef test_group<llleap_data> llleap_group;
diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp
index b27e125d2ecd824aab7bd2421424bf8bb81ed359..5c87cdabd914600873d8d0b062735b4d899a7804 100644
--- a/indra/llcommon/tests/llprocess_test.cpp
+++ b/indra/llcommon/tests/llprocess_test.cpp
@@ -34,6 +34,7 @@
 #include "stringize.h"
 #include "llsdutil.h"
 #include "llevents.h"
+#include "llstring.h"
 #include "wrapllerrs.h"
 
 #if defined(LL_WINDOWS)
@@ -142,8 +143,8 @@ struct PythonProcessLauncher
         mDesc(desc),
         mScript("py", script)
     {
-        const char* PYTHON(getenv("PYTHON"));
-        tut::ensure("Set $PYTHON to the Python interpreter", PYTHON);
+        auto PYTHON(LLStringUtil::getenv("PYTHON"));
+        tut::ensure("Set $PYTHON to the Python interpreter", !PYTHON.empty());
 
         mParams.desc = desc + " script";
         mParams.executable = PYTHON;
diff --git a/indra/llcommon/tests/llsdserialize_test.cpp b/indra/llcommon/tests/llsdserialize_test.cpp
index 745e3a168c18a5e077f8d31f08f1c85755f31ce1..6ac974e659383dfe7493595ffd648236f41066a7 100644
--- a/indra/llcommon/tests/llsdserialize_test.cpp
+++ b/indra/llcommon/tests/llsdserialize_test.cpp
@@ -41,6 +41,7 @@ typedef U32 uint32_t;
 #include <sys/stat.h>
 #include <sys/wait.h>
 #include "llprocess.h"
+#include "llstring.h"
 #endif
 
 #include "boost/range.hpp"
@@ -1705,8 +1706,8 @@ namespace tut
         template <typename CONTENT>
         void python(const std::string& desc, const CONTENT& script, int expect=0)
         {
-            const char* PYTHON(getenv("PYTHON"));
-            ensure("Set $PYTHON to the Python interpreter", PYTHON);
+            auto PYTHON(LLStringUtil::getenv("PYTHON"));
+            ensure("Set $PYTHON to the Python interpreter", !PYTHON.empty());
 
             NamedTempFile scriptfile("py", script);
 
@@ -1714,7 +1715,7 @@ namespace tut
             std::string q("\"");
             std::string qPYTHON(q + PYTHON + q);
             std::string qscript(q + scriptfile.getName() + q);
-            int rc = _spawnl(_P_WAIT, PYTHON, qPYTHON.c_str(), qscript.c_str(), NULL);
+            int rc = _spawnl(_P_WAIT, PYTHON.c_str(), qPYTHON.c_str(), qscript.c_str(), NULL);
             if (rc == -1)
             {
                 char buffer[256];
diff --git a/indra/llcommon/tests/wrapllerrs.h b/indra/llcommon/tests/wrapllerrs.h
index 9a4bbbd630d002a6effa410c39e737f0dfc23d89..08fbf19b1cdccec7323769b515246737368de2d0 100644
--- a/indra/llcommon/tests/wrapllerrs.h
+++ b/indra/llcommon/tests/wrapllerrs.h
@@ -109,6 +109,12 @@ class CaptureLogRecorder : public LLError::Recorder, public boost::noncopyable
         mMessages.push_back(message);
     }
 
+    friend inline
+    std::ostream& operator<<(std::ostream& out, const CaptureLogRecorder& log)
+    {
+        return log.streamto(out);
+    }
+
     /// Don't assume the message we want is necessarily the LAST log message
     /// emitted by the underlying code; search backwards through all messages
     /// for the sought string.
@@ -126,7 +132,7 @@ class CaptureLogRecorder : public LLError::Recorder, public boost::noncopyable
 
         throw tut::failure(STRINGIZE("failed to find '" << search
                                      << "' in captured log messages:\n"
-                                     << boost::ref(*this)));
+                                     << *this));
     }
 
     std::ostream& streamto(std::ostream& out) const
@@ -200,10 +206,4 @@ class CaptureLog : public boost::noncopyable
 	LLError::RecorderPtr mRecorder;
 };
 
-inline
-std::ostream& operator<<(std::ostream& out, const CaptureLogRecorder& log)
-{
-    return log.streamto(out);
-}
-
 #endif /* ! defined(LL_WRAPLLERRS_H) */
diff --git a/indra/llimagej2coj/CMakeLists.txt b/indra/llimagej2coj/CMakeLists.txt
index 97d22cf86ae65dc097b036232c72e9d6a5f5a6bd..c9423d50dd618e91a593edc0222a99c63bca44cb 100644
--- a/indra/llimagej2coj/CMakeLists.txt
+++ b/indra/llimagej2coj/CMakeLists.txt
@@ -29,7 +29,9 @@ set_source_files_properties(${llimagej2coj_HEADER_FILES}
 list(APPEND llimagej2coj_SOURCE_FILES ${llimagej2coj_HEADER_FILES})
 
 add_library (llimagej2coj ${llimagej2coj_SOURCE_FILES})
+
 target_link_libraries(
     llimagej2coj
     ${OPENJPEG_LIBRARIES}
     )
+
diff --git a/indra/llmessage/tests/commtest.h b/indra/llmessage/tests/commtest.h
index 7c8f27bbd2f1ae266190a413fec9af3e7657c5d2..0359eba803f535c7bae79164f0d32cf57b24b59f 100644
--- a/indra/llmessage/tests/commtest.h
+++ b/indra/llmessage/tests/commtest.h
@@ -34,6 +34,7 @@
 #include "llsd.h"
 #include "llhost.h"
 #include "llexception.h"
+#include "llstring.h"
 #include "stringize.h"
 #include <map>
 #include <string>
@@ -46,12 +47,7 @@ struct CommtestError: public LLException
 
 static bool query_verbose()
 {
-    const char* cbose = getenv("INTEGRATION_TEST_VERBOSE");
-    if (! cbose)
-    {
-        cbose = "1";
-    }
-    std::string strbose(cbose);
+    std::string strbose(LLStringUtil::getenv("INTEGRATION_TEST_VERBOSE", "1"));
     return (! (strbose == "0" || strbose == "off" ||
                strbose == "false" || strbose == "quiet"));
 }
diff --git a/indra/llmessage/tests/llhttpclient_test.cpp b/indra/llmessage/tests/llhttpclient_test.cpp
index 9356a14f1f89fd24a5ee6bc0047bf763c4744002..78faa66a0d240fad1ff490cb257ad724618a2639 100644
--- a/indra/llmessage/tests/llhttpclient_test.cpp
+++ b/indra/llmessage/tests/llhttpclient_test.cpp
@@ -41,6 +41,7 @@
 #include "llpumpio.h"
 
 #include "lliosocket.h"
+#include "llstring.h"
 #include "stringize.h"
 #include "llcleanup.h"
 
@@ -50,13 +51,13 @@ namespace tut
 	{
 	public:
 		HTTPClientTestData():
-			PORT(getenv("PORT")),
+			PORT(LLStringUtil::getenv("PORT")),
 			// Turning NULL PORT into empty string doesn't make things work;
 			// that's just to keep this initializer from blowing up. We test
 			// PORT separately in the constructor body.
-			local_server(STRINGIZE("http://127.0.0.1:" << (PORT? PORT : "") << "/"))
+			local_server(STRINGIZE("http://127.0.0.1:" << PORT << "/"))
 		{
-			ensure("Set environment variable PORT to local test server port", PORT);
+			ensure("Set environment variable PORT to local test server port", !PORT.empty());
 			apr_pool_create(&mPool, NULL);
 			LLCurl::initClass(false);
 			mClientPump = new LLPumpIO(mPool);
@@ -87,7 +88,7 @@ namespace tut
 			}
 		}
 
-		const char* const PORT;
+		const std::string PORT;
 		const std::string local_server;
 
 	private:
diff --git a/indra/llplugin/slplugin/CMakeLists.txt b/indra/llplugin/slplugin/CMakeLists.txt
index 0e5e835777b5d117db8975a96728a0c9c4b97358..33520ad64c25ba32900563e8d48dd14515ce9b7f 100644
--- a/indra/llplugin/slplugin/CMakeLists.txt
+++ b/indra/llplugin/slplugin/CMakeLists.txt
@@ -48,7 +48,7 @@ add_executable(SLPlugin
     WIN32
     MACOSX_BUNDLE
     ${SLPlugin_SOURCE_FILES}
-)
+    )
 
 if (WINDOWS)
 set_target_properties(SLPlugin
diff --git a/indra/llrender/llfontgl.cpp b/indra/llrender/llfontgl.cpp
index cf0a1175676fd8d426873643c63b7fd32f1f595a..8cd18c5fa1987cd4a1b364a076f901e6221348a3 100644
--- a/indra/llrender/llfontgl.cpp
+++ b/indra/llrender/llfontgl.cpp
@@ -40,10 +40,17 @@
 #include "v4color.h"
 #include "lltexture.h"
 #include "lldir.h"
+#include "llstring.h"
 
 // Third party library includes
 #include <boost/tokenizer.hpp>
 
+#if LL_WINDOWS
+#include <Shlobj.h>
+#include <Knownfolders.h>
+#include <Objbase.h>
+#endif // LL_WINDOWS
+
 const S32 BOLD_OFFSET = 1;
 
 // static class members
@@ -1063,33 +1070,33 @@ LLFontGL* LLFontGL::getFontDefault()
 // static 
 std::string LLFontGL::getFontPathSystem()
 {
-	std::string system_path;
-
-	// Try to figure out where the system's font files are stored.
-	char *system_root = NULL;
-#if LL_WINDOWS
-	system_root = getenv("SystemRoot");	/* Flawfinder: ignore */
-	if (!system_root)
-	{
-		LL_WARNS() << "SystemRoot not found, attempting to load fonts from default path." << LL_ENDL;
-	}
+#if LL_DARWIN
+    // HACK for Mac OS X
+    return "/System/Library/Fonts/";
+
+#elif LL_WINDOWS
+    auto system_root = LLStringUtil::getenv("SystemRoot");
+    if (! system_root.empty())
+    {
+        std::string fontpath(gDirUtilp->add(system_root, "fonts") + gDirUtilp->getDirDelimiter());
+        LL_INFOS() << "from SystemRoot: " << fontpath << LL_ENDL;
+        return fontpath;
+    }
+
+    wchar_t *pwstr = NULL;
+    HRESULT okay = SHGetKnownFolderPath(FOLDERID_Fonts, 0, NULL, &pwstr);
+    if (SUCCEEDED(okay) && pwstr)
+    {
+        std::string fontpath(ll_convert_wide_to_string(pwstr));
+        // SHGetKnownFolderPath() contract requires us to free pwstr
+        CoTaskMemFree(pwstr);
+        LL_INFOS() << "from SHGetKnownFolderPath(): " << fontpath << LL_ENDL;
+        return fontpath;
+    }
 #endif
 
-	if (system_root)
-	{
-		system_path = llformat("%s/fonts/", system_root);
-	}
-	else
-	{
-#if LL_WINDOWS
-		// HACK for windows 98/Me
-		system_path = "/WINDOWS/FONTS/";
-#elif LL_DARWIN
-		// HACK for Mac OS X
-		system_path = "/System/Library/Fonts/";
-#endif
-	}
-	return system_path;
+    LL_WARNS() << "Could not determine system fonts path" << LL_ENDL;
+    return {};
 }
 
 
diff --git a/indra/llui/llnotificationslistener.cpp b/indra/llui/llnotificationslistener.cpp
index b6a32a0e78e7ea3914771d684736edb05f96980c..be26416cbbf958ecb150ba8747d0f729ea05c675 100644
--- a/indra/llui/llnotificationslistener.cpp
+++ b/indra/llui/llnotificationslistener.cpp
@@ -90,9 +90,12 @@ void LLNotificationsListener::requestAdd(const LLSD& event_data) const
 {
 	if(event_data.has("reply"))
 	{
+		LLSD payload(event_data["payload"]);
+		// copy reqid, if provided, to link response with request
+		payload["reqid"] = event_data["reqid"];
 		mNotifications.add(event_data["name"], 
 						   event_data["substitutions"], 
-						   event_data["payload"],
+						   payload,
 						   boost::bind(&LLNotificationsListener::NotificationResponder, 
 									   this, 
 									   event_data["reply"].asString(), 
@@ -112,10 +115,12 @@ void LLNotificationsListener::NotificationResponder(const std::string& reply_pum
 										const LLSD& notification, 
 										const LLSD& response) const
 {
-	LLSD reponse_event;
-	reponse_event["notification"] = notification;
-	reponse_event["response"] = response;
-	LLEventPumps::getInstance()->obtain(reply_pump).post(reponse_event);
+	LLSD response_event;
+	response_event["notification"] = notification;
+	response_event["response"] = response;
+	// surface reqid at top level of response for request/response protocol
+	response_event["reqid"] = notification["payload"]["reqid"];
+	LLEventPumps::getInstance()->obtain(reply_pump).post(response_event);
 }
 
 void LLNotificationsListener::listChannels(const LLSD& params) const
diff --git a/indra/llvfs/lldir.cpp b/indra/llvfs/lldir.cpp
index 2069888774fce47c55f47291f9d9cf4df26c23fb..18836e54b0fd19a991f1bf820aa41672c2a955e7 100644
--- a/indra/llvfs/lldir.cpp
+++ b/indra/llvfs/lldir.cpp
@@ -42,6 +42,7 @@
 
 #include "lldiriterator.h"
 #include "stringize.h"
+#include "llstring.h"
 #include <boost/filesystem.hpp>
 #include <boost/foreach.hpp>
 #include <boost/range/begin.hpp>
@@ -317,9 +318,9 @@ const std::string& LLDir::getChatLogsDir() const
 void LLDir::setDumpDir( const std::string& path )
 {
     LLDir::sDumpDir = path;
-    if (! sDumpDir.empty() && sDumpDir.rbegin() == mDirDelimiter.rbegin() )
+    if (LLStringUtil::endsWith(sDumpDir, mDirDelimiter))
     {
-        sDumpDir.erase(sDumpDir.size() -1);
+        sDumpDir.erase(sDumpDir.size() - mDirDelimiter.size());
     }
 }
 
diff --git a/indra/llvfs/lldir_linux.cpp b/indra/llvfs/lldir_linux.cpp
index 2cd06b81f8434db856e59b53f396fad58a2047ac..80ad05345a34a6273147dd7ba8c7c37fdeb2d901 100644
--- a/indra/llvfs/lldir_linux.cpp
+++ b/indra/llvfs/lldir_linux.cpp
@@ -29,6 +29,7 @@
 #include "lldir_linux.h"
 #include "llerror.h"
 #include "llrand.h"
+#include "llstring.h"
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <unistd.h>
@@ -40,28 +41,24 @@ static std::string getCurrentUserHome(char* fallback)
 {
 	const uid_t uid = getuid();
 	struct passwd *pw;
-	char *result_cstr = fallback;
-	
+
 	pw = getpwuid(uid);
 	if ((pw != NULL) && (pw->pw_dir != NULL))
 	{
-		result_cstr = (char*) pw->pw_dir;
+		return pw->pw_dir;
+	}
+
+	LL_INFOS() << "Couldn't detect home directory from passwd - trying $HOME" << LL_ENDL;
+	auto home_env = LLStringUtil::getoptenv("HOME");
+	if (home_env)
+	{
+		return *home_env;
 	}
 	else
 	{
-		LL_INFOS() << "Couldn't detect home directory from passwd - trying $HOME" << LL_ENDL;
-		const char *const home_env = getenv("HOME");	/* Flawfinder: ignore */ 
-		if (home_env)
-		{
-			result_cstr = (char*) home_env;
-		}
-		else
-		{
-			LL_WARNS() << "Couldn't detect home directory!  Falling back to " << fallback << LL_ENDL;
-		}
+		LL_WARNS() << "Couldn't detect home directory!  Falling back to " << fallback << LL_ENDL;
+		return fallback;
 	}
-	
-	return std::string(result_cstr);
 }
 
 
@@ -156,18 +153,18 @@ void LLDir_Linux::initAppDirs(const std::string &app_name,
 	if (!app_read_only_data_dir.empty())
 	{
 		mAppRODataDir = app_read_only_data_dir;
-		mSkinBaseDir = mAppRODataDir + mDirDelimiter + "skins";
+		mSkinBaseDir = add(mAppRODataDir, "skins");
 	}
 	mAppName = app_name;
 
 	std::string upper_app_name(app_name);
 	LLStringUtil::toUpper(upper_app_name);
 
-	char* app_home_env = getenv((upper_app_name + "_USER_DIR").c_str());	/* Flawfinder: ignore */ 
+	auto app_home_env(LLStringUtil::getoptenv(upper_app_name + "_USER_DIR"));
 	if (app_home_env)
 	{
 		// user has specified own userappdir i.e. $SECONDLIFE_USER_DIR
-		mOSUserAppDir = app_home_env;
+		mOSUserAppDir = *app_home_env;
 	}
 	else
 	{
diff --git a/indra/llvfs/lldir_mac.cpp b/indra/llvfs/lldir_mac.cpp
index 79c436274743cd40f224c465e6d365cabd1160cd..87dc1b9795f518d4b4e2e0d12cb2ea6c4a2bc646 100644
--- a/indra/llvfs/lldir_mac.cpp
+++ b/indra/llvfs/lldir_mac.cpp
@@ -171,9 +171,9 @@ void LLDir_Mac::initAppDirs(const std::string &app_name,
 	if (!app_read_only_data_dir.empty())
 	{
 		mAppRODataDir = app_read_only_data_dir;
-		mSkinBaseDir = mAppRODataDir + mDirDelimiter + "skins";
+		mSkinBaseDir = add(mAppRODataDir, "skins");
 	}
-	mCAFile = getExpandedFilename(LL_PATH_EXECUTABLE, "../Resources", "ca-bundle.crt");
+	mCAFile = add(mAppRODataDir, "ca-bundle.crt");
 }
 
 std::string LLDir_Mac::getCurPath()
diff --git a/indra/llvfs/lldir_solaris.cpp b/indra/llvfs/lldir_solaris.cpp
index d3536a12eec9195c69c21ad7284e201801078f42..f18560ff20256f6c9dcd1b4d2c24cf6811beff83 100644
--- a/indra/llvfs/lldir_solaris.cpp
+++ b/indra/llvfs/lldir_solaris.cpp
@@ -29,6 +29,7 @@
 #include "lldir_solaris.h"
 #include "llerror.h"
 #include "llrand.h"
+#include "llstring.h"
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <unistd.h>
@@ -41,30 +42,28 @@
 
 static std::string getCurrentUserHome(char* fallback)
 {
+	// fwiw this exactly duplicates getCurrentUserHome() in lldir_linux.cpp...
+	// we should either derive both from LLDir_Posix or just axe Solaris.
 	const uid_t uid = getuid();
 	struct passwd *pw;
-	char *result_cstr = fallback;
-	
+
 	pw = getpwuid(uid);
 	if ((pw != NULL) && (pw->pw_dir != NULL))
 	{
-		result_cstr = (char*) pw->pw_dir;
+		return pw->pw_dir;
+	}
+
+	LL_INFOS() << "Couldn't detect home directory from passwd - trying $HOME" << LL_ENDL;
+	auto home_env = LLStringUtil::getoptenv("HOME");
+	if (home_env)
+	{
+		return *home_env;
 	}
 	else
 	{
-		LL_INFOS() << "Couldn't detect home directory from passwd - trying $HOME" << LL_ENDL;
-		const char *const home_env = getenv("HOME");	/* Flawfinder: ignore */ 
-		if (home_env)
-		{
-			result_cstr = (char*) home_env;
-		}
-		else
-		{
-			LL_WARNS() << "Couldn't detect home directory!  Falling back to " << fallback << LL_ENDL;
-		}
+		LL_WARNS() << "Couldn't detect home directory!  Falling back to " << fallback << LL_ENDL;
+		return fallback;
 	}
-	
-	return std::string(result_cstr);
 }
 
 
@@ -135,27 +134,15 @@ LLDir_Solaris::LLDir_Solaris()
 	//NOTE: Why force people to cd into the package directory?
 	//      Look for SECONDLIFE env variable and use it, if set.
 
-	char *dcf = getenv("SECONDLIFE");
-	if(dcf != NULL){
-		(void)strcpy(path, dcf);
-		(void)strcat(path, "/bin");	//NOTE:  make sure we point at the bin
-		mExecutableDir = strdup(path);
+	auto SECONDLIFE(LLDirUtil::getoptenv("SECONDLIFE"));
+	if(SECONDLIFE){
+		mExecutableDir = add(*SECONDLIFE, "bin"); //NOTE:  make sure we point at the bin
 	}else{
-			// plunk a null at last '/' to get exec dir
-		char *s = execpath + strlen(execpath) -1;
-		while(*s != '/' && s != execpath){
-			--s;
-		}
-	
-		if(s != execpath){
-			*s = (char)NULL;
-	
-			mExecutableDir = strdup(execpath);
-			LL_INFOS() << "mExecutableDir = [" << mExecutableDir << "]" << LL_ENDL;
-		}
+		mExecutableDir = getDirName(execpath);
+		LL_INFOS() << "mExecutableDir = [" << mExecutableDir << "]" << LL_ENDL;
 	}
-	
-	mLLPluginDir = mExecutableDir + mDirDelimiter + "llplugin";
+
+	mLLPluginDir = add(mExecutableDir, "llplugin");
 
 	// *TODO: don't use /tmp, use $HOME/.secondlife/tmp or something.
 	mTempDir = "/tmp";
@@ -175,17 +162,18 @@ void LLDir_Solaris::initAppDirs(const std::string &app_name,
 	if (!app_read_only_data_dir.empty())
 	{
 		mAppRODataDir = app_read_only_data_dir;
+		mSkinBaseDir = add(mAppRODataDir, "skins");
 	}
 	mAppName = app_name;
 
 	std::string upper_app_name(app_name);
 	LLStringUtil::toUpper(upper_app_name);
 
-	char* app_home_env = getenv((upper_app_name + "_USER_DIR").c_str());	/* Flawfinder: ignore */ 
+	auto app_home_env(LLStringUtil::getoptenv(upper_app_name + "_USER_DIR"));
 	if (app_home_env)
 	{
 		// user has specified own userappdir i.e. $SECONDLIFE_USER_DIR
-		mOSUserAppDir = app_home_env;
+		mOSUserAppDir = *app_home_env;
 	}
 	else
 	{
diff --git a/indra/llvfs/lldir_win32.cpp b/indra/llvfs/lldir_win32.cpp
index 9836fa28f2920f8485d89f554efbb8b3178dc30c..b3b3afb37e61c1f474622601b818a61a611cc6d8 100644
--- a/indra/llvfs/lldir_win32.cpp
+++ b/indra/llvfs/lldir_win32.cpp
@@ -30,7 +30,9 @@
 
 #include "lldir_win32.h"
 #include "llerror.h"
-#include "llrand.h"		// for gLindenLabRandomNumber
+#include "llstring.h"
+#include "stringize.h"
+#include "llfile.h"
 #include <shlobj.h>
 #include <fstream>
 
@@ -43,15 +45,87 @@
 #define PACKVERSION(major,minor) MAKELONG(minor,major)
 DWORD GetDllVersion(LPCTSTR lpszDllName);
 
+namespace
+{ // anonymous
+    enum class prst { INIT, OPEN, SKIP } state = prst::INIT;
+    // This is called so early that we can't count on static objects being
+    // properly constructed yet, so declare a pointer instead of an instance.
+    std::ofstream* prelogf = nullptr;
+
+    void prelog(const std::string& message)
+    {
+        boost::optional<std::string> prelog_name;
+
+        switch (state)
+        {
+        case prst::INIT:
+            // assume we failed, until we succeed
+            state = prst::SKIP;
+
+            prelog_name = LLStringUtil::getoptenv("PRELOG");
+            if (! prelog_name)
+                // no PRELOG variable set, carry on
+                return;
+            prelogf = new llofstream(*prelog_name, std::ios_base::app);
+            if (! (prelogf && prelogf->is_open()))
+                // can't complain to anybody; how?
+                return;
+            // got the log file open, cool!
+            state = prst::OPEN;
+            (*prelogf) << "========================================================================"
+                       << std::endl;
+            // fall through, don't break
+
+        case prst::OPEN:
+            (*prelogf) << message << std::endl;
+            break;
+
+        case prst::SKIP:
+            // either PRELOG isn't set, or we failed to open that pathname
+            break;
+        }
+    }
+} // anonymous namespace
+
+#define PRELOG(expression) prelog(STRINGIZE(expression))
+
 LLDir_Win32::LLDir_Win32()
 {
 	// set this first: used by append() and add() methods
 	mDirDelimiter = "\\";
 
+	WCHAR w_str[MAX_PATH];
 	// Application Data is where user settings go. We rely on $APPDATA being
-	// correct; in fact the VMP makes a point of setting it properly, since
-	// Windows itself botches the job for non-ASCII usernames (MAINT-8087).
-	mOSUserDir = ll_safe_string(getenv("APPDATA"));
+	// correct.
+	auto APPDATA = LLStringUtil::getoptenv("APPDATA");
+	if (APPDATA)
+	{
+		mOSUserDir = *APPDATA;
+	}
+	PRELOG("APPDATA='" << mOSUserDir << "'");
+	// On Windows, we could have received a plain-ASCII pathname in which
+	// non-ASCII characters have been munged to '?', or the pathname could
+	// have been badly encoded and decoded such that we now have garbage
+	// instead of a valid path. Check that mOSUserDir actually exists.
+	if (mOSUserDir.empty() || ! fileExists(mOSUserDir))
+	{
+		PRELOG("APPDATA does not exist");
+		//HRESULT okay = SHGetFolderPath(NULL, CSIDL_APPDATA, NULL, 0, w_str);
+		wchar_t *pwstr = NULL;
+		HRESULT okay = SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, NULL, &pwstr);
+		PRELOG("SHGetKnownFolderPath(FOLDERID_RoamingAppData) returned " << okay);
+		if (SUCCEEDED(okay) && pwstr)
+		{
+			// But of course, only update mOSUserDir if SHGetKnownFolderPath() works.
+			mOSUserDir = ll_convert_wide_to_string(pwstr);
+			// Not only that: update our environment so that child processes
+			// will see a reasonable value as well.
+			_wputenv_s(L"APPDATA", pwstr);
+			// SHGetKnownFolderPath() contract requires us to free pwstr
+			CoTaskMemFree(pwstr);
+			PRELOG("mOSUserDir='" << mOSUserDir << "'");
+		}
+	}
 
 	// We want cache files to go on the local disk, even if the
 	// user is on a network with a "roaming profile".
@@ -61,9 +135,34 @@ LLDir_Win32::LLDir_Win32()
 	//
 	// We used to store the cache in AppData\Roaming, and the installer
 	// cleans up that version on upgrade.  JC
-	mOSCacheDir = ll_safe_string(getenv("LOCALAPPDATA"));
+	auto LOCALAPPDATA = LLStringUtil::getoptenv("LOCALAPPDATA");
+	if (LOCALAPPDATA)
+	{
+		mOSCacheDir = *LOCALAPPDATA;
+	}
+	PRELOG("LOCALAPPDATA='" << mOSCacheDir << "'");
+	// Windows really does not deal well with pathnames containing non-ASCII
+	// characters. See above remarks about APPDATA.
+	if (mOSCacheDir.empty() || ! fileExists(mOSCacheDir))
+	{
+		PRELOG("LOCALAPPDATA does not exist");
+		//HRESULT okay = SHGetFolderPath(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, w_str);
+		wchar_t *pwstr = NULL;
+		HRESULT okay = SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, NULL, &pwstr);
+		PRELOG("SHGetKnownFolderPath(FOLDERID_LocalAppData) returned " << okay);
+		if (SUCCEEDED(okay) && pwstr)
+		{
+			// But of course, only update mOSCacheDir if SHGetKnownFolderPath() works.
+			mOSCacheDir = ll_convert_wide_to_string(pwstr);
+			// Update our environment so that child processes will see a
+			// reasonable value as well.
+			_wputenv_s(L"LOCALAPPDATA", pwstr);
+			// SHGetKnownFolderPath() contract requires us to free pwstr
+			CoTaskMemFree(pwstr);
+			PRELOG("mOSCacheDir='" << mOSCacheDir << "'");
+		}
+	}
 
-	WCHAR w_str[MAX_PATH];
 	if (GetTempPath(MAX_PATH, w_str))
 	{
 		if (wcslen(w_str))	/* Flawfinder: ignore */ 
diff --git a/indra/llwindow/llwindowmacosx.cpp b/indra/llwindow/llwindowmacosx.cpp
index 843294c239826d8f35b3f3cf88c9dd11b574ad6d..d4afbb15df4f7a8fd1ebd58fdbd9d8f13cc31a86 100644
--- a/indra/llwindow/llwindowmacosx.cpp
+++ b/indra/llwindow/llwindowmacosx.cpp
@@ -1429,12 +1429,10 @@ static CursorRef gCursors[UI_CURSOR_COUNT];
 static void initPixmapCursor(int cursorid, int hotspotX, int hotspotY)
 {
 	// cursors are in <Application Bundle>/Contents/Resources/cursors_mac/UI_CURSOR_FOO.tif
-	std::string fullpath = gDirUtilp->getAppRODataDir();
-	fullpath += gDirUtilp->getDirDelimiter();
-	fullpath += "cursors_mac";
-	fullpath += gDirUtilp->getDirDelimiter();
-	fullpath += cursorIDToName(cursorid);
-	fullpath += ".tif";
+	std::string fullpath = gDirUtilp->add(
+		gDirUtilp->getAppRODataDir(),
+		"cursors_mac",
+		cursorIDToName(cursorid) + std::string(".tif"));
 
 	gCursors[cursorid] = createImageCursor(fullpath.c_str(), hotspotX, hotspotY);
 }
diff --git a/indra/llwindow/llwindowwin32.cpp b/indra/llwindow/llwindowwin32.cpp
index 4ee4a5357c53eda5dd69ae7e2c99315e265fedbc..504c1589b0c8c641a2acbc807ab58a41f1ddb1eb 100644
--- a/indra/llwindow/llwindowwin32.cpp
+++ b/indra/llwindow/llwindowwin32.cpp
@@ -3275,8 +3275,10 @@ S32 OSMessageBoxWin32(const std::string& text, const std::string& caption, U32 t
 		break;
 	}
 
-	// HACK! Doesn't properly handle wide strings!
-	int retval_win = MessageBoxA(NULL, text.c_str(), caption.c_str(), uType);
+	int retval_win = MessageBoxW(NULL, // HWND
+								 ll_convert_string_to_wide(text).c_str(),
+								 ll_convert_string_to_wide(caption).c_str(),
+								 uType);
 	S32 retval;
 
 	switch(retval_win)
diff --git a/indra/llxml/tests/llcontrol_test.cpp b/indra/llxml/tests/llcontrol_test.cpp
index 2b691ffbb1a8ba63503b6adec0ef9a0733e96c34..f7e43d6def37e4025576991dfd4ddc180f14bec0 100644
--- a/indra/llxml/tests/llcontrol_test.cpp
+++ b/indra/llxml/tests/llcontrol_test.cpp
@@ -27,43 +27,31 @@
 
 #include "linden_common.h"
 #include "llsdserialize.h"
+#include "llfile.h"
+#include "stringize.h"
 
 #include "../llcontrol.h"
 
 #include "../test/lltut.h"
+#include <memory>
+#include <vector>
 
 namespace tut
 {
-
 	struct control_group
 	{
-		LLControlGroup* mCG;
+		std::unique_ptr<LLControlGroup> mCG;
 		std::string mTestConfigDir;
 		std::string mTestConfigFile;
+		std::vector<std::string> mCleanups;
 		static bool mListenerFired;
 		control_group()
 		{
-			mCG = new LLControlGroup("foo");
+			mCG.reset(new LLControlGroup("foo"));
 			LLUUID random;
 			random.generate();
 			// generate temp dir
-			std::ostringstream oStr;
-
-#ifdef LL_WINDOWS
-			char* tmp_dir = getenv("TMP");
-			if(tmp_dir)
-			{
-				oStr << tmp_dir << "/llcontrol-test-" << random << "/";
-			}
-			else
-			{
-				oStr << "c:/tmp/llcontrol-test-" << random << "/";
-			}
-#else
-			oStr << "/tmp/llcontrol-test-" << random << "/";
-#endif
-
-			mTestConfigDir = oStr.str();
+			mTestConfigDir = STRINGIZE(LLFile::tmpdir() << "llcontrol-test-" << random << "/");
 			mTestConfigFile = mTestConfigDir + "settings.xml";
 			LLFile::mkdir(mTestConfigDir);
 			LLSD config;
@@ -76,7 +64,12 @@ namespace tut
 		~control_group()
 		{
 			//Remove test files
-			delete mCG;
+			for (auto filename : mCleanups)
+			{
+				LLFile::remove(filename);
+			}
+			LLFile::remove(mTestConfigFile);
+			LLFile::rmdir(mTestConfigDir);
 		}
 		void writeSettingsFile(const LLSD& config)
 		{
@@ -118,6 +111,7 @@ namespace tut
 		ensure_equals("value of changed setting", mCG->getU32("TestSetting"), 13);
 		LLControlGroup test_cg("foo2");
 		std::string temp_test_file = (mTestConfigDir + "setting_llsd_temp.xml");
+		mCleanups.push_back(temp_test_file);
 		mCG->saveToFile(temp_test_file.c_str(), TRUE);
 		results = test_cg.loadFromFile(temp_test_file.c_str());
 		ensure("number of changed settings loaded", (results == 1));
@@ -139,6 +133,7 @@ namespace tut
 		ensure_equals("value of changed setting", mCG->getU32("TestSetting"), 13);
 		LLControlGroup test_cg("foo3");
 		std::string temp_test_file = (mTestConfigDir + "setting_llsd_persist_temp.xml");
+		mCleanups.push_back(temp_test_file);
 		mCG->saveToFile(temp_test_file.c_str(), TRUE);
 		results = test_cg.loadFromFile(temp_test_file.c_str());
 		//If we haven't changed any settings, then we shouldn't have any settings to load
@@ -153,7 +148,7 @@ namespace tut
 		ensure("number of settings", (results == 1));
 		mCG->getControl("TestSetting")->getSignal()->connect(boost::bind(&this->handleListenerTest));
 		mCG->setU32("TestSetting", 13);
-		ensure("listener fired on changed setting", mListenerFired);	   
+		ensure("listener fired on changed setting", mListenerFired);
 	}
 
 }
diff --git a/indra/media_plugins/base/CMakeLists.txt b/indra/media_plugins/base/CMakeLists.txt
index 70c81d40236562776d3ff3893ce68897724e1d2f..7f2b82ffdd0b34c501b2a18f228eaafeb63ed7e7 100644
--- a/indra/media_plugins/base/CMakeLists.txt
+++ b/indra/media_plugins/base/CMakeLists.txt
@@ -48,5 +48,5 @@ set(media_plugin_base_HEADER_FILES
 
 add_library(media_plugin_base
     ${media_plugin_base_SOURCE_FILES}
-)
+    )
 
diff --git a/indra/media_plugins/cef/CMakeLists.txt b/indra/media_plugins/cef/CMakeLists.txt
index 5452fd9d1eb8f196be5efb627d8d6008c66cd23a..ce6278963db537bc1f2498b37dd27d4fe9ae4b88 100644
--- a/indra/media_plugins/cef/CMakeLists.txt
+++ b/indra/media_plugins/cef/CMakeLists.txt
@@ -81,7 +81,7 @@ list(APPEND media_plugin_cef_SOURCE_FILES ${media_plugin_cef_HEADER_FILES})
 add_library(media_plugin_cef
     SHARED
     ${media_plugin_cef_SOURCE_FILES}
-)
+    )
 
 #add_dependencies(media_plugin_cef
 #  ${MEDIA_PLUGIN_BASE_LIBRARIES}
diff --git a/indra/media_plugins/example/CMakeLists.txt b/indra/media_plugins/example/CMakeLists.txt
index 6f5b28b8e91bcf4a355aad26363461ba08953750..eb067a7f6e56a204c59b8f0a2a1d07cbdbc5b7cb 100644
--- a/indra/media_plugins/example/CMakeLists.txt
+++ b/indra/media_plugins/example/CMakeLists.txt
@@ -47,7 +47,7 @@ set(media_plugin_example_SOURCE_FILES
 add_library(media_plugin_example
     SHARED
     ${media_plugin_example_SOURCE_FILES}
-)
+    )
 
 target_link_libraries(media_plugin_example
   ${LLPLUGIN_LIBRARIES}
diff --git a/indra/media_plugins/gstreamer010/CMakeLists.txt b/indra/media_plugins/gstreamer010/CMakeLists.txt
index 6d18814b1e8a44fe828ea5b3f6416867da8deb43..571eb57b248f95b1ee5debd51d72d2c2bdf9208e 100644
--- a/indra/media_plugins/gstreamer010/CMakeLists.txt
+++ b/indra/media_plugins/gstreamer010/CMakeLists.txt
@@ -56,7 +56,7 @@ set(media_plugin_gstreamer010_HEADER_FILES
 add_library(media_plugin_gstreamer010
     SHARED
     ${media_plugin_gstreamer010_SOURCE_FILES}
-)
+    )
 
 target_link_libraries(media_plugin_gstreamer010
   ${LLPLUGIN_LIBRARIES}
diff --git a/indra/media_plugins/libvlc/CMakeLists.txt b/indra/media_plugins/libvlc/CMakeLists.txt
index d3e92430694521c89b55144405cf222c30d46d1a..97392bbe089f960db59ab3f374d4082df80209d1 100644
--- a/indra/media_plugins/libvlc/CMakeLists.txt
+++ b/indra/media_plugins/libvlc/CMakeLists.txt
@@ -48,7 +48,7 @@ set(media_plugin_libvlc_SOURCE_FILES
 add_library(media_plugin_libvlc
     SHARED
     ${media_plugin_libvlc_SOURCE_FILES}
-)
+    )
 
 target_link_libraries(media_plugin_libvlc
   ${LLPLUGIN_LIBRARIES}
diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt
index ce8b662231e9dbef1a4d29f7ce5d48651a089423..99f99834f3083fd017265bd5933d5a18a69378b4 100644
--- a/indra/newview/CMakeLists.txt
+++ b/indra/newview/CMakeLists.txt
@@ -3,7 +3,14 @@
 project(viewer)
 
 include(00-Common)
+# DON'T move Linking.cmake to its place in the alphabetized list below: it
+# sets variables on which the 3p .cmake files depend.
+include(Linking)
+
 include(Boost)
+if (BUGSPLAT_DB)
+  include(bugsplat)
+endif (BUGSPLAT_DB)
 include(BuildPackagesInfo)
 include(BuildVersion)
 include(CMakeCopyIfDifferent)
@@ -16,7 +23,6 @@ include(GLOD)
 include(Hunspell)
 include(JsonCpp)
 include(LLAppearance)
-include(LLBase)
 include(LLAudio)
 include(LLCA)
 include(LLCharacter)
@@ -37,14 +43,12 @@ include(LLUI)
 include(LLVFS)
 include(LLWindow)
 include(LLXML)
-include(Linking)
 include(NDOF)
 include(NVAPI)
 include(OPENAL)
 include(OpenGL)
 include(OpenSSL)
 include(PNG)
-include(Requests)
 include(TemplateCheck)
 include(UI)
 include(UnixInstall)
@@ -93,6 +97,12 @@ include_directories(
     ${CMAKE_CURRENT_SOURCE_DIR}
     )
 
+if (BUGSPLAT_DB)
+  include_directories(
+    ${BUGSPLAT_INCLUDE_DIR}
+    )
+endif (BUGSPLAT_DB)
+
 include_directories(SYSTEM
     ${LLCOMMON_SYSTEM_INCLUDE_DIRS}
     ${LLXML_SYSTEM_INCLUDE_DIRS}
@@ -1360,6 +1370,14 @@ if (DARWIN)
 
   # This should be compiled with the viewer.
   LIST(APPEND viewer_SOURCE_FILES llappdelegate-objc.mm)
+  set_source_files_properties(
+    llappdelegate-objc.mm
+    PROPERTIES
+    COMPILE_DEFINITIONS "${VIEWER_CHANNEL_VERSION_DEFINES}"
+    # BugsplatMac is a module, imported with @import. That language feature
+    # demands these switches.
+    COMPILE_FLAGS "-fmodules -fcxx-modules"
+    )
 
   find_library(AGL_LIBRARY AGL)
   find_library(APPKIT_LIBRARY AppKit)
@@ -1374,6 +1392,12 @@ if (DARWIN)
     ${COREAUDIO_LIBRARY}
     )
 
+  if (BUGSPLAT_DB)
+    list(APPEND viewer_LIBRARIES
+      ${BUGSPLAT_LIBRARIES}
+      )
+  endif (BUGSPLAT_DB)
+
   # Add resource files to the project.
   set(viewer_RESOURCE_FILES
     secondlife.icns
@@ -1399,6 +1423,11 @@ endif (DARWIN)
 
 if (LINUX)
     LIST(APPEND viewer_SOURCE_FILES llappviewerlinux.cpp)
+    set_source_files_properties(
+      llappviewerlinux.cpp
+      PROPERTIES
+      COMPILE_DEFINITIONS "${VIEWER_CHANNEL_VERSION_DEFINES}"
+      )
     LIST(APPEND viewer_SOURCE_FILES llappviewerlinux_api_dbus.cpp)
     SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--as-needed")
 
@@ -1415,6 +1444,11 @@ if (WINDOWS)
          llappviewerwin32.cpp
          llwindebug.cpp
          )
+    set_source_files_properties(
+      llappviewerwin32.cpp
+      PROPERTIES
+      COMPILE_DEFINITIONS "${VIEWER_CHANNEL_VERSION_DEFINES}"
+      )
 
     list(APPEND viewer_HEADER_FILES
          llappviewerwin32.h
@@ -1551,7 +1585,6 @@ if (WINDOWS)
         kernel32
         odbc32
         odbccp32
-        ole32
         oleaut32
         shell32
         Vfw32
@@ -1697,6 +1730,11 @@ if (SDL_FOUND)
     )
 endif (SDL_FOUND)
 
+if (BUGSPLAT_DB)
+  set_property(TARGET ${VIEWER_BINARY_NAME}
+    PROPERTY COMPILE_DEFINITIONS "LL_BUGSPLAT")
+endif (BUGSPLAT_DB)
+
 # add package files
 file(GLOB EVENT_HOST_SCRIPT_GLOB_LIST
      ${CMAKE_CURRENT_SOURCE_DIR}/../viewer_components/*.py)
@@ -1795,7 +1833,7 @@ if (WINDOWS)
            ${SHARED_LIB_STAGING_DIR}/Debug/fmodexL.dll
           )
     endif (FMODEX)
-    
+
     add_custom_command(
       OUTPUT  ${CMAKE_CFG_INTDIR}/copy_touched.bat
       COMMAND ${PYTHON_EXECUTABLE}
@@ -1804,15 +1842,16 @@ if (WINDOWS)
         --actions=copy
         --arch=${ARCH}
         --artwork=${ARTWORK_DIR}
+        "--bugsplat=${BUGSPLAT_DB}"
         --build=${CMAKE_CURRENT_BINARY_DIR}
         --buildtype=${CMAKE_BUILD_TYPE}
+        "--channel=${VIEWER_CHANNEL}"
         --configuration=${CMAKE_CFG_INTDIR}
         --dest=${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}
         --grid=${GRID}
-        "--channel=${VIEWER_CHANNEL}"
-        --versionfile=${CMAKE_CURRENT_BINARY_DIR}/viewer_version.txt
         --source=${CMAKE_CURRENT_SOURCE_DIR}
         --touch=${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/copy_touched.bat
+        --versionfile=${CMAKE_CURRENT_BINARY_DIR}/viewer_version.txt
       DEPENDS
         ${CMAKE_CURRENT_SOURCE_DIR}/viewer_manifest.py
         stage_third_party_libs
@@ -1830,24 +1869,9 @@ if (WINDOWS)
 
     add_dependencies(${VIEWER_BINARY_NAME}
       SLPlugin
-   windows-crash-logger
+      windows-crash-logger
     )
 
-    # sets the 'working directory' for debugging from visual studio.
-    if (NOT UNATTENDED)
-        add_custom_command(
-            TARGET ${VIEWER_BINARY_NAME} POST_BUILD
-            COMMAND ${CMAKE_SOURCE_DIR}/tools/vstool/vstool.exe
-            ARGS
-              --solution
-              ${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}.sln
-              --workingdir
-              ${VIEWER_BINARY_NAME}
-              "${CMAKE_CURRENT_SOURCE_DIR}"
-            COMMENT "Setting the ${VIEWER_BINARY_NAME} working directory for debugging."
-            )
-    endif (NOT UNATTENDED)
-
     if (PACKAGE)
       add_custom_command(
         OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/event_host.tar.bz2
@@ -1870,15 +1894,16 @@ if (WINDOWS)
           ${CMAKE_CURRENT_SOURCE_DIR}/viewer_manifest.py
           --arch=${ARCH}
           --artwork=${ARTWORK_DIR}
+          "--bugsplat=${BUGSPLAT_DB}"
           --build=${CMAKE_CURRENT_BINARY_DIR}
           --buildtype=${CMAKE_BUILD_TYPE}
           "--channel=${VIEWER_CHANNEL}"
-          --versionfile=${CMAKE_CURRENT_BINARY_DIR}/viewer_version.txt
           --configuration=${CMAKE_CFG_INTDIR}
           --dest=${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}
           --grid=${GRID}
           --source=${CMAKE_CURRENT_SOURCE_DIR}
           --touch=${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/touched.bat
+          --versionfile=${CMAKE_CURRENT_BINARY_DIR}/viewer_version.txt
         DEPENDS
             ${VIEWER_BINARY_NAME}
             ${CMAKE_CURRENT_SOURCE_DIR}/viewer_manifest.py
@@ -1909,8 +1934,8 @@ else (WINDOWS)
 endif (WINDOWS)
 
 # *NOTE: - this list is very sensitive to ordering, test carefully on all
-# platforms if you change the releative order of the entries here.
-# In particular, cmake 2.6.4 (when buidling with linux/makefile generators)
+# platforms if you change the relative order of the entries here.
+# In particular, cmake 2.6.4 (when building with linux/makefile generators)
 # appears to sometimes de-duplicate redundantly listed dependencies improperly.
 # To work around this, higher level modules should be listed before the modules
 # that they depend upon. -brad
@@ -1985,6 +2010,12 @@ target_link_libraries(${VIEWER_BINARY_NAME}
     ${LLAPPEARANCE_LIBRARIES}
     )
 
+if (BUGSPLAT_DB)
+  target_link_libraries(${VIEWER_BINARY_NAME}
+    ${BUGSPLAT_LIBRARIES}
+    )
+endif (BUGSPLAT_DB)
+
 set(ARTWORK_DIR ${CMAKE_CURRENT_SOURCE_DIR} CACHE PATH
     "Path to artwork files.")
 
@@ -2008,15 +2039,16 @@ if (LINUX)
         ${CMAKE_CURRENT_SOURCE_DIR}/viewer_manifest.py
         --arch=${ARCH}
         --artwork=${ARTWORK_DIR}
+        "--bugsplat=${BUGSPLAT_DB}"
         --build=${CMAKE_CURRENT_BINARY_DIR}
         --buildtype=${CMAKE_BUILD_TYPE}
         "--channel=${VIEWER_CHANNEL}"
-        --versionfile=${CMAKE_CURRENT_BINARY_DIR}/viewer_version.txt
         --configuration=${CMAKE_CFG_INTDIR}
         --dest=${CMAKE_CURRENT_BINARY_DIR}/packaged
         --grid=${GRID}
         --source=${CMAKE_CURRENT_SOURCE_DIR}
         --touch=${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/.${product}.touched
+        --versionfile=${CMAKE_CURRENT_BINARY_DIR}/viewer_version.txt
       DEPENDS
         ${CMAKE_CURRENT_SOURCE_DIR}/viewer_manifest.py
         ${COPY_INPUT_DEPENDENCIES}
@@ -2030,17 +2062,18 @@ if (LINUX)
     COMMAND ${PYTHON_EXECUTABLE}
     ARGS
       ${CMAKE_CURRENT_SOURCE_DIR}/viewer_manifest.py
-      --arch=${ARCH}
       --actions=copy
+      --arch=${ARCH}
       --artwork=${ARTWORK_DIR}
+      "--bugsplat=${BUGSPLAT_DB}"
       --build=${CMAKE_CURRENT_BINARY_DIR}
       --buildtype=${CMAKE_BUILD_TYPE}
+      "--channel=${VIEWER_CHANNEL}"
       --configuration=${CMAKE_CFG_INTDIR}
       --dest=${CMAKE_CURRENT_BINARY_DIR}/packaged
       --grid=${GRID}
-      "--channel=${VIEWER_CHANNEL}"
-      --versionfile=${CMAKE_CURRENT_BINARY_DIR}/viewer_version.txt
       --source=${CMAKE_CURRENT_SOURCE_DIR}
+      --versionfile=${CMAKE_CURRENT_BINARY_DIR}/viewer_version.txt
     DEPENDS
       ${CMAKE_CURRENT_SOURCE_DIR}/viewer_manifest.py
       ${COPY_INPUT_DEPENDENCIES}
@@ -2058,37 +2091,46 @@ if (LINUX)
 endif (LINUX)
 
 if (DARWIN)
-  # These all get set with PROPERTIES
-  set(product "Second Life")
-  # this is the setting for the Python wrapper, see SL-322 and WRAPPER line in Info-SecondLife.plist
-  if (PACKAGE)
-      set(MACOSX_WRAPPER_EXECUTABLE_NAME "SL_Launcher")
-  else (PACKAGE)
-      # force the name of the actual executable to allow running it within Xcode for debugging
-      set(MACOSX_WRAPPER_EXECUTABLE_NAME "../Resources/Second Life Viewer.app/Contents/MacOS/Second Life")
-  endif (PACKAGE)
-  set(MACOSX_BUNDLE_INFO_STRING "Second Life Viewer")
+  # These all get set with PROPERTIES. It's not that the property names are
+  # magically known to CMake -- it's that these names are referenced in the
+  # Info-SecondLife.plist file in the configure_file() directive below.
+  set(product "${VIEWER_CHANNEL}")
+  set(MACOSX_EXECUTABLE_NAME "${VIEWER_CHANNEL}")
+  set(MACOSX_BUNDLE_INFO_STRING "${VIEWER_CHANNEL}")
   set(MACOSX_BUNDLE_ICON_FILE "secondlife.icns")
   set(MACOSX_BUNDLE_GUI_IDENTIFIER "com.secondlife.indra.viewer")
   set(MACOSX_BUNDLE_LONG_VERSION_STRING "${VIEWER_CHANNEL} ${VIEWER_SHORT_VERSION}.${VIEWER_VERSION_REVISION}")
   set(MACOSX_BUNDLE_BUNDLE_NAME "SecondLife")
-  set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${VIEWER_SHORT_VERSION}")
+  set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${VIEWER_SHORT_VERSION}.${VIEWER_VERSION_REVISION}")
   set(MACOSX_BUNDLE_BUNDLE_VERSION "${VIEWER_SHORT_VERSION}${VIEWER_MACOSX_PHASE}${VIEWER_REVISION}")
   set(MACOSX_BUNDLE_COPYRIGHT "Copyright © Linden Research, Inc. 2007")
   set(MACOSX_BUNDLE_NSMAIN_NIB_FILE "SecondLife.nib")
   set(MACOSX_BUNDLE_NSPRINCIPAL_CLASS "NSApplication")
+
+  # https://blog.kitware.com/upcoming-in-cmake-2-8-12-osx-rpath-support/
+  set(CMAKE_MACOSX_RPATH 1)
   
   set_target_properties(
     ${VIEWER_BINARY_NAME}
     PROPERTIES
     OUTPUT_NAME "${product}"
+    # From Contents/MacOS/SecondLife, look in Contents/Frameworks
+    INSTALL_RPATH "@loader_path/../Frameworks"
+    # SIGH, as of 2018-05-24 (cmake 3.11.1) the INSTALL_RPATH property simply
+    # does not work. Try this:
+    LINK_FLAGS "-rpath @loader_path/../Frameworks"
     MACOSX_BUNDLE_INFO_PLIST
     "${CMAKE_CURRENT_SOURCE_DIR}/Info-SecondLife.plist"
     )
 
+  set(VIEWER_APP_BUNDLE "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/${product}.app")
+  set(VIEWER_APP_EXE "${VIEWER_APP_BUNDLE}/Contents/MacOS/${product}")
+  set(VIEWER_APP_DSYM "${VIEWER_APP_EXE}.dSYM")
+  set(VIEWER_APP_XCARCHIVE "${VIEWER_APP_BUNDLE}/../${product}.xcarchive.zip")
+
   configure_file(
      "${CMAKE_CURRENT_SOURCE_DIR}/Info-SecondLife.plist"
-     "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/${product}.app/Contents/Info.plist"
+     "${VIEWER_APP_BUNDLE}/Contents/Info.plist"
     )
 
   add_custom_command(
@@ -2099,15 +2141,16 @@ if (DARWIN)
       --actions=copy
       --arch=${ARCH}
       --artwork=${ARTWORK_DIR}
+      "--bugsplat=${BUGSPLAT_DB}"
       --build=${CMAKE_CURRENT_BINARY_DIR}
       --buildtype=${CMAKE_BUILD_TYPE}
+      --bundleid=${MACOSX_BUNDLE_GUI_IDENTIFIER}
+      "--channel=${VIEWER_CHANNEL}"
       --configuration=${CMAKE_CFG_INTDIR}
-      --dest=${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/${product}.app
+      --dest=${VIEWER_APP_BUNDLE}
       --grid=${GRID}
-      "--channel=${VIEWER_CHANNEL}"
-      --versionfile=${CMAKE_CURRENT_BINARY_DIR}/viewer_version.txt
-      --bundleid=${MACOSX_BUNDLE_GUI_IDENTIFIER}
       --source=${CMAKE_CURRENT_SOURCE_DIR}
+      --versionfile=${CMAKE_CURRENT_BINARY_DIR}/viewer_version.txt
     DEPENDS
       ${VIEWER_BINARY_NAME}
       ${CMAKE_CURRENT_SOURCE_DIR}/viewer_manifest.py
@@ -2132,15 +2175,16 @@ if (DARWIN)
           ${CMAKE_CURRENT_SOURCE_DIR}/viewer_manifest.py
           --arch=${ARCH}
           --artwork=${ARTWORK_DIR}
+          "--bugsplat=${BUGSPLAT_DB}"
           --build=${CMAKE_CURRENT_BINARY_DIR}
           --buildtype=${CMAKE_BUILD_TYPE}
+          "--channel=${VIEWER_CHANNEL}"
           --configuration=${CMAKE_CFG_INTDIR}
-          --dest=${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/${product}.app
+          --dest=${VIEWER_APP_BUNDLE}
           --grid=${GRID}
-          "--channel=${VIEWER_CHANNEL}"
-          --versionfile=${CMAKE_CURRENT_BINARY_DIR}/viewer_version.txt
           --source=${CMAKE_CURRENT_SOURCE_DIR}
           --touch=${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/.${product}.touched
+          --versionfile=${CMAKE_CURRENT_BINARY_DIR}/viewer_version.txt
           ${SIGNING_SETTING}
         DEPENDS
           ${CMAKE_CURRENT_SOURCE_DIR}/viewer_manifest.py
@@ -2152,67 +2196,152 @@ if (INSTALL)
   include(${CMAKE_CURRENT_SOURCE_DIR}/ViewerInstall.cmake)
 endif (INSTALL)
 
-if (PACKAGE)
-  set(SYMBOL_SEARCH_DIRS "")
-  # Note that the path to VIEWER_SYMBOL_FILE must match that in ../../build.sh
-  if (WINDOWS)
-    list(APPEND SYMBOL_SEARCH_DIRS "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}")
-    set(VIEWER_SYMBOL_FILE "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/secondlife-symbols-windows-$ENV{AUTOBUILD_ADDRSIZE}.tar.bz2")
-    # slplugin.exe failing symbols dump - need to debug, might have to do with updated version of google breakpad
-    # set(VIEWER_EXE_GLOBS "${VIEWER_BINARY_NAME}${CMAKE_EXECUTABLE_SUFFIX} slplugin.exe")
-    set(VIEWER_EXE_GLOBS "${VIEWER_BINARY_NAME}${CMAKE_EXECUTABLE_SUFFIX}")
-    set(VIEWER_LIB_GLOB "*${CMAKE_SHARED_MODULE_SUFFIX}")
-    set(VIEWER_COPY_MANIFEST copy_w_viewer_manifest)
-  endif (WINDOWS)
-  if (DARWIN)
-    list(APPEND SYMBOL_SEARCH_DIRS "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}")
-    # *TODO: Generate these search dirs in the cmake files related to each binary.
-    list(APPEND SYMBOL_SEARCH_DIRS "${CMAKE_BINARY_DIR}/llplugin/slplugin/${CMAKE_CFG_INTDIR}")
-    list(APPEND SYMBOL_SEARCH_DIRS "${CMAKE_BINARY_DIR}/mac_crash_logger/${CMAKE_CFG_INTDIR}")
-    list(APPEND SYMBOL_SEARCH_DIRS "${CMAKE_BINARY_DIR}/media_plugins/gstreamer010/${CMAKE_CFG_INTDIR}")
-    set(VIEWER_SYMBOL_FILE "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/secondlife-symbols-darwin-$ENV{AUTOBUILD_ADDRSIZE}.tar.bz2")
-    set(VIEWER_EXE_GLOBS "'Second Life' SLPlugin mac-crash-logger")
-    set(VIEWER_EXE_GLOBS "'Second Life' mac-crash-logger")
-    set(VIEWER_LIB_GLOB "*.dylib")
-  endif (DARWIN)
-  if (LINUX)
-    list(APPEND SYMBOL_SEARCH_DIRS "${CMAKE_CURRENT_BINARY_DIR}/packaged")
-    set(VIEWER_SYMBOL_FILE "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/secondlife-symbols-linux-$ENV{AUTOBUILD_ADDRSIZE}.tar.bz2")
-    set(VIEWER_EXE_GLOBS "do-not-directly-run-secondlife-bin SLPlugin")
-    set(VIEWER_EXE_GLOBS "do-not-directly-run-secondlife-bin")
-    set(VIEWER_LIB_GLOB "*${CMAKE_SHARED_MODULE_SUFFIX}*")
-    set(VIEWER_COPY_MANIFEST copy_l_viewer_manifest)
-  endif (LINUX)
-
-  if(RELEASE_CRASH_REPORTING OR NON_RELEASE_CRASH_REPORTING)
-  if(CMAKE_CFG_INTDIR STREQUAL ".")
-      set(LLBUILD_CONFIG ${CMAKE_BUILD_TYPE})
-  else(CMAKE_CFG_INTDIR STREQUAL ".")
-      # set LLBUILD_CONFIG to be a shell variable evaluated at build time
-      # reflecting the configuration we are currently building.
-      set(LLBUILD_CONFIG ${CMAKE_CFG_INTDIR})
-  endif(CMAKE_CFG_INTDIR STREQUAL ".")
-  add_custom_command(OUTPUT "${VIEWER_SYMBOL_FILE}"
-    COMMAND "${PYTHON_EXECUTABLE}"
-    ARGS
-      "${CMAKE_CURRENT_SOURCE_DIR}/generate_breakpad_symbols.py"
-      "${LLBUILD_CONFIG}"
-      "${SYMBOL_SEARCH_DIRS}"
-      "${VIEWER_EXE_GLOBS}"
-      "${VIEWER_LIB_GLOB}"
-      "${AUTOBUILD_INSTALL_DIR}/bin/dump_syms"
-      "${VIEWER_SYMBOL_FILE}"
-    DEPENDS generate_breakpad_symbols.py
-        VERBATIM)
-
-  add_custom_target(generate_breakpad_symbols DEPENDS "${VIEWER_SYMBOL_FILE}" "${VIEWER_BINARY_NAME}" "${VIEWER_COPY_MANIFEST}")
-  add_dependencies(generate_breakpad_symbols "${VIEWER_BINARY_NAME}")
-  if (WINDOWS OR LINUX)
-    add_dependencies(generate_breakpad_symbols "${VIEWER_COPY_MANIFEST}")
-  endif (WINDOWS OR LINUX)
-  add_dependencies(llpackage generate_breakpad_symbols)
-  endif(RELEASE_CRASH_REPORTING OR NON_RELEASE_CRASH_REPORTING)
-endif (PACKAGE)
+# Note that the conventional VIEWER_SYMBOL_FILE is set by ../../build.sh
+if (PACKAGE AND (RELEASE_CRASH_REPORTING OR NON_RELEASE_CRASH_REPORTING) AND VIEWER_SYMBOL_FILE)
+  if (NOT BUGSPLAT_DB)
+    # Breakpad symbol-file generation
+    set(SYMBOL_SEARCH_DIRS "")
+    if (WINDOWS)
+      list(APPEND SYMBOL_SEARCH_DIRS "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}")
+      # slplugin.exe failing symbols dump - need to debug, might have to do with updated version of google breakpad
+      # set(VIEWER_EXE_GLOBS "${VIEWER_BINARY_NAME}${CMAKE_EXECUTABLE_SUFFIX} slplugin.exe")
+      set(VIEWER_EXE_GLOBS "${VIEWER_BINARY_NAME}${CMAKE_EXECUTABLE_SUFFIX}")
+      set(VIEWER_LIB_GLOB "*${CMAKE_SHARED_MODULE_SUFFIX}")
+      set(VIEWER_COPY_MANIFEST copy_w_viewer_manifest)
+    endif (WINDOWS)
+    if (DARWIN)
+      list(APPEND SYMBOL_SEARCH_DIRS "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}")
+      # *TODO: Generate these search dirs in the cmake files related to each binary.
+      list(APPEND SYMBOL_SEARCH_DIRS "${CMAKE_BINARY_DIR}/llplugin/slplugin/${CMAKE_CFG_INTDIR}")
+      list(APPEND SYMBOL_SEARCH_DIRS "${CMAKE_BINARY_DIR}/mac_crash_logger/${CMAKE_CFG_INTDIR}")
+      list(APPEND SYMBOL_SEARCH_DIRS "${CMAKE_BINARY_DIR}/media_plugins/gstreamer010/${CMAKE_CFG_INTDIR}")
+      set(VIEWER_EXE_GLOBS "'${product}' SLPlugin mac-crash-logger")
+      set(VIEWER_EXE_GLOBS "'${product}' mac-crash-logger")
+      set(VIEWER_LIB_GLOB "*.dylib")
+    endif (DARWIN)
+    if (LINUX)
+      list(APPEND SYMBOL_SEARCH_DIRS "${CMAKE_CURRENT_BINARY_DIR}/packaged")
+      set(VIEWER_EXE_GLOBS "do-not-directly-run-secondlife-bin SLPlugin")
+      set(VIEWER_EXE_GLOBS "do-not-directly-run-secondlife-bin")
+      set(VIEWER_LIB_GLOB "*${CMAKE_SHARED_MODULE_SUFFIX}*")
+      set(VIEWER_COPY_MANIFEST copy_l_viewer_manifest)
+    endif (LINUX)
+
+    if(CMAKE_CFG_INTDIR STREQUAL ".")
+        set(LLBUILD_CONFIG ${CMAKE_BUILD_TYPE})
+    else(CMAKE_CFG_INTDIR STREQUAL ".")
+        # set LLBUILD_CONFIG to be a shell variable evaluated at build time
+        # reflecting the configuration we are currently building.
+        set(LLBUILD_CONFIG ${CMAKE_CFG_INTDIR})
+    endif(CMAKE_CFG_INTDIR STREQUAL ".")
+    add_custom_command(OUTPUT "${VIEWER_SYMBOL_FILE}"
+      COMMAND "${PYTHON_EXECUTABLE}"
+      ARGS
+        "${CMAKE_CURRENT_SOURCE_DIR}/generate_breakpad_symbols.py"
+        "${LLBUILD_CONFIG}"
+        "${SYMBOL_SEARCH_DIRS}"
+        "${VIEWER_EXE_GLOBS}"
+        "${VIEWER_LIB_GLOB}"
+        "${AUTOBUILD_INSTALL_DIR}/bin/dump_syms"
+        "${VIEWER_SYMBOL_FILE}"
+      DEPENDS generate_breakpad_symbols.py
+          VERBATIM)
+
+    add_custom_target(generate_symbols DEPENDS "${VIEWER_SYMBOL_FILE}" ${VIEWER_BINARY_NAME} "${VIEWER_COPY_MANIFEST}")
+    add_dependencies(generate_symbols ${VIEWER_BINARY_NAME})
+    if (WINDOWS OR LINUX)
+      add_dependencies(generate_symbols "${VIEWER_COPY_MANIFEST}")
+    endif (WINDOWS OR LINUX)
+
+  else (NOT BUGSPLAT_DB)
+    # BugSplat symbol-file generation
+    if (WINDOWS)
+      # Just pack up a tarball containing only the .pdb file for the
+      # executable. Because we intend to use cygwin tar, we must render
+      # VIEWER_SYMBOL_FILE in cygwin path syntax.
+      execute_process(COMMAND "cygpath" "-u" "${VIEWER_SYMBOL_FILE}"
+        OUTPUT_VARIABLE VIEWER_SYMBOL_FILE_CYGWIN
+        OUTPUT_STRIP_TRAILING_WHITESPACE)
+      execute_process(COMMAND "cygpath" "-u" "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}"
+        OUTPUT_VARIABLE PARENT_DIRECTORY_CYGWIN
+        OUTPUT_STRIP_TRAILING_WHITESPACE)
+      add_custom_command(OUTPUT "${VIEWER_SYMBOL_FILE}"
+        # Use of 'tar ...j' here assumes VIEWER_SYMBOL_FILE endswith .tar.bz2;
+        # testing a string suffix is painful enough in CMake language that
+        # we'll continue assuming it until forced to generalize.
+        COMMAND "tar"
+        ARGS
+          "cjf"
+          "${VIEWER_SYMBOL_FILE_CYGWIN}"
+          "-C"
+          "${PARENT_DIRECTORY_CYGWIN}"
+          "secondlife-bin.pdb"
+        DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/secondlife-bin.pdb"
+        COMMENT "Packing viewer PDB into ${VIEWER_SYMBOL_FILE_CYGWIN}"
+        )
+      add_custom_target(generate_symbols DEPENDS "${VIEWER_SYMBOL_FILE}" ${VIEWER_BINARY_NAME})
+      add_dependencies(generate_symbols ${VIEWER_BINARY_NAME})
+    endif (WINDOWS)
+    if (DARWIN)
+      # Have to run dsymutil first, then pack up the resulting .dSYM directory
+      add_custom_command(OUTPUT "${VIEWER_APP_DSYM}"
+        COMMAND "dsymutil"
+        ARGS
+          ${VIEWER_APP_EXE}
+        COMMENT "Generating ${VIEWER_APP_DSYM}"
+        )
+      add_custom_target(dsym_generate DEPENDS "${VIEWER_APP_DSYM}")
+      add_dependencies(dsym_generate ${VIEWER_BINARY_NAME})
+      add_custom_command(OUTPUT "${VIEWER_SYMBOL_FILE}"
+        # See above comments about "tar ...j"
+        COMMAND "tar"
+        ARGS
+          "cjf"
+          "${VIEWER_SYMBOL_FILE}"
+          "-C"
+          "${VIEWER_APP_DSYM}/.."
+          "${product}.dSYM"
+        DEPENDS "${VIEWER_APP_DSYM}"
+        COMMENT "Packing dSYM into ${VIEWER_SYMBOL_FILE}"
+        )
+      add_custom_target(dsym_tarball DEPENDS "${VIEWER_SYMBOL_FILE}")
+      add_dependencies(dsym_tarball dsym_generate)
+      add_custom_command(OUTPUT "${VIEWER_APP_XCARCHIVE}"
+        COMMAND "zip"
+        ARGS
+          "-r"
+          "${VIEWER_APP_XCARCHIVE}"
+          "."
+        WORKING_DIRECTORY "${VIEWER_APP_DSYM}/.."
+        DEPENDS "${VIEWER_APP_DSYM}"
+        COMMENT "Generating xcarchive.zip for upload to BugSplat"
+        )
+      add_custom_target(dsym_xcarchive DEPENDS "${VIEWER_APP_XCARCHIVE}")
+      add_dependencies(dsym_xcarchive dsym_generate)
+      # Have to create a stamp file, and depend on it, to force CMake to run
+      # the cleanup step.
+      add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/dsym.stamp"
+        COMMAND rm -rf "${VIEWER_APP_DSYM}"
+        COMMAND touch "${CMAKE_CURRENT_BINARY_DIR}/dsym.stamp"
+        DEPENDS "${VIEWER_SYMBOL_FILE}" "${VIEWER_APP_XCARCHIVE}"
+        COMMENT "Cleaning up dSYM"
+        )
+      add_custom_target(generate_symbols DEPENDS
+        "${VIEWER_APP_DSYM}"
+        "${VIEWER_SYMBOL_FILE}"
+        "${VIEWER_APP_XCARCHIVE}"
+        "${CMAKE_CURRENT_BINARY_DIR}/dsym.stamp"
+        )
+      add_dependencies(generate_symbols dsym_tarball dsym_xcarchive)
+    endif (DARWIN)
+    if (LINUX)
+      # TBD
+    endif (LINUX)
+  endif (NOT BUGSPLAT_DB)
+
+  # for both BUGSPLAT_DB and Breakpad
+  add_dependencies(llpackage generate_symbols)
+endif ()
 
 if (LL_TESTS)
   # To add a viewer unit test, just add the test .cpp file below
diff --git a/indra/newview/Info-SecondLife.plist b/indra/newview/Info-SecondLife.plist
index af4cf26ac6838e08e7ee9fd4352a2d79516acfb1..cfe9d991c5a90c803acca5ce64ea101d47a2c480 100644
--- a/indra/newview/Info-SecondLife.plist
+++ b/indra/newview/Info-SecondLife.plist
@@ -5,7 +5,7 @@
 	<key>CFBundleDevelopmentRegion</key>
 	<string>English</string>
 	<key>CFBundleExecutable</key>
-	<string>${MACOSX_WRAPPER_EXECUTABLE_NAME}</string>
+	<string>${MACOSX_EXECUTABLE_NAME}</string>
 	<key>CFBundleGetInfoString</key>
 	<string>${MACOSX_BUNDLE_INFO_STRING}</string>
 	<key>CFBundleIconFile</key>
@@ -21,7 +21,7 @@
 	<key>CFBundlePackageType</key>
 	<string>APPL</string>
 	<key>CFBundleShortVersionString</key>
-	<string>${MACOSX_BUNDLE_LONG_VERSION_STRING}</string>
+	<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
 	<key>CFBundleSignature</key>
 	<string>????</string>
 	<key>CFBundleVersion</key>
@@ -32,6 +32,8 @@
 	<true/>
 	<key>NSHumanReadableCopyright</key>
 	<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
+	<key>NSMicrophoneUsageDescription</key>
+	<string>For voice chat, you must grant permission for Second Life to use the microphone.</string>
 	<key>CFBundleDocumentTypes</key>
 	<array>
 		<dict>
diff --git a/indra/newview/VIEWER_VERSION.txt b/indra/newview/VIEWER_VERSION.txt
index 9b9a244206f6ab79c1155fa07c154d15d4ac54ab..dfda3e0b4f011bb0ab65e8585ae0ba122cf8f120 100644
--- a/indra/newview/VIEWER_VERSION.txt
+++ b/indra/newview/VIEWER_VERSION.txt
@@ -1 +1 @@
-6.0.2
+6.1.0
diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml
index 3ad8b6cdedd42f2f472dd27620c0fc0ba4232183..6e7ddd0bf42fbfbad9636b51c102eb476d6f41d5 100644
--- a/indra/newview/app_settings/settings.xml
+++ b/indra/newview/app_settings/settings.xml
@@ -4404,6 +4404,17 @@
       <key>Value</key>
       <real>96.0</real>
     </map>
+    <key>ForceAddressSize</key>
+    <map>
+      <key>Comment</key>
+      <string>Force Windows update to 32-bit or 64-bit viewer.</string>
+      <key>Persist</key>
+      <integer>1</integer>
+      <key>Type</key>
+      <string>U32</string>
+      <key>Value</key>
+      <integer>0</integer>
+    </map>
     <key>ForceAssetFail</key>
     <map>
       <key>Comment</key>
@@ -13587,7 +13598,7 @@
     <key>UpdaterServiceURL</key>
     <map>
       <key>Comment</key>
-      <string>Default location for the updater service.</string>
+      <string>Obsolete; no longer used.</string>
       <key>Persist</key>
       <integer>0</integer>
       <key>Type</key>
@@ -14068,17 +14079,6 @@
       <key>Value</key>
       <integer>1</integer>
     </map>
-    <key>VerboseLogs</key>
-    <map>
-      <key>Comment</key>
-      <string>Display source file and line number for each log item for debugging purposes</string>
-      <key>Persist</key>
-      <integer>1</integer>
-      <key>Type</key>
-      <string>Boolean</string>
-      <key>Value</key>
-      <integer>0</integer>
-    </map>
     <key>VertexShaderEnable</key>
     <map>
       <key>Comment</key>
@@ -16260,7 +16260,7 @@
       <string>if true, disables running the GPU benchmark at startup
       (default to class 1)</string>
       <key>Persist</key>
-      <integer>0</integer>
+      <integer>1</integer>
       <key>Type</key>
       <string>Boolean</string>
       <key>Value</key>
diff --git a/indra/newview/installers/windows/installer_template.nsi b/indra/newview/installers/windows/installer_template.nsi
index 14c8dba39f19c2ff18f4f9f583571371c9415201..4afef4630ca50a5f64115b80609717848607c88e 100644
--- a/indra/newview/installers/windows/installer_template.nsi
+++ b/indra/newview/installers/windows/installer_template.nsi
@@ -18,8 +18,7 @@
 ;;
 ;; Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
 ;;
-;; NSIS Unicode 2.46.5 or higher required
-;; http://www.scratchpaper.com/
+;; NSIS 3 or higher required for Unicode support
 ;;
 ;; Author: James Cook, TankMaster Finesmith, Don Kjer, Callum Prentice
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -27,6 +26,7 @@
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;; Compiler flags
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+Unicode true
 SetOverwrite on				# Overwrite files
 SetCompress auto			# Compress if saves space
 SetCompressor /solid lzma	# Compress whole installer as one block
@@ -46,28 +46,33 @@ RequestExecutionLevel admin	# For when we write to Program Files
 ;; (these files are in the same place as the nsi template but the python script generates a new nsi file in the 
 ;; application directory so we have to add a path to these include files)
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-!include "%%SOURCE%%\installers\windows\lang_da.nsi"
-!include "%%SOURCE%%\installers\windows\lang_de.nsi"
+;; Ansariel notes: "Under certain circumstances the installer will fall back
+;; to the first defined (aka default) language version. So you want to include
+;; en-us as first language file."
 !include "%%SOURCE%%\installers\windows\lang_en-us.nsi"
+
+# Danish and Polish no longer supported by the viewer itself
+##!include "%%SOURCE%%\installers\windows\lang_da.nsi"
+!include "%%SOURCE%%\installers\windows\lang_de.nsi"
 !include "%%SOURCE%%\installers\windows\lang_es.nsi"
 !include "%%SOURCE%%\installers\windows\lang_fr.nsi"
 !include "%%SOURCE%%\installers\windows\lang_ja.nsi"
 !include "%%SOURCE%%\installers\windows\lang_it.nsi"
-!include "%%SOURCE%%\installers\windows\lang_pl.nsi"
+##!include "%%SOURCE%%\installers\windows\lang_pl.nsi"
 !include "%%SOURCE%%\installers\windows\lang_pt-br.nsi"
 !include "%%SOURCE%%\installers\windows\lang_ru.nsi"
 !include "%%SOURCE%%\installers\windows\lang_tr.nsi"
 !include "%%SOURCE%%\installers\windows\lang_zh.nsi"
 
 # *TODO: Move these into the language files themselves
-LangString LanguageCode ${LANG_DANISH}   "da"
+##LangString LanguageCode ${LANG_DANISH}   "da"
 LangString LanguageCode ${LANG_GERMAN}   "de"
 LangString LanguageCode ${LANG_ENGLISH}  "en"
 LangString LanguageCode ${LANG_SPANISH}  "es"
 LangString LanguageCode ${LANG_FRENCH}   "fr"
 LangString LanguageCode ${LANG_JAPANESE} "ja"
 LangString LanguageCode ${LANG_ITALIAN}  "it"
-LangString LanguageCode ${LANG_POLISH}   "pl"
+##LangString LanguageCode ${LANG_POLISH}   "pl"
 LangString LanguageCode ${LANG_PORTUGUESEBR} "pt"
 LangString LanguageCode ${LANG_RUSSIAN}  "ru"
 LangString LanguageCode ${LANG_TURKISH}  "tr"
@@ -80,9 +85,12 @@ Name ${INSTNAME}
 
 SubCaption 0 $(LicenseSubTitleSetup)	# Override "license agreement" text
 
+!define MUI_ICON   "%%SOURCE%%\installers\windows\install_icon.ico"
+!define MUI_UNICON "%%SOURCE%%\installers\windows\uninstall_icon.ico"
+
 BrandingText " "						# Bottom of window text
-Icon          %%SOURCE%%\installers\windows\install_icon.ico
-UninstallIcon %%SOURCE%%\installers\windows\uninstall_icon.ico
+Icon          "${MUI_ICON}"
+UninstallIcon "${MUI_UNICON}"
 WindowIcon on							# Show our icon in left corner
 BGGradient off							# No big background window
 CRCCheck on								# Make sure CRC is OK
@@ -90,20 +98,49 @@ InstProgressFlags smooth colored		# New colored smooth look
 SetOverwrite on							# Overwrite files by default
 AutoCloseWindow true					# After all files install, close window
 
-# initial location of install (default when not already installed)
-#   note: Now we defer looking for existing install until onInit when we
-#   are able to engage the 32/64 registry function
-InstallDir "%%PROGRAMFILES%%\${INSTNAME}"
+# Registry key paths, ours and Microsoft's
+!define LINDEN_KEY      "SOFTWARE\Linden Research, Inc."
+!define INSTNAME_KEY    "${LINDEN_KEY}\${INSTNAME}"
+!define MSCURRVER_KEY   "SOFTWARE\Microsoft\Windows\CurrentVersion"
+!define MSNTCURRVER_KEY "SOFTWARE\Microsoft\Windows NT\CurrentVersion"
+!define MSUNINSTALL_KEY "${MSCURRVER_KEY}\Uninstall\${INSTNAME}"
+
+# from http://nsis.sourceforge.net/Docs/MultiUser/Readme.html
+### Highest level permitted for user: Admin for Admin, Standard for Standard
+##!define MULTIUSER_EXECUTIONLEVEL Highest
+!define MULTIUSER_EXECUTIONLEVEL Admin
+!define MULTIUSER_MUI
+### Look for /AllUsers or /CurrentUser switches
+##!define MULTIUSER_INSTALLMODE_COMMANDLINE
+# appended to $PROGRAMFILES, as affected by MULTIUSER_USE_PROGRAMFILES64
+!define MULTIUSER_INSTALLMODE_INSTDIR "${INSTNAME}"
+# expands to !define MULTIUSER_USE_PROGRAMFILES64 or nothing
+%%PROGRAMFILES%%
+# should make MultiUser.nsh initialization read existing INSTDIR from registry
+!define MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_KEY "${INSTNAME_KEY}"
+!define MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_VALUENAME ""
+# Don't set MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_KEY and
+# MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_VALUENAME to cause the installer to
+# write $MultiUser.InstallMode to the registry, because when the user installs
+# multiple viewers with the same channel (same ${INSTNAME}, hence same
+# ${INSTNAME_KEY}), the registry entry is overwritten. Instead we'll write a
+# little file into the install directory -- see .onInstSuccess and un.onInit.
+!include MultiUser.nsh
+!include MUI2.nsh
+!define MUI_BGCOLOR FFFFFF
+!insertmacro MUI_FUNCTION_GUIINIT
 
 UninstallText $(UninstallTextMsg)
 DirText $(DirectoryChooseTitle) $(DirectoryChooseSetup)
-Page directory dirPre
-Page instfiles
+##!insertmacro MULTIUSER_PAGE_INSTALLMODE
+!define MUI_PAGE_CUSTOMFUNCTION_PRE dirPre
+!insertmacro MUI_PAGE_DIRECTORY
+!insertmacro MUI_PAGE_INSTFILES
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;; Variables
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-Var INSTPROG
+Var INSTNAME
 Var INSTEXE
 Var VIEWER_EXE
 Var INSTSHORTCUT
@@ -142,20 +179,13 @@ FunctionEnd
 ;; entry to the language ID selector below
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 Function .onInit
+!insertmacro MULTIUSER_INIT
 
 %%ENGAGEREGISTRY%%
 
-# read the current location of the install for this version
-# if $0 is empty, this is the first time for this viewer name
-ReadRegStr $0 HKEY_LOCAL_MACHINE "SOFTWARE\\Linden Research, Inc.\\${INSTNAME}" ""
-
-# viewer with this name not installed before
-${If} $0 == ""
-    # nothing to do here
-${Else}
-	# use the value we got from registry as install location
-    StrCpy $INSTDIR $0
-${EndIf}
+# Setting MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_KEY and
+# MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_VALUENAME should
+# read the current location of the install for this version into INSTDIR.
 
 Call CheckCPUFlags							# Make sure we have SSE2 support
 Call CheckWindowsVersion					# Don't install On unsupported systems
@@ -181,7 +211,7 @@ Call CheckWindowsVersion					# Don't install On unsupported systems
 lbl_configure_default_lang:
 # If we currently have a version of SL installed, default to the language of that install
 # Otherwise don't change $LANGUAGE and it will default to the OS UI language.
-    ReadRegStr $0 HKEY_LOCAL_MACHINE "SOFTWARE\Linden Research, Inc.\${INSTNAME}" "InstallerLanguage"
+    ReadRegStr $0 SHELL_CONTEXT "${INSTNAME_KEY}" "InstallerLanguage"
     IfErrors +2 0	# If error skip the copy instruction 
 	StrCpy $LANGUAGE $0
 
@@ -191,7 +221,6 @@ lbl_configure_default_lang:
     Goto lbl_return
     StrCmp $SKIP_DIALOGS "true" lbl_return
   
-lbl_build_menu:
 	Push ""
 # Use separate file so labels can be UTF-16 but we can still merge changes into this ASCII file. JC
     !include "%%SOURCE%%\installers\windows\language_menu.nsi"
@@ -204,7 +233,7 @@ lbl_build_menu:
     StrCpy $LANGUAGE $0
 
 # Save language in registry		
-	WriteRegStr HKEY_LOCAL_MACHINE "SOFTWARE\Linden Research, Inc.\${INSTNAME}" "InstallerLanguage" $LANGUAGE
+	WriteRegStr SHELL_CONTEXT "${INSTNAME_KEY}" "InstallerLanguage" $LANGUAGE
 lbl_return:
     Pop $0
     Return
@@ -215,14 +244,32 @@ FunctionEnd
 ;; Prep Uninstaller Section
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 Function un.onInit
+    # Save $INSTDIR -- it appears to have the correct value before
+    # MULTIUSER_UNINIT, but then gets munged by MULTIUSER_UNINIT?!
+    Push $INSTDIR
+    !insertmacro MULTIUSER_UNINIT
+    Pop $INSTDIR
+
+    # Now read InstallMode.txt from $INSTDIR
+    Push $0
+    ClearErrors
+    FileOpen $0 "$INSTDIR\InstallMode.txt" r
+    IfErrors skipread
+    FileRead $0 $MultiUser.InstallMode
+    FileClose $0
+skipread:
+    Pop $0
 
 %%ENGAGEREGISTRY%%
 
 # Read language from registry and set for uninstaller. Key will be removed on successful uninstall
-	ReadRegStr $0 HKEY_LOCAL_MACHINE "SOFTWARE\Linden Research, Inc.\${INSTNAME}" "InstallerLanguage"
+	ReadRegStr $0 SHELL_CONTEXT "${INSTNAME_KEY}" "InstallerLanguage"
     IfErrors lbl_end
 	StrCpy $LANGUAGE $0
 lbl_end:
+
+##  MessageBox MB_OK "After restoring:$\n$$INSTDIR = '$INSTDIR'$\n$$MultiUser.InstallMode = '$MultiUser.InstallMode'$\n$$LANGUAGE = '$LANGUAGE'"
+
     Return
 
 FunctionEnd
@@ -272,10 +319,10 @@ FunctionEnd
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 Section ""
 
-SetShellVarContext all			# Install for all users (if you change this, change it in the uninstall as well)
+# SetShellVarContext is set by MultiUser.nsh initialization.
 
 # Start with some default values.
-StrCpy $INSTPROG "${INSTNAME}"
+StrCpy $INSTNAME "${INSTNAME}"
 StrCpy $INSTEXE "${INSTEXE}"
 StrCpy $VIEWER_EXE "${VIEWER_EXE}"
 StrCpy $INSTSHORTCUT "${SHORTCUT}"
@@ -299,7 +346,7 @@ StrCpy $SHORTCUT_LANG_PARAM "--set InstallLanguage $(LanguageCode)"
 CreateDirectory	"$SMPROGRAMS\$INSTSHORTCUT"
 SetOutPath "$INSTDIR"
 CreateShortCut	"$SMPROGRAMS\$INSTSHORTCUT\$INSTSHORTCUT.lnk" \
-				"$INSTDIR\$INSTEXE" "$SHORTCUT_LANG_PARAM" "$INSTDIR\$VIEWER_EXE"
+				"$INSTDIR\$VIEWER_EXE" "$SHORTCUT_LANG_PARAM" "$INSTDIR\$VIEWER_EXE"
 
 
 WriteINIStr		"$SMPROGRAMS\$INSTSHORTCUT\SL Create Account.url" \
@@ -317,31 +364,31 @@ CreateShortCut	"$SMPROGRAMS\$INSTSHORTCUT\Uninstall $INSTSHORTCUT.lnk" \
 # Other shortcuts
 SetOutPath "$INSTDIR"
 CreateShortCut "$DESKTOP\$INSTSHORTCUT.lnk" \
-        "$INSTDIR\$INSTEXE" "$SHORTCUT_LANG_PARAM" "$INSTDIR\$VIEWER_EXE"
+        "$INSTDIR\$VIEWER_EXE" "$SHORTCUT_LANG_PARAM" "$INSTDIR\$VIEWER_EXE"
 CreateShortCut "$INSTDIR\$INSTSHORTCUT.lnk" \
-        "$INSTDIR\$INSTEXE" "$SHORTCUT_LANG_PARAM" "$INSTDIR\$VIEWER_EXE"
+        "$INSTDIR\$VIEWER_EXE" "$SHORTCUT_LANG_PARAM" "$INSTDIR\$VIEWER_EXE"
 CreateShortCut "$INSTDIR\Uninstall $INSTSHORTCUT.lnk" \
 				'"$INSTDIR\uninst.exe"' ''
 
 # Write registry
-WriteRegStr HKEY_LOCAL_MACHINE "SOFTWARE\Linden Research, Inc.\$INSTPROG" "" "$INSTDIR"
-WriteRegStr HKEY_LOCAL_MACHINE "SOFTWARE\Linden Research, Inc.\$INSTPROG" "Version" "${VERSION_LONG}"
-WriteRegStr HKEY_LOCAL_MACHINE "SOFTWARE\Linden Research, Inc.\$INSTPROG" "Shortcut" "$INSTSHORTCUT"
-WriteRegStr HKEY_LOCAL_MACHINE "SOFTWARE\Linden Research, Inc.\$INSTPROG" "Exe" "$INSTEXE"
-WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\$INSTPROG" "Publisher" "Linden Research, Inc."
-WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\$INSTPROG" "URLInfoAbout" "http://secondlife.com/whatis/"
-WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\$INSTPROG" "URLUpdateInfo" "http://secondlife.com/support/downloads/"
-WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\$INSTPROG" "HelpLink" "https://support.secondlife.com/contact-support/"
-WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\$INSTPROG" "DisplayName" "$INSTPROG"
-WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\$INSTPROG" "UninstallString" '"$INSTDIR\uninst.exe"'
-WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\$INSTPROG" "DisplayVersion" "${VERSION_LONG}"
-WriteRegDWORD HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\$INSTPROG" "EstimatedSize" "0x0001D500"		# ~117 MB
+WriteRegStr SHELL_CONTEXT "${INSTNAME_KEY}" "" "$INSTDIR"
+WriteRegStr SHELL_CONTEXT "${INSTNAME_KEY}" "Version" "${VERSION_LONG}"
+WriteRegStr SHELL_CONTEXT "${INSTNAME_KEY}" "Shortcut" "$INSTSHORTCUT"
+WriteRegStr SHELL_CONTEXT "${INSTNAME_KEY}" "Exe" "$VIEWER_EXE"
+WriteRegStr SHELL_CONTEXT "${MSUNINSTALL_KEY}" "Publisher" "Linden Research, Inc."
+WriteRegStr SHELL_CONTEXT "${MSUNINSTALL_KEY}" "URLInfoAbout" "http://secondlife.com/whatis/"
+WriteRegStr SHELL_CONTEXT "${MSUNINSTALL_KEY}" "URLUpdateInfo" "http://secondlife.com/support/downloads/"
+WriteRegStr SHELL_CONTEXT "${MSUNINSTALL_KEY}" "HelpLink" "https://support.secondlife.com/contact-support/"
+WriteRegStr SHELL_CONTEXT "${MSUNINSTALL_KEY}" "DisplayName" "$INSTNAME"
+WriteRegStr SHELL_CONTEXT "${MSUNINSTALL_KEY}" "UninstallString" '"$INSTDIR\uninst.exe"'
+WriteRegStr SHELL_CONTEXT "${MSUNINSTALL_KEY}" "DisplayVersion" "${VERSION_LONG}"
+WriteRegDWORD SHELL_CONTEXT "${MSUNINSTALL_KEY}" "EstimatedSize" "0x0001D500"		# ~117 MB
 
 # from FS:Ansariel
-WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\$INSTPROG" "DisplayIcon" '"$INSTDIR\$INSTEXE"'
+WriteRegStr SHELL_CONTEXT "${MSUNINSTALL_KEY}" "DisplayIcon" '"$INSTDIR\$VIEWER_EXE"'
 
 # BUG-2707 Disable SEHOP for installed viewer.
-WriteRegDWORD HKEY_LOCAL_MACHINE "Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\$INSTEXE" "DisableExceptionChainValidation" 1
+WriteRegDWORD SHELL_CONTEXT "${MSNTCURRVER_KEY}\Image File Execution Options\$VIEWER_EXE" "DisableExceptionChainValidation" 1
 
 # Write URL registry info
 WriteRegStr HKEY_CLASSES_ROOT "${URLNAME}" "(default)" "URL:Second Life"
@@ -358,9 +405,8 @@ WriteRegStr HKEY_CLASSES_ROOT "x-grid-location-info\DefaultIcon" "" '"$INSTDIR\$
 # URL param must be last item passed to viewer, it ignores subsequent params to avoid parameter injection attacks.
 WriteRegExpandStr HKEY_CLASSES_ROOT "x-grid-location-info\shell\open\command" "" '"$INSTDIR\$VIEWER_EXE" -url "%1"'
 
-# Only allow Launcher to be the icon
-WriteRegStr HKEY_CLASSES_ROOT "Applications\$INSTEXE" "IsHostApp" ""
-WriteRegStr HKEY_CLASSES_ROOT "Applications\${VIEWER_EXE}" "NoStartPage" ""
+WriteRegStr HKEY_CLASSES_ROOT "Applications\$VIEWER_EXE" "IsHostApp" ""
+##WriteRegStr HKEY_CLASSES_ROOT "Applications\${VIEWER_EXE}" "NoStartPage" ""
 
 # Write out uninstaller
 WriteUninstaller "$INSTDIR\uninst.exe"
@@ -381,25 +427,32 @@ SectionEnd
 Section Uninstall
 
 # Start with some default values.
-StrCpy $INSTPROG "${INSTNAME}"
+StrCpy $INSTNAME "${INSTNAME}"
 StrCpy $INSTEXE "${INSTEXE}"
+StrCpy $VIEWER_EXE "${VIEWER_EXE}"
 StrCpy $INSTSHORTCUT "${SHORTCUT}"
 
-# Make sure the user can install/uninstall
-Call un.CheckIfAdministrator
-
-# Uninstall for all users (if you change this, change it in the install as well)
-SetShellVarContext all			
+# SetShellVarContext per the mode saved at install time in registry at
+# MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_KEY
+# MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_VALUENAME
+# Couldn't get NSIS to expand $MultiUser.InstallMode into the function name at Call time
+${If} $MultiUser.InstallMode == 'AllUsers'
+##MessageBox MB_OK "Uninstalling for all users"
+  Call un.MultiUser.InstallMode.AllUsers
+${Else}
+##MessageBox MB_OK "Uninstalling for current user"
+  Call un.MultiUser.InstallMode.CurrentUser
+${EndIf}
 
 # Make sure we're not running
 Call un.CloseSecondLife
 
 # Clean up registry keys and subkeys (these should all be !defines somewhere)
-DeleteRegKey HKEY_LOCAL_MACHINE "SOFTWARE\Linden Research, Inc.\$INSTPROG"
-DeleteRegKey HKEY_LOCAL_MACHINE "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$INSTPROG"
+DeleteRegKey SHELL_CONTEXT "${INSTNAME_KEY}"
+DeleteRegKey SHELL_CONTEXT "${MSCURRVER_KEY}\Uninstall\$INSTNAME"
 # BUG-2707 Remove entry that disabled SEHOP
-DeleteRegKey HKEY_LOCAL_MACHINE "Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\$INSTEXE"
-DeleteRegKey HKEY_CLASSES_ROOT "Applications\$INSTEXE"
+DeleteRegKey SHELL_CONTEXT "${MSNTCURRVER_KEY}\Image File Execution Options\$VIEWER_EXE"
+##DeleteRegKey HKEY_CLASSES_ROOT "Applications\$INSTEXE"
 DeleteRegKey HKEY_CLASSES_ROOT "Applications\${VIEWER_EXE}"
 
 # Clean up shortcuts
@@ -537,6 +590,7 @@ Function RemoveProgFilesOnInst
 
 # Remove old SecondLife.exe to invalidate any old shortcuts to it that may be in non-standard locations. See MAINT-3575
 Delete "$INSTDIR\$INSTEXE"
+Delete "$INSTDIR\$VIEWER_EXE"
 
 # Remove old shader files first so fallbacks will work. See DEV-5663
 RMDir /r "$INSTDIR\app_settings\shaders"
@@ -570,10 +624,10 @@ Push $2
   StrCpy $0 0	# Index number used to iterate via EnumRegKey
 
   LOOP:
-    EnumRegKey $1 HKEY_LOCAL_MACHINE "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" $0
+    EnumRegKey $1 SHELL_CONTEXT "${MSNTCURRVER_KEY}\ProfileList" $0
     StrCmp $1 "" DONE               # No more users
 
-    ReadRegStr $2 HKEY_LOCAL_MACHINE "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$1" "ProfileImagePath" 
+    ReadRegStr $2 SHELL_CONTEXT "${MSNTCURRVER_KEY}\ProfileList\$1" "ProfileImagePath" 
     StrCmp $2 "" CONTINUE 0         # "ProfileImagePath" value is missing
 
 # Required since ProfileImagePath is of type REG_EXPAND_SZ
@@ -603,7 +657,7 @@ Pop $0
 
 # Delete files in ProgramData\Secondlife
 Push $0
-  ReadRegStr $0 HKEY_LOCAL_MACHINE "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders" "Common AppData"
+  ReadRegStr $0 SHELL_CONTEXT "${MSCURRVER_KEY}\Explorer\Shell Folders" "Common AppData"
   StrCmp $0 "" +2
   RMDir /r "$0\SecondLife"
 Pop $0
@@ -624,6 +678,9 @@ Function un.ProgramFiles
 # This placeholder is replaced by the complete list of files to uninstall by viewer_manifest.py
 %%DELETE_FILES%%
 
+# our InstallMode.txt
+Delete "$INSTDIR\InstallMode.txt"
+
 # Optional/obsolete files.  Delete won't fail if they don't exist.
 Delete "$INSTDIR\autorun.bat"
 Delete "$INSTDIR\dronesettings.ini"
@@ -660,8 +717,8 @@ NOFOLDER:
 MessageBox MB_YESNO $(DeleteRegistryKeysMB) IDYES DeleteKeys IDNO NoDelete
 
 DeleteKeys:
-  DeleteRegKey HKEY_LOCAL_MACHINE "SOFTWARE\Classes\x-grid-location-info"
-  DeleteRegKey HKEY_LOCAL_MACHINE "SOFTWARE\Classes\secondlife"
+  DeleteRegKey SHELL_CONTEXT "SOFTWARE\Classes\x-grid-location-info"
+  DeleteRegKey SHELL_CONTEXT "SOFTWARE\Classes\secondlife"
   DeleteRegKey HKEY_CLASSES_ROOT "x-grid-location-info"
   DeleteRegKey HKEY_CLASSES_ROOT "secondlife"
 
@@ -673,7 +730,13 @@ FunctionEnd
 ;; After install completes, launch app
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 Function .onInstSuccess
-Call CheckWindowsServPack		# Warn if not on the latest SP before asking to launch.
+        Push $0
+        FileOpen $0 "$INSTDIR\InstallMode.txt" w
+        # No newline -- this is for our use, not for users to read.
+        FileWrite $0 "$MultiUser.InstallMode"
+        FileClose $0
+        Pop $0
+
         Push $R0
         Push $0
         ;; MAINT-7812: Only write nsis.winstall file with /marker switch
@@ -692,11 +755,24 @@ Call CheckWindowsServPack		# Warn if not on the latest SP before asking to launc
         ClearErrors
         Pop $0
         Pop $R0
-        Push $R0					# Option value, unused# 
+
+        Call CheckWindowsServPack		# Warn if not on the latest SP before asking to launch.
         StrCmp $SKIP_AUTORUN "true" +2;
-# Assumes SetOutPath $INSTDIR
-	Exec '"$WINDIR\explorer.exe" "$INSTDIR\$INSTSHORTCUT.lnk"'
-        Pop $R0
+        # Assumes SetOutPath $INSTDIR
+        # Run INSTEXE (our updater), passing VIEWER_EXE plus the command-line
+        # arguments built into our shortcuts. This gives the updater a chance
+        # to verify that the viewer we just installed is appropriate for the
+        # running system -- or, if not, to download and install a different
+        # viewer. For instance, if a user running 32-bit Windows installs a
+        # 64-bit viewer, it cannot run on this system. But since the updater
+        # is a 32-bit executable even in the 64-bit viewer package, the
+        # updater can detect the problem and adapt accordingly.
+        # Once everything is in order, the updater will run the specified
+        # viewer with the specified params.
+        # Quote the updater executable and the viewer executable because each
+        # must be a distinct command-line token, but DO NOT quote the language
+        # string because it must decompose into separate command-line tokens.
+        Exec '"$INSTDIR\$INSTEXE" precheck "$INSTDIR\$VIEWER_EXE" $SHORTCUT_LANG_PARAM'
 # 
 FunctionEnd
 
@@ -733,10 +809,10 @@ FunctionEnd
 ;    StrCpy $0 0	# Index number used to iterate via EnumRegKey
 ;
 ;  LOOP:
-;    EnumRegKey $1 HKEY_LOCAL_MACHINE "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" $0
+;    EnumRegKey $1 SHELL_CONTEXT "${MSNTCURRVER_KEY}\ProfileList" $0
 ;    StrCmp $1 "" DONE               # no more users
 ;
-;    ReadRegStr $2 HKEY_LOCAL_MACHINE "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$1" "ProfileImagePath"
+;    ReadRegStr $2 SHELL_CONTEXT "${MSNTCURRVER_KEY}\ProfileList\$1" "ProfileImagePath"
 ;    StrCmp $2 "" CONTINUE 0         # "ProfileImagePath" value is missing
 ;
 ;# Required since ProfileImagePath is of type REG_EXPAND_SZ
@@ -755,7 +831,7 @@ FunctionEnd
 ;
 ;# Copy files in Documents and Settings\All Users\SecondLife
 ;Push $0
-;    ReadRegStr $0 HKEY_LOCAL_MACHINE "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders" "Common AppData"
+;    ReadRegStr $0 SHELL_CONTEXT "${MSCURRVER_KEY}\Explorer\Shell Folders" "Common AppData"
 ;    StrCmp $0 "" +2
 ;    RMDir /r "$2\Application Data\SecondLife\"
 ;Pop $0
diff --git a/indra/newview/installers/windows/lang_da.nsi b/indra/newview/installers/windows/lang_da.nsi
index 83e1a3ea9417bd1565c93b90b5c8070d66548415..f462c82078800e8e47544b724af691e9f5e01701 100644
Binary files a/indra/newview/installers/windows/lang_da.nsi and b/indra/newview/installers/windows/lang_da.nsi differ
diff --git a/indra/newview/installers/windows/lang_de.nsi b/indra/newview/installers/windows/lang_de.nsi
index 2a868acc89cd7b516ff07df69df5b95630dc442f..6abd496849b5421deb5edcd1f6d57b76101e0383 100644
Binary files a/indra/newview/installers/windows/lang_de.nsi and b/indra/newview/installers/windows/lang_de.nsi differ
diff --git a/indra/newview/installers/windows/lang_en-us.nsi b/indra/newview/installers/windows/lang_en-us.nsi
index 00aa47de690a2a066ab023bccfe05a7559c3c938..fd4d340816abc487166547c2ec01d4a9ac58cf88 100644
Binary files a/indra/newview/installers/windows/lang_en-us.nsi and b/indra/newview/installers/windows/lang_en-us.nsi differ
diff --git a/indra/newview/installers/windows/lang_es.nsi b/indra/newview/installers/windows/lang_es.nsi
index 1ecf254ffb484c59b8df0f2b50916a78045447e1..5daac908c974b3b8e696600175383ae035ecf904 100644
Binary files a/indra/newview/installers/windows/lang_es.nsi and b/indra/newview/installers/windows/lang_es.nsi differ
diff --git a/indra/newview/installers/windows/lang_fr.nsi b/indra/newview/installers/windows/lang_fr.nsi
index bec5835bed9671c46d335cd618fb3629a7d10be9..c437a0bdb466b0dec068de82cb7abd9b133bccec 100644
Binary files a/indra/newview/installers/windows/lang_fr.nsi and b/indra/newview/installers/windows/lang_fr.nsi differ
diff --git a/indra/newview/installers/windows/lang_it.nsi b/indra/newview/installers/windows/lang_it.nsi
index 1d2e150525176efdcc6528898e8e012a019bebd1..78be4a346403ff55e5258ec36f5bab5f767d8449 100644
Binary files a/indra/newview/installers/windows/lang_it.nsi and b/indra/newview/installers/windows/lang_it.nsi differ
diff --git a/indra/newview/installers/windows/lang_ja.nsi b/indra/newview/installers/windows/lang_ja.nsi
index 1bd6526670c1ffacbecc3a3b7c789fb88ff92b15..2eb40e280bd9ecb113b23e1edbe7a4594a30ed80 100644
Binary files a/indra/newview/installers/windows/lang_ja.nsi and b/indra/newview/installers/windows/lang_ja.nsi differ
diff --git a/indra/newview/installers/windows/lang_pl.nsi b/indra/newview/installers/windows/lang_pl.nsi
index a172f0cdeb4d23c48da7c6d29426696df1cd14fc..05977847b96a83993a201e58ef3298da74501720 100644
Binary files a/indra/newview/installers/windows/lang_pl.nsi and b/indra/newview/installers/windows/lang_pl.nsi differ
diff --git a/indra/newview/installers/windows/lang_pt-br.nsi b/indra/newview/installers/windows/lang_pt-br.nsi
index 87032fec181b3e23ab87cff5b12c5d8e600c5644..79c2b7b1146bc4e1d2b414b17ec6d8f12a97c539 100644
Binary files a/indra/newview/installers/windows/lang_pt-br.nsi and b/indra/newview/installers/windows/lang_pt-br.nsi differ
diff --git a/indra/newview/installers/windows/lang_ru.nsi b/indra/newview/installers/windows/lang_ru.nsi
index 019c66123c811926eca0373fe1ac37b8d507793d..0ef1023d886dcb7b520430b4e8d528f434e6d73e 100644
Binary files a/indra/newview/installers/windows/lang_ru.nsi and b/indra/newview/installers/windows/lang_ru.nsi differ
diff --git a/indra/newview/installers/windows/lang_tr.nsi b/indra/newview/installers/windows/lang_tr.nsi
index 1c4e2c2f4831ab29450a6b33ccd6eff064626724..4bb4e14bfa5d049c6239891b544830079bcb2c47 100644
Binary files a/indra/newview/installers/windows/lang_tr.nsi and b/indra/newview/installers/windows/lang_tr.nsi differ
diff --git a/indra/newview/installers/windows/lang_zh.nsi b/indra/newview/installers/windows/lang_zh.nsi
index 355e01a333e168d4ae2cc335b4f141ddf39126ee..fccf1196255b17dd4a0d29a40749a3f568306e93 100644
Binary files a/indra/newview/installers/windows/lang_zh.nsi and b/indra/newview/installers/windows/lang_zh.nsi differ
diff --git a/indra/newview/installers/windows/language_menu.nsi b/indra/newview/installers/windows/language_menu.nsi
index 08ad42532f46988169c2d0a413e51fd9b967be03..2f426a0f47c92aac0b8e17441243802da7ab79e7 100644
Binary files a/indra/newview/installers/windows/language_menu.nsi and b/indra/newview/installers/windows/language_menu.nsi differ
diff --git a/indra/newview/llappdelegate-objc.mm b/indra/newview/llappdelegate-objc.mm
index aebae4c4344a35fff4080e43e2c67f843769397e..1d555374278ea00658edb318b90c4814bbe77fa6 100644
--- a/indra/newview/llappdelegate-objc.mm
+++ b/indra/newview/llappdelegate-objc.mm
@@ -25,7 +25,16 @@
  */
 
 #import "llappdelegate-objc.h"
+#if defined(LL_BUGSPLAT)
+#include <boost/filesystem.hpp>
+#include <vector>
+@import BugsplatMac;
+// derived from BugsplatMac's BugsplatTester/AppDelegate.m
+@interface LLAppDelegate () <BugsplatStartupManagerDelegate>
+@end
+#endif
 #include "llwindowmacosx-objc.h"
+#include "llappviewermacosx-for-objc.h"
 #include <Carbon/Carbon.h> // Used for Text Input Services ("Safe" API - it's supported)
 
 @implementation LLAppDelegate
@@ -47,6 +56,25 @@
 
 - (void) applicationDidFinishLaunching:(NSNotification *)notification
 {
+	// Call constructViewer() first so our logging subsystem is in place. This
+	// risks missing crashes in the LLAppViewerMacOSX constructor, but for
+	// present purposes it's more important to get the startup sequence
+	// properly logged.
+	// Someday I would like to modify the logging system so that calls before
+	// it's initialized are cached in a std::ostringstream and then, once it's
+	// initialized, "played back" into whatever handlers have been set up.
+	constructViewer();
+
+#if defined(LL_BUGSPLAT)
+	// Engage BugsplatStartupManager *before* calling initViewer() to handle
+	// any crashes during initialization.
+	// https://www.bugsplat.com/docs/platforms/os-x#initialization
+	[BugsplatStartupManager sharedManager].autoSubmitCrashReport = YES;
+	[BugsplatStartupManager sharedManager].askUserDetails = NO;
+	[BugsplatStartupManager sharedManager].delegate = self;
+	[[BugsplatStartupManager sharedManager] start];
+#endif
+
 	frameTimer = nil;
 
 	[self languageUpdated];
@@ -179,4 +207,138 @@
     return true;
 }
 
+#if defined(LL_BUGSPLAT)
+
+- (NSString *)applicationLogForBugsplatStartupManager:(BugsplatStartupManager *)bugsplatStartupManager
+{
+    CrashMetadata& meta(CrashMetadata_instance());
+    // As of BugsplatMac 1.0.6, userName and userEmail properties are now
+    // exposed by the BugsplatStartupManager. Set them here, since the
+    // defaultUserNameForBugsplatStartupManager and
+    // defaultUserEmailForBugsplatStartupManager methods are called later, for
+    // the *current* run, rather than for the previous crashed run whose crash
+    // report we are about to send.
+    infos("applicationLogForBugsplatStartupManager setting userName = '" +
+          meta.agentFullname + '"');
+    bugsplatStartupManager.userName =
+        [NSString stringWithCString:meta.agentFullname.c_str()
+                           encoding:NSUTF8StringEncoding];
+    // Use the email field for OS version, just as we do on Windows, until
+    // BugSplat provides more metadata fields.
+    infos("applicationLogForBugsplatStartupManager setting userEmail = '" +
+          meta.OSInfo + '"');
+    bugsplatStartupManager.userEmail =
+        [NSString stringWithCString:meta.OSInfo.c_str()
+                           encoding:NSUTF8StringEncoding];
+    // This strangely-named override method's return value contributes the
+    // User Description metadata field.
+    infos("applicationLogForBugsplatStartupManager -> '" + meta.fatalMessage + "'");
+    return [NSString stringWithCString:meta.fatalMessage.c_str()
+                              encoding:NSUTF8StringEncoding];
+}
+
+- (NSString *)applicationKeyForBugsplatStartupManager:(BugsplatStartupManager *)bugsplatStartupManager signal:(NSString *)signal exceptionName:(NSString *)exceptionName exceptionReason:(NSString *)exceptionReason {
+    // TODO: exceptionName, exceptionReason
+
+    // Windows sends location within region as well, but that's because
+    // BugSplat for Windows intercepts crashes during the same run, and that
+    // information can be queried once. On the Mac, any metadata we have is
+    // written (and rewritten) to the static_debug_info.log file that we read
+    // at the start of the next viewer run. It seems ridiculously expensive to
+    // rewrite that file on every frame in which the avatar moves.
+    std::string regionName(CrashMetadata_instance().regionName);
+    infos("applicationKeyForBugsplatStartupManager -> '" + regionName + "'");
+    return [NSString stringWithCString:regionName.c_str()
+                              encoding:NSUTF8StringEncoding];
+}
+
+- (NSString *)defaultUserNameForBugsplatStartupManager:(BugsplatStartupManager *)bugsplatStartupManager {
+    std::string agentFullname(CrashMetadata_instance().agentFullname);
+    infos("defaultUserNameForBugsplatStartupManager -> '" + agentFullname + "'");
+    return [NSString stringWithCString:agentFullname.c_str()
+                              encoding:NSUTF8StringEncoding];
+}
+
+- (NSString *)defaultUserEmailForBugsplatStartupManager:(BugsplatStartupManager *)bugsplatStartupManager {
+    // Use the email field for OS version, just as we do on Windows, until
+    // BugSplat provides more metadata fields.
+    std::string OSInfo(CrashMetadata_instance().OSInfo);
+    infos("defaultUserEmailForBugsplatStartupManager -> '" + OSInfo + "'");
+    return [NSString stringWithCString:OSInfo.c_str()
+                              encoding:NSUTF8StringEncoding];
+}
+
+- (void)bugsplatStartupManagerWillSendCrashReport:(BugsplatStartupManager *)bugsplatStartupManager
+{
+    infos("bugsplatStartupManagerWillSendCrashReport");
+}
+
+struct AttachmentInfo
+{
+    AttachmentInfo(const std::string& path, const std::string& type):
+        pathname(path),
+        basename(boost::filesystem::path(path).filename().string()),
+        mimetype(type)
+    {}
+
+    std::string pathname, basename, mimetype;
+};
+
+- (NSArray<BugsplatAttachment *> *)attachmentsForBugsplatStartupManager:(BugsplatStartupManager *)bugsplatStartupManager
+{
+    const CrashMetadata& metadata(CrashMetadata_instance());
+
+    // Since we must do very similar processing for each of several file
+    // pathnames, start by collecting them into a vector so we can iterate
+    // instead of spelling out the logic for each.
+    std::vector<AttachmentInfo> info{
+        AttachmentInfo(metadata.logFilePathname,      "text/plain"),
+        AttachmentInfo(metadata.userSettingsPathname, "text/xml"),
+        AttachmentInfo(metadata.staticDebugPathname,  "text/xml")
+    };
+
+    // We "happen to know" that info[0].basename is "SecondLife.old" -- due to
+    // the fact that BugsplatMac only notices a crash during the viewer run
+    // following the crash. Replace .old with .log to reduce confusion.
+    info[0].basename = 
+        boost::filesystem::path(info[0].pathname).stem().string() + ".log";
+
+    NSMutableArray *attachments = [[NSMutableArray alloc] init];
+
+    // Iterate over each AttachmentInfo in info vector
+    for (const AttachmentInfo& attach : info)
+    {
+        NSString *nspathname = [NSString stringWithCString:attach.pathname.c_str()
+                                                  encoding:NSUTF8StringEncoding];
+        NSString *nsbasename = [NSString stringWithCString:attach.basename.c_str()
+                                                  encoding:NSUTF8StringEncoding];
+        NSString *nsmimetype = [NSString stringWithCString:attach.mimetype.c_str()
+                                                  encoding:NSUTF8StringEncoding];
+        NSData *nsdata = [NSData dataWithContentsOfFile:nspathname];
+
+        BugsplatAttachment *attachment =
+            [[BugsplatAttachment alloc] initWithFilename:nsbasename
+                                          attachmentData:nsdata
+                                             contentType:nsmimetype];
+
+        [attachments addObject:attachment];
+        infos("attachmentsForBugsplatStartupManager attaching " + attach.pathname);
+    }
+
+    return attachments;
+}
+
+- (void)bugsplatStartupManagerDidFinishSendingCrashReport:(BugsplatStartupManager *)bugsplatStartupManager
+{
+    infos("Sent crash report to BugSplat");
+}
+
+- (void)bugsplatStartupManager:(BugsplatStartupManager *)bugsplatStartupManager didFailWithError:(NSError *)error
+{
+    // TODO: message string from NSError
+    infos("Could not send crash report to BugSplat");
+}
+
+#endif // LL_BUGSPLAT
+
 @end
diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp
index 4374caacdff0757332ab1612dce21642b32246ef..d3577d50a1ab3327219452b4c56587582a26e091 100644
--- a/indra/newview/llappviewer.cpp
+++ b/indra/newview/llappviewer.cpp
@@ -707,6 +707,22 @@ LLAppViewer::LLAppViewer()
 	//
 
 	LLLoginInstance::instance().setPlatformInfo(gPlatform, LLOSInfo::instance().getOSVersionString(), LLOSInfo::instance().getOSStringSimple());
+
+	// Under some circumstances we want to read the static_debug_info.log file
+	// from the previous viewer run between this constructor call and the
+	// init() call, which will overwrite the static_debug_info.log file for
+	// THIS run. So setDebugFileNames() early.
+#if LL_BUGSPLAT
+	// MAINT-8917: don't create a dump directory just for the
+	// static_debug_info.log file
+	std::string logdir = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, "");
+#else // ! LL_BUGSPLAT
+	// write Google Breakpad minidump files to a per-run dump directory to avoid multiple viewer issues.
+	std::string logdir = gDirUtilp->getExpandedFilename(LL_PATH_DUMP, "");
+#endif // ! LL_BUGSPLAT
+	mDumpPath = logdir;
+	setMiniDumpDir(logdir);
+	setDebugFileNames(logdir);
 }
 
 LLAppViewer::~LLAppViewer()
@@ -781,13 +797,6 @@ bool LLAppViewer::init()
 	initMaxHeapSize() ;
 	LLCoros::instance().setStackSize(gSavedSettings.getS32("CoroutineStackSize"));
 
-	// write Google Breakpad minidump files to a per-run dump directory to avoid multiple viewer issues.
-	std::string logdir = gDirUtilp->getExpandedFilename(LL_PATH_DUMP, "");
-	mDumpPath = logdir;
-	setMiniDumpDir(logdir);
-	logdir += gDirUtilp->getDirDelimiter();
-    setDebugFileNames(logdir);
-
 
 	// Although initLoggingAndGetLastDuration() is the right place to mess with
 	// setFatalFunction(), we can't query gSavedSettings until after
@@ -876,11 +885,6 @@ bool LLAppViewer::init()
 	mNumSessions++;
 	gSavedSettings.setS32("NumSessions", mNumSessions);
 
-	if (gSavedSettings.getBOOL("VerboseLogs"))
-	{
-		LLError::setPrintLocation(true);
-	}
-
 	// LLKeyboard relies on LLUI to know what some accelerator keys are called.
 	LLKeyboard::setStringTranslatorFunc( LLTrans::getKeyboardString );
 
@@ -1083,26 +1087,6 @@ bool LLAppViewer::init()
 		}
 	}
 
-// don't nag developers who need to run the executable directly
-#if LL_RELEASE_FOR_DOWNLOAD
-	// MAINT-8305: If we're processing a SLURL, skip the launcher check.
-	if (gSavedSettings.getString("CmdLineLoginLocation").empty() && !beingDebugged())
-	{
-		const char* PARENT = getenv("PARENT");
-		if (! (PARENT && std::string(PARENT) == "SL_Launcher"))
-		{
-			// Don't directly run this executable. Please run the launcher, which
-			// will run the viewer itself.
-			// Naturally we do not consider this bulletproof. The point is to
-			// gently remind a user who *inadvertently* finds him/herself in this
-			// situation to do things the Right Way. Anyone who intentionally
-			// bypasses this mechanism needs no reminder that s/he's shooting
-			// him/herself in the foot.
-			LLNotificationsUtil::add("RunLauncher");
-		}
-	}
-#endif
-
 #if LL_WINDOWS
 	if (gGLManager.mGLVersion < LLFeatureManager::getInstance()->getExpectedGLVersion())
 	{
@@ -1150,6 +1134,34 @@ bool LLAppViewer::init()
 
 	gGLActive = FALSE;
 
+	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;
+#if LL_WINDOWS
+	updater.executable = gDirUtilp->getExpandedFilename(LL_PATH_EXECUTABLE, "SLVersionChecker.exe");
+#elif LL_DARWIN
+	// explicitly run the system Python interpreter on SLVersionChecker.py
+	updater.executable = "python";
+	updater.args.add(gDirUtilp->add(gDirUtilp->getAppRODataDir(), "updater", "SLVersionChecker.py"));
+#else
+	updater.executable = gDirUtilp->getExpandedFilename(LL_PATH_EXECUTABLE, "SLVersionChecker");
+#endif
+	// add LEAP mode command-line argument to whichever of these we selected
+	updater.args.add("leap");
+	// UpdaterServiceSettings
+	updater.args.add(stringize(gSavedSettings.getU32("UpdaterServiceSetting")));
+	// channel
+	updater.args.add(LLVersionInfo::getChannel());
+	// testok
+	updater.args.add(stringize(gSavedSettings.getBOOL("UpdaterWillingToTest")));
+	// ForceAddressSize
+	updater.args.add(stringize(gSavedSettings.getU32("ForceAddressSize")));
+
+	// Run the updater. An exception from launching the updater should bother us.
+	LLLeap::create(updater, true);
+
 	// 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"));
@@ -1693,7 +1705,7 @@ bool LLAppViewer::cleanup()
 
 	release_start_screen(); // just in case
 
-	LLError::logToFixedBuffer(NULL);
+	LLError::logToFixedBuffer(NULL); // stop the fixed buffer recorder
 
 	LL_INFOS() << "Cleaning Up" << LL_ENDL;
 
@@ -2172,6 +2184,12 @@ void errorCallback(const std::string &error_string)
 	//Set the ErrorActivated global so we know to create a marker file
 	gLLErrorActivated = true;
 
+	gDebugInfo["FatalMessage"] = error_string;
+	// We're not already crashing -- we simply *intend* to crash. Since we
+	// haven't actually trashed anything yet, we can afford to write the whole
+	// static info file.
+	LLAppViewer::instance()->writeDebugInfo();
+
 	LLError::crashAndLoop(error_string);
 }
 
@@ -3039,14 +3057,11 @@ void LLAppViewer::writeDebugInfo(bool isStatic)
         ? getStaticDebugFile()
         : getDynamicDebugFile() );
 
-	LL_INFOS() << "Opening debug file " << *debug_filename << LL_ENDL;
-	llofstream out_file(debug_filename->c_str());
+    LL_INFOS() << "Writing debug file " << *debug_filename << LL_ENDL;
+    llofstream out_file(debug_filename->c_str());
 
     isStatic ?  LLSDSerialize::toPrettyXML(gDebugInfo, out_file)
              :  LLSDSerialize::toPrettyXML(gDebugInfo["Dynamic"], out_file);
-
-
-	out_file.close();
 }
 
 LLSD LLAppViewer::getViewerInfo() const
diff --git a/indra/newview/llappviewermacosx-for-objc.h b/indra/newview/llappviewermacosx-for-objc.h
new file mode 100644
index 0000000000000000000000000000000000000000..37e8a3917a928f483585f0402ab61dc25e1d4fbf
--- /dev/null
+++ b/indra/newview/llappviewermacosx-for-objc.h
@@ -0,0 +1,53 @@
+/**
+ * @file   llappviewermacosx-for-objc.h
+ * @author Nat Goodspeed
+ * @date   2018-06-15
+ * @brief  llappviewermacosx.h publishes the C++ API for
+ *         llappviewermacosx.cpp, just as
+ *         llappviewermacosx-objc.h publishes the Objective-C++ API for
+ *         llappviewermacosx-objc.mm.
+ *
+ *         This header is intended to publish for Objective-C++ consumers a
+ *         subset of the C++ API presented by llappviewermacosx.cpp. It's a
+ *         subset because, if an Objective-C++ consumer were to #include
+ *         the full llappviewermacosx.h, we would almost surely run into
+ *         trouble due to the discrepancy between Objective-C++'s BOOL versus
+ *         classic Microsoft/Linden BOOL.
+ * 
+ * $LicenseInfo:firstyear=2018&license=viewerlgpl$
+ * Copyright (c) 2018, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_LLAPPVIEWERMACOSX_FOR_OBJC_H)
+#define LL_LLAPPVIEWERMACOSX_FOR_OBJC_H
+
+#include <string>
+
+void constructViewer();
+bool initViewer();
+void handleUrl(const char* url_utf8);
+bool pumpMainLoop();
+void handleQuit();
+void cleanupViewer();
+void infos(const std::string& message);
+
+// This struct is malleable; it only serves as a way to convey a number of
+// fields from llappviewermacosx.cpp's CrashMetadata_instance() function to the
+// consuming functions in llappdelegate-objc.mm. As long as both those sources
+// are compiled with this same header, the content and order of CrashMetadata
+// can change as needed.
+struct CrashMetadata
+{
+    std::string logFilePathname;
+    std::string userSettingsPathname;
+    std::string staticDebugPathname;
+    std::string OSInfo;
+    std::string agentFullname;
+    std::string regionName;
+    std::string fatalMessage;
+};
+
+CrashMetadata& CrashMetadata_instance();
+
+#endif /* ! defined(LL_LLAPPVIEWERMACOSX_FOR_OBJC_H) */
diff --git a/indra/newview/llappviewermacosx.cpp b/indra/newview/llappviewermacosx.cpp
index d472f8926b11855a4fbc8361388b1826d201e723..81f04744f8781e92ca36cd2099ea357aab119ced 100644
--- a/indra/newview/llappviewermacosx.cpp
+++ b/indra/newview/llappviewermacosx.cpp
@@ -36,20 +36,25 @@
 #include "llappviewermacosx-objc.h"
 
 #include "llappviewermacosx.h"
+#include "llappviewermacosx-for-objc.h"
 #include "llwindowmacosx-objc.h"
 #include "llcommandlineparser.h"
+#include "llsdserialize.h"
 
 #include "llviewernetwork.h"
 #include "llviewercontrol.h"
 #include "llmd5.h"
 #include "llfloaterworldmap.h"
 #include "llurldispatcher.h"
+#include "llerrorcontrol.h"
+#include "llvoavatarself.h"         // for gAgentAvatarp->getFullname()
 #include <ApplicationServices/ApplicationServices.h>
 #ifdef LL_CARBON_CRASH_HANDLER
 #include <Carbon/Carbon.h>
 #endif
 #include <vector>
 #include <exception>
+#include <fstream>
 
 #include "lldir.h"
 #include <signal.h>
@@ -81,7 +86,7 @@ static void exceptionTerminateHandler()
 	gOldTerminateHandler(); // call old terminate() handler
 }
 
-bool initViewer()
+void constructViewer()
 {
 	// Set the working dir to <bundle>/Contents/Resources
 	if (chdir(gDirUtilp->getAppRODataDir().c_str()) == -1)
@@ -97,18 +102,20 @@ bool initViewer()
 	gOldTerminateHandler = std::set_terminate(exceptionTerminateHandler);
 
 	gViewerAppPtr->setErrorHandler(LLAppViewer::handleViewerCrash);
+}
 
-	
+bool initViewer()
+{
 	bool ok = gViewerAppPtr->init();
 	if(!ok)
 	{
 		LL_WARNS() << "Application init failed." << LL_ENDL;
 	}
-    else if (!gHandleSLURL.empty())
-    {
-        dispatchUrl(gHandleSLURL);
-        gHandleSLURL = "";
-    }
+	else if (!gHandleSLURL.empty())
+	{
+		dispatchUrl(gHandleSLURL);
+		gHandleSLURL = "";
+	}
 	return ok;
 }
 
@@ -147,6 +154,73 @@ void cleanupViewer()
 	gViewerAppPtr = NULL;
 }
 
+// The BugsplatMac API is structured as a number of different method
+// overrides, each returning a different piece of metadata. But since we
+// obtain such metadata by opening and parsing a file, it seems ridiculous to
+// reopen and reparse it for every individual string desired. What we want is
+// to open and parse the file once, retaining the data for subsequent
+// requests. That's why this is an LLSingleton.
+// Another approach would be to provide a function that simply returns
+// CrashMetadata, storing the struct in LLAppDelegate, but nat doesn't know
+// enough Objective-C++ to code that. We'd still have to detect which of the
+// method overrides is called first so that the results are order-insensitive.
+class CrashMetadataSingleton: public CrashMetadata, public LLSingleton<CrashMetadataSingleton>
+{
+    LLSINGLETON(CrashMetadataSingleton);
+
+    // convenience method to log each metadata field retrieved by constructor
+    std::string get_metadata(const LLSD& info, const LLSD::String& key) const
+    {
+        std::string data(info[key].asString());
+        LL_INFOS() << "  " << key << "='" << data << "'" << LL_ENDL;
+        return data;
+    }
+};
+
+// Populate the fields of our public base-class struct.
+CrashMetadataSingleton::CrashMetadataSingleton()
+{
+    // Note: we depend on being able to read the static_debug_info.log file
+    // from the *previous* run before we overwrite it with the new one for
+    // *this* run. LLAppViewer initialization must happen in the Right Order.
+    staticDebugPathname = *gViewerAppPtr->getStaticDebugFile();
+    std::ifstream static_file(staticDebugPathname);
+    LLSD info;
+    if (! static_file.is_open())
+    {
+        LL_INFOS() << "Can't open '" << staticDebugPathname
+                   << "'; no metadata about previous run" << LL_ENDL;
+    }
+    else if (! LLSDSerialize::deserialize(info, static_file, LLSDSerialize::SIZE_UNLIMITED))
+    {
+        LL_INFOS() << "Can't parse '" << staticDebugPathname
+                   << "'; no metadata about previous run" << LL_ENDL;
+    }
+    else
+    {
+        LL_INFOS() << "Metadata from '" << staticDebugPathname << "':" << LL_ENDL;
+        logFilePathname      = get_metadata(info, "SLLog");
+        userSettingsPathname = get_metadata(info, "SettingsFilename");
+        OSInfo               = get_metadata(info, "OSInfo");
+        agentFullname        = get_metadata(info, "LoginName");
+        // Translate underscores back to spaces
+        LLStringUtil::replaceChar(agentFullname, '_', ' ');
+        regionName           = get_metadata(info, "CurrentRegion");
+        fatalMessage         = get_metadata(info, "FatalMessage");
+    }
+}
+
+// Avoid having to compile all of our LLSingleton machinery in Objective-C++.
+CrashMetadata& CrashMetadata_instance()
+{
+    return CrashMetadataSingleton::instance();
+}
+
+void infos(const std::string& message)
+{
+    LL_INFOS() << message << LL_ENDL;
+}
+
 int main( int argc, char **argv ) 
 {
 	// Store off the command line args for use later.
diff --git a/indra/newview/llappviewerwin32.cpp b/indra/newview/llappviewerwin32.cpp
index 3942613ee0a92f5e8a8f62a247d475f2313b0740..fff2653c985024d661b6fbd2fcfb6190eb03135f 100644
--- a/indra/newview/llappviewerwin32.cpp
+++ b/indra/newview/llappviewerwin32.cpp
@@ -66,8 +66,101 @@
 #endif
 
 #include "stringize.h"
+#include "lldir.h"
+#include "llerrorcontrol.h"
 
+#include <fstream>
 #include <exception>
+
+// Bugsplat (http://bugsplat.com) crash reporting tool
+#ifdef LL_BUGSPLAT
+#include "BugSplat.h"
+#include "reader.h"                 // JsonCpp
+#include "llagent.h"                // for agent location
+#include "llviewerregion.h"
+#include "llvoavatarself.h"         // for agent name
+
+namespace
+{
+    // MiniDmpSender's constructor is defined to accept __wchar_t* instead of
+    // plain wchar_t*. That said, wunder() returns std::basic_string<__wchar_t>,
+    // NOT plain __wchar_t*, despite the apparent convenience. Calling
+    // wunder(something).c_str() as an argument expression is fine: that
+    // std::basic_string instance will survive until the function returns.
+    // Calling c_str() on a std::basic_string local to wunder() would be
+    // Undefined Behavior: we'd be left with a pointer into a destroyed
+    // std::basic_string instance. But we can do that with a macro...
+    #define WCSTR(string) wunder(string).c_str()
+
+    // It would be nice if, when wchar_t is the same as __wchar_t, this whole
+    // function would optimize away. However, we use it only for the arguments
+    // to the BugSplat API -- a handful of calls.
+    inline std::basic_string<__wchar_t> wunder(const std::wstring& str)
+    {
+        return { str.begin(), str.end() };
+    }
+
+    // when what we have in hand is a std::string, convert from UTF-8 using
+    // specific wstringize() overload
+    inline std::basic_string<__wchar_t> wunder(const std::string& str)
+    {
+        return wunder(wstringize(str));
+    }
+
+    // Irritatingly, MiniDmpSender::setCallback() is defined to accept a
+    // classic-C function pointer instead of an arbitrary C++ callable. If it
+    // did accept a modern callable, we could pass a lambda that binds our
+    // MiniDmpSender pointer. As things stand, though, we must define an
+    // actual function and store the pointer statically.
+    static MiniDmpSender *sBugSplatSender = nullptr;
+
+    bool bugsplatSendLog(UINT nCode, LPVOID lpVal1, LPVOID lpVal2)
+    {
+        if (nCode == MDSCB_EXCEPTIONCODE)
+        {
+            // send the main viewer log file
+            // widen to wstring, convert to __wchar_t, then pass c_str()
+            sBugSplatSender->sendAdditionalFile(
+                WCSTR(gDirUtilp->getExpandedFilename(LL_PATH_LOGS, "SecondLife.log")));
+
+            sBugSplatSender->sendAdditionalFile(
+                WCSTR(gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, "settings.xml")));
+
+            sBugSplatSender->sendAdditionalFile(
+                WCSTR(*LLAppViewer::instance()->getStaticDebugFile()));
+
+            // We don't have an email address for any user. Hijack this
+            // metadata field for the platform identifier.
+            sBugSplatSender->setDefaultUserEmail(
+                WCSTR(STRINGIZE(LLOSInfo::instance().getOSStringSimple() << " ("
+                                << ADDRESS_SIZE << "-bit)")));
+
+            if (gAgentAvatarp)
+            {
+                // user name, when we have it
+                sBugSplatSender->setDefaultUserName(WCSTR(gAgentAvatarp->getFullname()));
+            }
+
+            // LL_ERRS message, when there is one
+            sBugSplatSender->setDefaultUserDescription(WCSTR(LLError::getFatalMessage()));
+
+            if (gAgent.getRegion())
+            {
+                // region location, when we have it
+                LLVector3 loc = gAgent.getPositionAgent();
+                sBugSplatSender->resetAppIdentifier(
+                    WCSTR(STRINGIZE(gAgent.getRegion()->getName()
+                                    << '/' << loc.mV[0]
+                                    << '/' << loc.mV[1]
+                                    << '/' << loc.mV[2])));
+            }
+        } // MDSCB_EXCEPTIONCODE
+
+        return false;
+    }
+}
+#endif // LL_BUGSPLAT
+
 namespace
 {
     void (*gOldTerminateHandler)() = NULL;
@@ -495,15 +588,71 @@ bool LLAppViewerWin32::init()
 	LLWinDebug::instance();
 #endif
 
-#if LL_WINDOWS
 #if LL_SEND_CRASH_REPORTS
-
+#if ! defined(LL_BUGSPLAT)
+#pragma message("Building without BugSplat")
 
 	LLAppViewer* pApp = LLAppViewer::instance();
 	pApp->initCrashReporting();
 
-#endif
-#endif
+#else // LL_BUGSPLAT
+#pragma message("Building with BugSplat")
+
+	std::string build_data_fname(
+		gDirUtilp->getExpandedFilename(LL_PATH_EXECUTABLE, "build_data.json"));
+	// Use llifstream instead of std::ifstream because LL_PATH_EXECUTABLE
+	// could contain non-ASCII characters, which std::ifstream doesn't handle.
+	llifstream inf(build_data_fname.c_str());
+	if (! inf.is_open())
+	{
+		LL_WARNS() << "Can't initialize BugSplat, can't read '" << build_data_fname
+				   << "'" << LL_ENDL;
+	}
+	else
+	{
+		Json::Reader reader;
+		Json::Value build_data;
+		if (! reader.parse(inf, build_data, false)) // don't collect comments
+		{
+			// gah, the typo is baked into Json::Reader API
+			LL_WARNS() << "Can't initialize BugSplat, can't parse '" << build_data_fname
+					   << "': " << reader.getFormatedErrorMessages() << LL_ENDL;
+		}
+		else
+		{
+			Json::Value BugSplat_DB = build_data["BugSplat DB"];
+			if (! BugSplat_DB)
+			{
+				LL_WARNS() << "Can't initialize BugSplat, no 'BugSplat DB' entry in '"
+						   << build_data_fname << "'" << LL_ENDL;
+			}
+			else
+			{
+				// Got BugSplat_DB, onward!
+				std::wstring version_string(WSTRINGIZE(LL_VIEWER_VERSION_MAJOR << '.' <<
+													   LL_VIEWER_VERSION_MINOR << '.' <<
+													   LL_VIEWER_VERSION_PATCH << '.' <<
+													   LL_VIEWER_VERSION_BUILD));
+
+				// have to convert normal wide strings to strings of __wchar_t
+				sBugSplatSender = new MiniDmpSender(
+					WCSTR(BugSplat_DB.asString()),
+					WCSTR(LL_TO_WSTRING(LL_VIEWER_CHANNEL)),
+					WCSTR(version_string),
+					nullptr,              // szAppIdentifier -- set later
+					MDSF_NONINTERACTIVE | // automatically submit report without prompting
+					MDSF_PREVENTHIJACKING); // disallow swiping Exception filter
+				sBugSplatSender->setCallback(bugsplatSendLog);
+
+				// engage stringize() overload that converts from wstring
+				LL_INFOS() << "Engaged BugSplat(" << LL_TO_STRING(LL_VIEWER_CHANNEL)
+						   << ' ' << stringize(version_string) << ')' << LL_ENDL;
+			} // got BugSplat_DB
+		} // parsed build_data.json
+	} // opened build_data.json
+
+#endif // LL_BUGSPLAT
+#endif // LL_SEND_CRASH_REPORTS
 
 	bool success = LLAppViewer::init();
 
diff --git a/indra/newview/llexternaleditor.cpp b/indra/newview/llexternaleditor.cpp
index df9c848cb812f9944f75a18a35210f37a5a77638..776bbf78c2a8dd2ce28d27551c742df42e559207 100644
--- a/indra/newview/llexternaleditor.cpp
+++ b/indra/newview/llexternaleditor.cpp
@@ -31,6 +31,7 @@
 #include "llui.h"
 #include "llprocess.h"
 #include "llsdutil.h"
+#include "llstring.h"
 #include <boost/foreach.hpp>
 
 // static
@@ -188,12 +189,12 @@ std::string LLExternalEditor::findCommand(
 		cmd = LLUI::sSettingGroups["config"]->getString(sSetting);
 		LL_INFOS() << "Using setting" << LL_ENDL;
 	}
-	else					// otherwise use the path specified by the environment variable
+	else                    // otherwise use the path specified by the environment variable
 	{
-		char* env_var_val = getenv(env_var.c_str());
+		auto env_var_val(LLStringUtil::getoptenv(env_var));
 		if (env_var_val)
 		{
-			cmd = env_var_val;
+			cmd = *env_var_val;
 			LL_INFOS() << "Using env var " << env_var << LL_ENDL;
 		}
 	}
diff --git a/indra/newview/lllogininstance.cpp b/indra/newview/lllogininstance.cpp
index bc93fa2c20f0e5592727b1b72eb25570892b6107..126c6be388b5777f88f542490d2474ef2c4f367a 100644
--- a/indra/newview/lllogininstance.cpp
+++ b/indra/newview/lllogininstance.cpp
@@ -253,14 +253,12 @@ bool LLLoginInstance::handleLoginEvent(const LLSD& event)
 
 	mLoginState = event["state"].asString();
 	mResponseData = event["data"];
-	
+
 	if(event.has("transfer_rate"))
 	{
 		mTransferRate = event["transfer_rate"].asReal();
 	}
 
-	
-
 	// Call the method registered in constructor, if any, for more specific
 	// handling
 	mDispatcher.try_call(event);
@@ -276,6 +274,14 @@ void LLLoginInstance::handleLoginFailure(const LLSD& event)
     // Login has failed. 
     // Figure out why and respond...
     LLSD response = event["data"];
+    LLSD updater  = response["updater"];
+
+    // Always provide a response to the updater, if in fact the updater
+    // contacted us, if in fact the ping contains a 'reply' key. Most code
+    // paths tell it not to proceed with updating.
+    ResponsePtr resp(std::make_shared<LLEventAPI::Response>
+                         (LLSDMap("update", false), updater));
+
     std::string reason_response = response["reason"].asString();
     std::string message_response = response["message"].asString();
     LL_DEBUGS("LLLogin") << "reason " << reason_response
@@ -328,17 +334,44 @@ void LLLoginInstance::handleLoginFailure(const LLSD& event)
     }
     else if(reason_response == "update")
     {
-        // This shouldn't happen - the viewer manager should have forced an update; 
-        // possibly the user ran the viewer directly and bypassed the update check
+        // This can happen if the user clicked Login quickly, before we heard
+        // back from the Viewer Version Manager, but login failed because
+        // login.cgi is insisting on a required update. We were called with an
+        // event that bundles both the login.cgi 'response' and the
+        // synchronization event from the 'updater'.
         std::string required_version = response["message_args"]["VERSION"];
         LL_WARNS("LLLogin") << "Login failed because an update to version " << required_version << " is required." << LL_ENDL;
 
         if (gViewerWindow)
             gViewerWindow->setShowProgress(FALSE);
 
-        LLSD data(LLSD::emptyMap());
-        data["VERSION"] = required_version;
-        LLNotificationsUtil::add("RequiredUpdate", data, LLSD::emptyMap(), boost::bind(&LLLoginInstance::handleLoginDisallowed, this, _1, _2));
+        LLSD args(LLSDMap("VERSION", required_version));
+        if (updater.isUndefined())
+        {
+            // If the updater failed to shake hands, better advise the user to
+            // download the update him/herself.
+            LLNotificationsUtil::add(
+                "RequiredUpdate",
+                args,
+                updater,
+                boost::bind(&LLLoginInstance::handleLoginDisallowed, this, _1, _2));
+        }
+        else
+        {
+            // If we've heard from the updater that an update is required,
+            // then display the prompt that assures the user we'll take care
+            // of it. This is the one case in which we bind 'resp':
+            // instead of destroying our Response object (and thus sending a
+            // negative reply to the updater) as soon as we exit this
+            // function, bind our shared_ptr so it gets passed into
+            // syncWithUpdater. That ensures that the response is delayed
+            // until the user has responded to the notification.
+            LLNotificationsUtil::add(
+                "PauseForUpdate",
+                args,
+                updater,
+                boost::bind(&LLLoginInstance::syncWithUpdater, this, resp, _1, _2));
+        }
     }
     else if(   reason_response == "key"
             || reason_response == "presence"
@@ -361,6 +394,19 @@ void LLLoginInstance::handleLoginFailure(const LLSD& event)
     }   
 }
 
+void LLLoginInstance::syncWithUpdater(ResponsePtr resp, const LLSD& notification, const LLSD& response)
+{
+    LL_INFOS("LLLogin") << "LLLoginInstance::syncWithUpdater" << LL_ENDL;
+    // 'resp' points to an instance of LLEventAPI::Response that will be
+    // destroyed as soon as we return and the notification response functor is
+    // unregistered. Modify it so that it tells the updater to go ahead and
+    // perform the update. Naturally, if we allowed the user a choice as to
+    // whether to proceed or not, this assignment would reflect the user's
+    // selection.
+    (*resp)["update"] = true;
+    attemptComplete();
+}
+
 void LLLoginInstance::handleLoginDisallowed(const LLSD& notification, const LLSD& response)
 {
     attemptComplete();
@@ -420,7 +466,6 @@ bool LLLoginInstance::handleTOSResponse(bool accepted, const std::string& key)
 	return true;
 }
 
-
 std::string construct_start_string()
 {
 	std::string start;
diff --git a/indra/newview/lllogininstance.h b/indra/newview/lllogininstance.h
index 651ad10afb1059e4f07e11d09ef0963c40fbc886..b759b43474d560c865b288bb41c4ac0c59f1b006 100644
--- a/indra/newview/lllogininstance.h
+++ b/indra/newview/lllogininstance.h
@@ -28,8 +28,10 @@
 #define LL_LLLOGININSTANCE_H
 
 #include "lleventdispatcher.h"
+#include "lleventapi.h"
 #include <boost/scoped_ptr.hpp>
 #include <boost/function.hpp>
+#include <memory>                   // std::shared_ptr
 #include "llsecapi.h"
 class LLLogin;
 class LLEventStream;
@@ -68,6 +70,7 @@ class LLLoginInstance : public LLSingleton<LLLoginInstance>
 	LLNotificationsInterface& getNotificationsInterface() const { return *mNotifications; }
 
 private:
+	typedef std::shared_ptr<LLEventAPI::Response> ResponsePtr;
 	void constructAuthParams(LLPointer<LLCredential> user_credentials);
 	void updateApp(bool mandatory, const std::string& message);
 	bool updateDialogCallback(const LLSD& notification, const LLSD& response);
@@ -77,7 +80,8 @@ class LLLoginInstance : public LLSingleton<LLLoginInstance>
 	void handleLoginSuccess(const LLSD& event);
 	void handleDisconnect(const LLSD& event);
 	void handleIndeterminate(const LLSD& event);
-    void handleLoginDisallowed(const LLSD& notification, const LLSD& response);
+	void handleLoginDisallowed(const LLSD& notification, const LLSD& response);
+	void syncWithUpdater(ResponsePtr resp, const LLSD& notification, const LLSD& response);
 
 	bool handleTOSResponse(bool v, const std::string& key);
 
diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp
index 37aaece5d6bb9b8678802111bd39e647d3806d6a..243ba004577eab242e95cc63dcd09b1cceb3c7d5 100644
--- a/indra/newview/llstartup.cpp
+++ b/indra/newview/llstartup.cpp
@@ -480,8 +480,7 @@ bool idle_startup()
 			if (!found_template)
 			{
 				message_template_path =
-					gDirUtilp->getExpandedFilename(LL_PATH_EXECUTABLE,
-												   "../Resources/app_settings",
+					gDirUtilp->getExpandedFilename(LL_PATH_APP_SETTINGS,
 												   "message_template.msg");
 				found_template = LLFile::fopen(message_template_path.c_str(), "r");		/* Flawfinder: ignore */
 			}		
diff --git a/indra/newview/llversioninfo.cpp b/indra/newview/llversioninfo.cpp
index 375dce485d71b2eec8c332f686a689ecb4bb4b9d..4e07223784dcfc9bde0a297e9ea4978a944ffcf9 100644
--- a/indra/newview/llversioninfo.cpp
+++ b/indra/newview/llversioninfo.cpp
@@ -101,14 +101,11 @@ namespace
 {
 	// LL_VIEWER_CHANNEL is a macro defined on the compiler command line. The
 	// macro expands to the string name of the channel, but without quotes. We
-	// need to turn it into a quoted string. This macro trick does that.
-#define stringize_inner(x) #x
-#define stringize_outer(x) stringize_inner(x)
-
+	// need to turn it into a quoted string. LL_TO_STRING() does that.
 	/// Storage of the channel name the viewer is using.
 	//  The channel name is set by hardcoded constant, 
 	//  or by calling LLVersionInfo::resetChannel()
-	std::string sWorkingChannelName(stringize_outer(LL_VIEWER_CHANNEL));
+	std::string sWorkingChannelName(LL_TO_STRING(LL_VIEWER_CHANNEL));
 
 	// Storage for the "version and channel" string.
 	// This will get reset too.
diff --git a/indra/newview/llviewerregion.cpp b/indra/newview/llviewerregion.cpp
index ba733bb06806c1475a111bb75e96291b3d74e0a7..0d7e295b9ad41289484484f8e0d45af438790c19 100644
--- a/indra/newview/llviewerregion.cpp
+++ b/indra/newview/llviewerregion.cpp
@@ -44,6 +44,7 @@
 
 #include "llagent.h"
 #include "llagentcamera.h"
+#include "llappviewer.h"
 #include "llavatarrenderinfoaccountant.h"
 #include "llcallingcard.h"
 #include "llcommandhandler.h"
@@ -105,6 +106,18 @@ typedef std::map<std::string, std::string> CapabilityMap;
 
 static void log_capabilities(const CapabilityMap &capmap);
 
+namespace
+{
+
+void newRegionEntry(LLViewerRegion& region)
+{
+    LL_INFOS("LLViewerRegion") << "Entering region [" << region.getName() << "]" << LL_ENDL;
+    gDebugInfo["CurrentRegion"] = region.getName();
+    LLAppViewer::instance()->writeDebugInfo();
+}
+
+} // anonymous namespace
+
 // support for secondlife:///app/region/{REGION} SLapps
 // N.B. this is defined to work exactly like the classic secondlife://{REGION}
 // However, the later syntax cannot support spaces in the region name because
@@ -252,6 +265,9 @@ void LLViewerRegionImpl::requestBaseCapabilitiesCoro(U64 regionHandle)
             return; // this error condition is not recoverable.
         }
 
+        // record that we just entered a new region
+        newRegionEntry(*regionp);
+
         // After a few attempts, continue login.  But keep trying to get the caps:
         if (mSeedCapAttempts >= mSeedCapMaxAttemptsBeforeLogin &&
             STATE_SEED_GRANTED_WAIT == LLStartUp::getStartupState())
@@ -375,6 +391,9 @@ void LLViewerRegionImpl::requestBaseCapabilitiesCompleteCoro(U64 regionHandle)
             break; // this error condition is not recoverable.
         }
 
+        // record that we just entered a new region
+        newRegionEntry(*regionp);
+
         LLSD capabilityNames = LLSD::emptyArray();
         buildCapabilityNames(capabilityNames);
 
diff --git a/indra/newview/llviewerwindow.cpp b/indra/newview/llviewerwindow.cpp
index e53ccd7b8db317cbbd381a7206ab0c0e2592523a..43e803fe7a8eebc154a729c0124f94ef2e88c653 100644
--- a/indra/newview/llviewerwindow.cpp
+++ b/indra/newview/llviewerwindow.cpp
@@ -306,6 +306,9 @@ class RecordToChatConsole : public LLSingleton<RecordToChatConsole>
 RecordToChatConsole::RecordToChatConsole():
 	mRecorder(new RecordToChatConsoleRecorder())
 {
+    mRecorder->showTags(false);
+    mRecorder->showLocation(false);
+    mRecorder->showMultiline(true);
 }
 
 ////////////////////////////////////////////////////////////////////////////
diff --git a/indra/newview/llvoicevivox.cpp b/indra/newview/llvoicevivox.cpp
index ee333bcee2815d2d3d8f871eadbb1c963eb944af..cf40058c34a7debc6062e0c226ddd1a532b79c54 100644
--- a/indra/newview/llvoicevivox.cpp
+++ b/indra/newview/llvoicevivox.cpp
@@ -770,14 +770,13 @@ bool LLVivoxVoiceClient::startAndLaunchDaemon()
     {
 #ifndef VIVOXDAEMON_REMOTEHOST
         // Launch the voice daemon
-        std::string exe_path = gDirUtilp->getExecutableDir();
-        exe_path += gDirUtilp->getDirDelimiter();
+        std::string exe_path = gDirUtilp->getAppRODataDir();
 #if LL_WINDOWS
-        exe_path += "SLVoice.exe";
+        gDirUtilp->append(exe_path, "SLVoice.exe");
 #elif LL_DARWIN
-        exe_path += "../Resources/SLVoice";
+        gDirUtilp->append(exe_path, "SLVoice");
 #else
-        exe_path += "SLVoice";
+        gDirUtilp->append(exe_path, "SLVoice");
 #endif
         // See if the vivox executable exists
         llstat s;
diff --git a/indra/newview/llwebprofile.cpp b/indra/newview/llwebprofile.cpp
index 81d4e30a7ae747c6e7514085c59d02eebd2612d9..8dcef2c7cdae9e393bb5beabd83cac1b1b798b2a 100644
--- a/indra/newview/llwebprofile.cpp
+++ b/indra/newview/llwebprofile.cpp
@@ -33,6 +33,7 @@
 #include "llimagepng.h"
 
 #include "llsdserialize.h"
+#include "llstring.h"
 
 // newview
 #include "llpanelprofile.h" // for getProfileURL(). FIXME: move the method to LLAvatarActions
@@ -264,6 +265,5 @@ void LLWebProfile::reportImageUploadStatus(bool ok)
 std::string LLWebProfile::getAuthCookie()
 {
 	// This is needed to test image uploads on Linux viewer built with OpenSSL 1.0.0 (0.9.8 works fine).
-	const char* debug_cookie = getenv("LL_SNAPSHOT_COOKIE");
-	return debug_cookie ? debug_cookie : sAuthCookie;
+	return LLStringUtil::getenv("LL_SNAPSHOT_COOKIE", sAuthCookie);
 }
diff --git a/indra/newview/skins/default/xui/en/notifications.xml b/indra/newview/skins/default/xui/en/notifications.xml
index cc57e1375ad4ea8aced27c1d14aec879ef08b98e..8780149d852f779650d948144b37b9c2adeafb75 100644
--- a/indra/newview/skins/default/xui/en/notifications.xml
+++ b/indra/newview/skins/default/xui/en/notifications.xml
@@ -3851,7 +3851,6 @@ Finished download of raw terrain file to:
    name="RequiredUpdate"
    type="alertmodal">
 Version [VERSION] is required for login.
-This should have been updated for you but apparently was not.
 Please download from https://secondlife.com/support/downloads/
     <tag>confirm</tag>
     <usetemplate
@@ -3859,6 +3858,44 @@ Please download from https://secondlife.com/support/downloads/
      yestext="OK"/>
   </notification>
 
+  <notification
+   icon="alertmodal.tga"
+   name="PauseForUpdate"
+   type="alertmodal">
+Version [VERSION] is required for login.
+Click OK to download and install.
+    <tag>confirm</tag>
+    <usetemplate
+     name="okbutton"
+     yestext="OK"/>
+  </notification>
+
+  <notification
+   icon="alertmodal.tga"
+   name="OptionalUpdateReady"
+   type="alertmodal">
+Version [VERSION] has been downloaded and is ready to install.
+Click OK to install.
+    <tag>confirm</tag>
+    <usetemplate
+     name="okbutton"
+     yestext="OK"/>
+  </notification>
+
+  <notification
+   icon="alertmodal.tga"
+   name="PromptOptionalUpdate"
+   type="alertmodal">
+Version [VERSION] has been downloaded and is ready to install.
+Proceed?
+    <tag>confirm</tag>
+    <usetemplate
+     canceltext="Not Now"
+     name="yesnocancelbuttons"
+     notext="Skip"
+     yestext="Install"/>
+  </notification>
+
   <notification
    icon="alertmodal.tga"
    name="LoginFailedUnknown"
diff --git a/indra/newview/tests/llversioninfo_test.cpp b/indra/newview/tests/llversioninfo_test.cpp
index 2f7a4e96012b8017e53f95c749c0c677938399fc..58f0469552384bfaef146c6beaab05efff9ff7f9 100644
--- a/indra/newview/tests/llversioninfo_test.cpp
+++ b/indra/newview/tests/llversioninfo_test.cpp
@@ -33,10 +33,8 @@
 
 // LL_VIEWER_CHANNEL is a macro defined on the compiler command line. The
 // macro expands to the string name of the channel, but without quotes. We
-// need to turn it into a quoted string. This macro trick does that.
-#define stringize_inner(x) #x
-#define stringize_outer(x) stringize_inner(x)
-#define ll_viewer_channel stringize_outer(LL_VIEWER_CHANNEL)
+// need to turn it into a quoted string. LL_TO_STRING() does that.
+#define ll_viewer_channel LL_TO_STRING(LL_VIEWER_CHANNEL)
 
 namespace tut
 {
diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py
index 541112a7659b6b969415c4ef79e87809182c079f..c0f642c852b0c84a8dddbc7e377001f60837ba7c 100755
--- a/indra/newview/viewer_manifest.py
+++ b/indra/newview/viewer_manifest.py
@@ -26,19 +26,20 @@
 Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
 $/LicenseInfo$
 """
-import sys
-import os
-import os.path
-import shutil
 import errno
 import json
+import os
+import os.path
 import plistlib
 import random
 import re
+import shutil
 import stat
 import subprocess
+import sys
 import tarfile
 import time
+import zipfile
 
 viewer_dir = os.path.dirname(__file__)
 # Add indra/lib/python to our path so we don't have to muck with PYTHONPATH.
@@ -63,7 +64,7 @@ def construct(self):
         self.path(src="../../etc/message.xml", dst="app_settings/message.xml")
 
         if self.is_packaging_viewer():
-            with self.prefix(src="app_settings"):
+            with self.prefix(src_dst="app_settings"):
                 self.exclude("logcontrol.xml")
                 self.exclude("logcontrol-dev.xml")
                 self.path("*.ini")
@@ -85,7 +86,7 @@ def construct(self):
             
                 # ... and the included spell checking dictionaries
                 pkgdir = os.path.join(self.args['build'], os.pardir, 'packages')
-                with self.prefix(src=pkgdir,dst=""):
+                with self.prefix(src=pkgdir):
                     self.path("dictionaries")
 
                 # include the extracted packages information (see BuildPackagesInfo.cmake)
@@ -107,17 +108,18 @@ def construct(self):
                                         Type='String',
                                         Value=''))
                 settings_install = {}
-                if 'sourceid' in self.args and self.args['sourceid']:
+                sourceid = self.args.get('sourceid')
+                if sourceid:
                     settings_install['sourceid'] = settings_template['sourceid'].copy()
-                    settings_install['sourceid']['Value'] = self.args['sourceid']
-                    print "Set sourceid in settings_install.xml to '%s'" % self.args['sourceid']
+                    settings_install['sourceid']['Value'] = sourceid
+                    print "Set sourceid in settings_install.xml to '%s'" % sourceid
 
-                if 'channel_suffix' in self.args and self.args['channel_suffix']:
+                if self.args.get('channel_suffix'):
                     settings_install['CmdLineChannel'] = settings_template['CmdLineChannel'].copy()
                     settings_install['CmdLineChannel']['Value'] = self.channel_with_pkg_suffix()
                     print "Set CmdLineChannel in settings_install.xml to '%s'" % self.channel_with_pkg_suffix()
 
-                if 'grid' in self.args and self.args['grid']:
+                if self.args.get('grid'):
                     settings_install['CmdLineGridChoice'] = settings_template['CmdLineGridChoice'].copy()
                     settings_install['CmdLineGridChoice']['Value'] = self.grid()
                     print "Set CmdLineGridChoice in settings_install.xml to '%s'" % self.grid()
@@ -129,20 +131,20 @@ def construct(self):
                                  src="environment")
 
 
-            with self.prefix(src="character"):
+            with self.prefix(src_dst="character"):
                 self.path("*.llm")
                 self.path("*.xml")
                 self.path("*.tga")
 
             # Include our fonts
-            with self.prefix(src="fonts"):
+            with self.prefix(src_dst="fonts"):
                 self.path("*.ttf")
                 self.path("*.txt")
 
             # skins
-            with self.prefix(src="skins"):
+            with self.prefix(src_dst="skins"):
                     # include the entire textures directory recursively
-                    with self.prefix(src="*/textures"):
+                    with self.prefix(src_dst="*/textures"):
                             self.path("*/*.tga")
                             self.path("*/*.j2c")
                             self.path("*/*.jpg")
@@ -170,7 +172,7 @@ def construct(self):
 
 
             # local_assets dir (for pre-cached textures)
-            with self.prefix(src="local_assets"):
+            with self.prefix(src_dst="local_assets"):
                 self.path("*.j2c")
                 self.path("*.tga")
 
@@ -186,6 +188,10 @@ def construct(self):
                             "Address Size":self.address_size,
                             "Update Service":"https://update.secondlife.com/update",
                             }
+            # Only store this if it's both present and non-empty
+            bugsplat_db = self.args.get('bugsplat')
+            if bugsplat_db:
+                build_data_dict["BugSplat DB"] = bugsplat_db
             build_data_dict = self.finish_build_data_dict(build_data_dict)
             with open(os.path.join(os.pardir,'build_data.json'), 'w') as build_data_handle:
                 json.dump(build_data_dict,build_data_handle)
@@ -206,8 +212,9 @@ def channel(self):
 
     def channel_with_pkg_suffix(self):
         fullchannel=self.channel()
-        if 'channel_suffix' in self.args and self.args['channel_suffix']:
-            fullchannel+=' '+self.args['channel_suffix']
+        channel_suffix = self.args.get('channel_suffix')
+        if channel_suffix:
+            fullchannel+=' '+channel_suffix
         return fullchannel
 
     def channel_variant(self):
@@ -215,8 +222,7 @@ def channel_variant(self):
         return self.channel().replace(CHANNEL_VENDOR_BASE, "").strip()
 
     def channel_type(self): # returns 'release', 'beta', 'project', or 'test'
-        global CHANNEL_VENDOR_BASE
-        channel_qualifier=self.channel().replace(CHANNEL_VENDOR_BASE, "").lower().strip()
+        channel_qualifier=self.channel_variant().lower()
         if channel_qualifier.startswith('release'):
             channel_type='release'
         elif channel_qualifier.startswith('beta'):
@@ -234,11 +240,12 @@ def channel_variant_app_suffix(self):
         if self.channel_type() == 'release':
             suffix=suffix.replace('Release', '').strip()
         # for the base release viewer, suffix will now be null - for any other, append what remains
-        if len(suffix) > 0:
-            suffix = "_"+ ("_".join(suffix.split()))
+        if suffix:
+            suffix = "_".join([''] + suffix.split())
         # the additional_packages mechanism adds more to the installer name (but not to the app name itself)
-        if 'channel_suffix' in self.args and self.args['channel_suffix']:
-            suffix+='_'+("_".join(self.args['channel_suffix'].split()))
+        # ''.split() produces empty list, so suffix only changes if
+        # channel_suffix is non-empty
+        suffix = "_".join([suffix] + self.args.get('channel_suffix', '').split())
         return suffix
 
     def installer_base_name(self):
@@ -424,7 +431,8 @@ def final_exe(self):
 
     def finish_build_data_dict(self, build_data_dict):
         #MAINT-7294: Windows exe names depend on channel name, so write that in also
-        build_data_dict.update({'Executable':self.final_exe()})
+        build_data_dict['Executable'] = self.final_exe()
+        build_data_dict['AppName']    = self.app_name()
         return build_data_dict
 
     def test_msvcrt_and_copy_action(self, src, dst):
@@ -470,7 +478,7 @@ def test_for_no_msvcrt_manifest_and_copy_action(self, src, dst):
                     pass
                 except NoMatchingAssemblyException as err:
                     pass
-                    
+
                 self.ccopy(src,dst)
             else:
                 raise Exception("Directories are not supported by test_CRT_and_copy_action()")
@@ -488,24 +496,17 @@ def construct(self):
             # Find secondlife-bin.exe in the 'configuration' dir, then rename it to the result of final_exe.
             self.path(src='%s/secondlife-bin.exe' % self.args['configuration'], dst=self.final_exe())
 
-            with self.prefix(src=os.path.join(pkgdir, "VMP"), dst=""):
+            with self.prefix(src=os.path.join(pkgdir, "VMP")):
                 # include the compiled launcher scripts so that it gets included in the file_list
-                self.path('SL_Launcher.exe')
-                #IUM is not normally executed directly, just imported.  No exe needed.
-                self.path("InstallerUserMessage.py")
-
-            with self.prefix(src=self.icon_path(), dst="vmp_icons"):
-                self.path("secondlife.ico")
+                self.path('SLVersionChecker.exe')
 
-            #VMP  Tkinter icons
-            with self.prefix("vmp_icons"):
-                self.path("*.png")
-                self.path("*.gif")
-
-            #before, we only needed llbase at build time.  With VMP, we need it at run time.
-            with self.prefix(src=os.path.join(pkgdir, "lib", "python", "llbase"), dst="llbase"):
-                self.path("*.py")
-                self.path("_cllsd.so")
+            with self.prefix(dst="vmp_icons"):
+                with self.prefix(src=self.icon_path()):
+                    self.path("secondlife.ico")
+                #VMP  Tkinter icons
+                with self.prefix(src="vmp_icons"):
+                    self.path("*.png")
+                    self.path("*.gif")
 
         # Plugin host application
         self.path2basename(os.path.join(os.pardir,
@@ -513,8 +514,8 @@ def construct(self):
                            "slplugin.exe")
         
         # Get shared libs from the shared libs staging directory
-        with self.prefix(src=os.path.join(os.pardir, 'sharedlibs', self.args['configuration']),
-                       dst=""):
+        with self.prefix(src=os.path.join(self.args['build'], os.pardir,
+                                          'sharedlibs', self.args['configuration'])):
 
             # Get llcommon and deps. If missing assume static linkage and continue.
             try:
@@ -580,6 +581,17 @@ def construct(self):
             # Hunspell
             self.path("libhunspell.dll")
 
+            # BugSplat
+            if self.args.get('bugsplat'):
+                if(self.address_size == 64):
+                    self.path("BsSndRpt64.exe")
+                    self.path("BugSplat64.dll")
+                    self.path("BugSplatRc64.dll")
+                else:
+                    self.path("BsSndRpt.exe")
+                    self.path("BugSplat.dll")
+                    self.path("BugSplatRc.dll")
+
             # For google-perftools tcmalloc allocator.
             try:
                 if self.args['configuration'].lower() == 'debug':
@@ -589,116 +601,118 @@ def construct(self):
             except:
                 print "Skipping libtcmalloc_minimal.dll"
 
-
         self.path(src="licenses-win32.txt", dst="licenses.txt")
         self.path("featuretable.txt")
 
-        with self.prefix(src=pkgdir,dst=""):
+        with self.prefix(src=pkgdir):
             self.path("ca-bundle.crt")
 
         # Media plugins - CEF
-        with self.prefix(src='../media_plugins/cef/%s' % self.args['configuration'], dst="llplugin"):
-            self.path("media_plugin_cef.dll")
-
-        # Media plugins - LibVLC
-        with self.prefix(src='../media_plugins/libvlc/%s' % self.args['configuration'], dst="llplugin"):
-            self.path("media_plugin_libvlc.dll")
-
-        # Media plugins - Example (useful for debugging - not shipped with release viewer)
-        if self.channel_type() != 'release':
-            with self.prefix(src='../media_plugins/example/%s' % self.args['configuration'], dst="llplugin"):
-                self.path("media_plugin_example.dll")
-
-        # CEF runtime files - debug
-        # CEF runtime files - not debug (release, relwithdebinfo etc.)
-        config = 'debug' if self.args['configuration'].lower() == 'debug' else 'release'
-        with self.prefix(src=os.path.join(pkgdir, 'bin', config), dst="llplugin"):
-            self.path("chrome_elf.dll")
-            self.path("d3dcompiler_43.dll")
-            self.path("d3dcompiler_47.dll")
-            self.path("libcef.dll")
-            self.path("libEGL.dll")
-            self.path("libGLESv2.dll")
-            self.path("dullahan_host.exe")
-            self.path("natives_blob.bin")
-            self.path("snapshot_blob.bin")
-            self.path("widevinecdmadapter.dll")
-
-        # MSVC DLLs needed for CEF and have to be in same directory as plugin
-        with self.prefix(src=os.path.join(os.pardir, 'sharedlibs', 'Release'), dst="llplugin"):
-            self.path("msvcp120.dll")
-            self.path("msvcr120.dll")
-
-        # CEF files common to all configurations
-        with self.prefix(src=os.path.join(pkgdir, 'resources'), dst="llplugin"):
-            self.path("cef.pak")
-            self.path("cef_100_percent.pak")
-            self.path("cef_200_percent.pak")
-            self.path("cef_extensions.pak")
-            self.path("devtools_resources.pak")
-            self.path("icudtl.dat")
-
-        with self.prefix(src=os.path.join(pkgdir, 'resources', 'locales'), dst=os.path.join('llplugin', 'locales')):
-            self.path("am.pak")
-            self.path("ar.pak")
-            self.path("bg.pak")
-            self.path("bn.pak")
-            self.path("ca.pak")
-            self.path("cs.pak")
-            self.path("da.pak")
-            self.path("de.pak")
-            self.path("el.pak")
-            self.path("en-GB.pak")
-            self.path("en-US.pak")
-            self.path("es-419.pak")
-            self.path("es.pak")
-            self.path("et.pak")
-            self.path("fa.pak")
-            self.path("fi.pak")
-            self.path("fil.pak")
-            self.path("fr.pak")
-            self.path("gu.pak")
-            self.path("he.pak")
-            self.path("hi.pak")
-            self.path("hr.pak")
-            self.path("hu.pak")
-            self.path("id.pak")
-            self.path("it.pak")
-            self.path("ja.pak")
-            self.path("kn.pak")
-            self.path("ko.pak")
-            self.path("lt.pak")
-            self.path("lv.pak")
-            self.path("ml.pak")
-            self.path("mr.pak")
-            self.path("ms.pak")
-            self.path("nb.pak")
-            self.path("nl.pak")
-            self.path("pl.pak")
-            self.path("pt-BR.pak")
-            self.path("pt-PT.pak")
-            self.path("ro.pak")
-            self.path("ru.pak")
-            self.path("sk.pak")
-            self.path("sl.pak")
-            self.path("sr.pak")
-            self.path("sv.pak")
-            self.path("sw.pak")
-            self.path("ta.pak")
-            self.path("te.pak")
-            self.path("th.pak")
-            self.path("tr.pak")
-            self.path("uk.pak")
-            self.path("vi.pak")
-            self.path("zh-CN.pak")
-            self.path("zh-TW.pak")
-
-        with self.prefix(src=os.path.join(pkgdir, 'bin', 'release'), dst="llplugin"):
-            self.path("libvlc.dll")
-            self.path("libvlccore.dll")
-            self.path("plugins/")
-
-        # pull in the crash logger and updater from other projects
+        with self.prefix(dst="llplugin"):
+            with self.prefix(src=os.path.join(self.args['build'], os.pardir, 'media_plugins')):
+                with self.prefix(src=os.path.join('cef', self.args['configuration'])):
+                    self.path("media_plugin_cef.dll")
+
+                # Media plugins - LibVLC
+                with self.prefix(src=os.path.join('libvlc', self.args['configuration'])):
+                    self.path("media_plugin_libvlc.dll")
+
+                # Media plugins - Example (useful for debugging - not shipped with release viewer)
+                if self.channel_type() != 'release':
+                    with self.prefix(src=os.path.join('example', self.args['configuration'])):
+                        self.path("media_plugin_example.dll")
+
+            # CEF runtime files - debug
+            # CEF runtime files - not debug (release, relwithdebinfo etc.)
+            config = 'debug' if self.args['configuration'].lower() == 'debug' else 'release'
+            with self.prefix(src=os.path.join(pkgdir, 'bin', config)):
+                self.path("chrome_elf.dll")
+                self.path("d3dcompiler_43.dll")
+                self.path("d3dcompiler_47.dll")
+                self.path("libcef.dll")
+                self.path("libEGL.dll")
+                self.path("libGLESv2.dll")
+                self.path("dullahan_host.exe")
+                self.path("natives_blob.bin")
+                self.path("snapshot_blob.bin")
+                self.path("widevinecdmadapter.dll")
+
+            # MSVC DLLs needed for CEF and have to be in same directory as plugin
+            with self.prefix(src=os.path.join(self.args['build'], os.pardir,
+                                              'sharedlibs', 'Release')):
+                self.path("msvcp120.dll")
+                self.path("msvcr120.dll")
+
+            # CEF files common to all configurations
+            with self.prefix(src=os.path.join(pkgdir, 'resources')):
+                self.path("cef.pak")
+                self.path("cef_100_percent.pak")
+                self.path("cef_200_percent.pak")
+                self.path("cef_extensions.pak")
+                self.path("devtools_resources.pak")
+                self.path("icudtl.dat")
+
+            with self.prefix(src=os.path.join(pkgdir, 'resources', 'locales'), dst='locales'):
+                self.path("am.pak")
+                self.path("ar.pak")
+                self.path("bg.pak")
+                self.path("bn.pak")
+                self.path("ca.pak")
+                self.path("cs.pak")
+                self.path("da.pak")
+                self.path("de.pak")
+                self.path("el.pak")
+                self.path("en-GB.pak")
+                self.path("en-US.pak")
+                self.path("es-419.pak")
+                self.path("es.pak")
+                self.path("et.pak")
+                self.path("fa.pak")
+                self.path("fi.pak")
+                self.path("fil.pak")
+                self.path("fr.pak")
+                self.path("gu.pak")
+                self.path("he.pak")
+                self.path("hi.pak")
+                self.path("hr.pak")
+                self.path("hu.pak")
+                self.path("id.pak")
+                self.path("it.pak")
+                self.path("ja.pak")
+                self.path("kn.pak")
+                self.path("ko.pak")
+                self.path("lt.pak")
+                self.path("lv.pak")
+                self.path("ml.pak")
+                self.path("mr.pak")
+                self.path("ms.pak")
+                self.path("nb.pak")
+                self.path("nl.pak")
+                self.path("pl.pak")
+                self.path("pt-BR.pak")
+                self.path("pt-PT.pak")
+                self.path("ro.pak")
+                self.path("ru.pak")
+                self.path("sk.pak")
+                self.path("sl.pak")
+                self.path("sr.pak")
+                self.path("sv.pak")
+                self.path("sw.pak")
+                self.path("ta.pak")
+                self.path("te.pak")
+                self.path("th.pak")
+                self.path("tr.pak")
+                self.path("uk.pak")
+                self.path("vi.pak")
+                self.path("zh-CN.pak")
+                self.path("zh-TW.pak")
+
+            with self.prefix(src=os.path.join(pkgdir, 'bin', 'release')):
+                self.path("libvlc.dll")
+                self.path("libvlccore.dll")
+                self.path("plugins/")
+
+        # pull in the crash logger from other projects
         # tag:"crash-logger" here as a cue to the exporter
         self.path(src='../win_crash_logger/%s/windows-crash-logger.exe' % self.args['configuration'],
                   dst="win_crash_logger.exe")
@@ -768,7 +782,7 @@ def package_finish(self):
         substitution_strings['installer_file'] = installer_file
         
         version_vars = """
-        !define INSTEXE "SL_Launcher.exe"
+        !define INSTEXE "SLVersionChecker.exe"
         !define VERSION "%(version_short)s"
         !define VERSION_LONG "%(version)s"
         !define VERSION_DASHES "%(version_dashes)s"
@@ -791,10 +805,10 @@ def package_finish(self):
 
         if(self.address_size == 64):
             engage_registry="SetRegView 64"
-            program_files="$PROGRAMFILES64"
+            program_files="!define MULTIUSER_USE_PROGRAMFILES64"
         else:
             engage_registry="SetRegView 32"
-            program_files="$PROGRAMFILES32"
+            program_files=""
 
         tempfile = "secondlife_setup_tmp.nsi"
         # the following replaces strings in the nsi template
@@ -812,16 +826,15 @@ def package_finish(self):
         # note that the enclosing setup exe is signed later, after the makensis makes it.
         # Unlike the viewer binary, the VMP filenames are invariant with respect to version, os, etc.
         for exe in (
-            "SL_Launcher.exe",
+            self.final_exe(),
+            "SLVersionChecker.exe",
             ):
             self.sign(exe)
             
-        # We use the Unicode version of NSIS, available from
-        # http://www.scratchpaper.com/
         # Check two paths, one for Program Files, and one for Program Files (x86).
         # Yay 64bit windows.
         for ProgramFiles in 'ProgramFiles', 'ProgramFiles(x86)':
-            NSIS_path = os.path.expandvars(r'${%s}\NSIS\Unicode\makensis.exe' % ProgramFiles)
+            NSIS_path = os.path.expandvars(r'${%s}\NSIS\makensis.exe' % ProgramFiles)
             if os.path.exists(NSIS_path):
                 break
         installer_created=False
@@ -879,395 +892,327 @@ def is_packaging_viewer(self):
         # darwin requires full app bundle packaging even for debugging.
         return True
 
-    def construct(self):
-        # These are the names of the top-level application and the embedded
-        # applications for the VMP and for the actual viewer, respectively.
-        # These names, without the .app suffix, determine the flyover text for
-        # their corresponding Dock icons.
-        toplevel_app, toplevel_icon = "Second Life.app",          "secondlife.icns"
-        launcher_app, launcher_icon = "Second Life Launcher.app", "secondlife.icns"
-        viewer_app,   viewer_icon   = "Second Life Viewer.app",   "secondlife.icns"
+    def is_rearranging(self):
+        # That said, some stuff should still only be performed once.
+        # Are either of these actions in 'actions'? Is the set intersection
+        # non-empty?
+        return bool(set(["package", "unpacked"]).intersection(self.args['actions']))
 
+    def construct(self):
         # copy over the build result (this is a no-op if run within the xcode script)
-        self.path(os.path.join(self.args['configuration'], toplevel_app), dst="")
+        self.path(os.path.join(self.args['configuration'], self.channel()+".app"), dst="")
 
         pkgdir = os.path.join(self.args['build'], os.pardir, 'packages')
         relpkgdir = os.path.join(pkgdir, "lib", "release")
         debpkgdir = os.path.join(pkgdir, "lib", "debug")
 
-        # -------------------- top-level Second Life.app ---------------------
-        # top-level Second Life application is only a container
         with self.prefix(src="", dst="Contents"):  # everything goes in Contents
-            # top-level Info.plist is as generated by CMake
-            Info_plist = "Info.plist"
-            ## This self.path() call reports 0 files... skip?
-            self.path(Info_plist)
-            Info_plist = self.dst_path_of(Info_plist)
-
-            # the one file in top-level MacOS directory is the trampoline to
-            # our nested launcher_app
+            bugsplat_db = self.args.get('bugsplat')
+            if bugsplat_db:
+                # Inject BugsplatServerURL into Info.plist if provided.
+                Info_plist = self.dst_path_of("Info.plist")
+                Info = plistlib.readPlist(Info_plist)
+                # https://www.bugsplat.com/docs/platforms/os-x#configuration
+                Info["BugsplatServerURL"] = \
+                    "https://{}.bugsplat.com/".format(bugsplat_db)
+                self.put_in_file(
+                    plistlib.writePlistToString(Info),
+                    os.path.basename(Info_plist),
+                    "Info.plist")
+
+            # CEF framework goes inside Contents/Frameworks.
+            # Remember where we parked this car.
+            with self.prefix(src="", dst="Frameworks"):
+                CEF_framework = "Chromium Embedded Framework.framework"
+                self.path2basename(relpkgdir, CEF_framework)
+                CEF_framework = self.dst_path_of(CEF_framework)
+
+                if self.args.get('bugsplat'):
+                    self.path2basename(relpkgdir, "BugsplatMac.framework")
+
             with self.prefix(dst="MacOS"):
-                toplevel_MacOS = self.get_dst_prefix()
-                trampoline = self.put_in_file("""\
-#!/bin/bash
-open "%s" --args "$@"
-""" %
-                    # up one directory from MacOS to its sibling Resources directory
-                    os.path.join('$(dirname "$0")', os.pardir, 'Resources', launcher_app),
-                    "SL_Launcher",      # write this file
-                    "trampoline")       # flag to add to list of copied files
-                # Script must be executable
-                self.run_command(["chmod", "+x", trampoline])
-
-            # Make a symlink to a nested app Frameworks directory that doesn't
-            # yet exist. We shouldn't need this; the only things that need
-            # Frameworks are nested apps under viewer_app, and they should
-            # simply find its Contents/Frameworks by relative pathnames. But
-            # empirically, we do: if we omit this symlink, CEF doesn't work --
-            # the login splash screen doesn't even display. SIIIIGH.
-            # We're passing a path that's already relative, hence symlinkf()
-            # rather than relsymlinkf().
-            self.symlinkf(os.path.join("Resources", viewer_app, "Contents", "Frameworks"))
-
-            with self.prefix(src="", dst="Resources"):
-                # top-level Resources directory should be pretty sparse
-                # need .icns file referenced by top-level Info.plist
+                executable = self.dst_path_of(self.channel())
+                if self.args.get('bugsplat'):
+                    # According to Apple Technical Note TN2206:
+                    # https://developer.apple.com/library/archive/technotes/tn2206/_index.html#//apple_ref/doc/uid/DTS40007919-CH1-TNTAG207
+                    # "If an app uses @rpath or an absolute path to link to a
+                    # dynamic library outside of the app, the app will be
+                    # rejected by Gatekeeper. ... Neither the codesign nor the
+                    # spctl tool will show the error."
+                    # (Thanks, Apple. Maybe fix spctl to warn?)
+                    # The BugsplatMac framework embeds @rpath, which is
+                    # causing scary Gatekeeper popups at viewer start. Work
+                    # around this by changing the reference baked into our
+                    # viewer. The install_name_tool -change option needs the
+                    # previous value. Instead of guessing -- which might
+                    # silently be defeated by a BugSplat SDK update that
+                    # changes their baked-in @rpath -- ask for the path
+                    # stamped into the framework.
+                    # Let exception, if any, propagate -- if this doesn't
+                    # work, we need the build to noisily fail!
+                    oldpath = subprocess.check_output(
+                        ['objdump', '-macho', '-dylib-id', '-non-verbose',
+                         os.path.join(relpkgdir, "BugsplatMac.framework", "BugsplatMac")]
+                        ).splitlines()[-1]  # take the last line of output
+                    self.run_command(
+                        ['install_name_tool', '-change', oldpath,
+                         '@executable_path/../Frameworks/BugsplatMac.framework/BugsplatMac',
+                         executable])
+
+                # NOTE: the -S argument to strip causes it to keep
+                # enough info for annotated backtraces (i.e. function
+                # names in the crash log). 'strip' with no arguments
+                # yields a slightly smaller binary but makes crash
+                # logs mostly useless. This may be desirable for the
+                # final release. Or not.
+                if ("package" in self.args['actions'] or 
+                    "unpacked" in self.args['actions']):
+                    self.run_command(
+                        ['strip', '-S', executable])
+
+            with self.prefix(dst="Resources"):
+                # defer cross-platform file copies until we're in the
+                # nested Resources directory
+                super(DarwinManifest, self).construct()
+
+                # need .icns file referenced by Info.plist
                 with self.prefix(src=self.icon_path(), dst="") :
-                    self.path(toplevel_icon)
-
-                # ------------------- nested launcher_app --------------------
-                with self.prefix(dst=os.path.join(launcher_app, "Contents")):
-                    # Info.plist is just like top-level one...
-                    Info = plistlib.readPlist(Info_plist)
-                    # except for these replacements:
-                    Info["CFBundleExecutable"] = "SL_Launcher"
-                    Info["CFBundleIconFile"] = launcher_icon
-                    self.put_in_file(
-                        plistlib.writePlistToString(Info),
-                        os.path.basename(Info_plist),
-                        "Info.plist")
-
-                    # copy VMP libs to MacOS
-                    with self.prefix(dst="MacOS"):              
-                        #this copies over the python wrapper script,
-                        #associated utilities and required libraries, see
-                        #SL-321, SL-322, SL-323
-                        with self.prefix(src=os.path.join(pkgdir, "VMP"), dst=""):
-                            self.path("SL_Launcher")
-                            self.path("*.py")
-                            # certifi will be imported by requests; this is
-                            # our custom version to get our ca-bundle.crt
-                            self.path("certifi")
-                        with self.prefix(src=os.path.join(pkgdir, "lib", "python"), dst=""):
-                            # llbase provides our llrest service layer and llsd decoding
-                            with self.prefix("llbase"):
-                                # (Why is llbase treated specially here? What
-                                # DON'T we want to copy out of lib/python/llbase?)
-                                self.path("*.py")
-                                self.path("_cllsd.so")
-                            #requests module needed by llbase/llrest.py
-                            #this is only needed on POSIX, because in Windows
-                            #we compile it into the EXE
-                            for pypkg in "chardet", "idna", "requests", "urllib3":
-                                self.path(pypkg)
-
-                    # launcher_app/Contents/Resources
-                    with self.prefix(dst="Resources"):
-                        with self.prefix(src=self.icon_path(), dst="") :
-                            self.path(launcher_icon)
-                            with self.prefix(dst="vmp_icons"):
-                                self.path("secondlife.ico")
-                        #VMP Tkinter icons
-                        with self.prefix("vmp_icons"):
-                            self.path("*.png")
-                            self.path("*.gif")
-
-                # -------------------- nested viewer_app ---------------------
-                with self.prefix(dst=os.path.join(viewer_app, "Contents")):
-                    # Info.plist is just like top-level one...
-                    Info = plistlib.readPlist(Info_plist)
-                    # except for these replacements:
-                    # (CFBundleExecutable may be moot: SL_Launcher directly
-                    # runs the executable, instead of launching the app)
-                    Info["CFBundleExecutable"] = "Second Life"
-                    Info["CFBundleIconFile"] = viewer_icon
-                    self.put_in_file(
-                        plistlib.writePlistToString(Info),
-                        os.path.basename(Info_plist),
-                        "Info.plist")
-
-                    # CEF framework goes inside viewer_app/Contents/Frameworks.
-                    # Remember where we parked this car.
-                    with self.prefix(src="", dst="Frameworks"):
-                        CEF_framework = "Chromium Embedded Framework.framework"
-                        self.path2basename(relpkgdir, CEF_framework)
-                        CEF_framework = self.dst_path_of(CEF_framework)
-
-                    with self.prefix(dst="MacOS"):
-                        # CMake constructs the Second Life executable in the
-                        # MacOS directory belonging to the top-level Second
-                        # Life.app. Move it here.
-                        here = self.get_dst_prefix()
-                        relbase = os.path.realpath(os.path.dirname(Info_plist))
-                        self.cmakedirs(here)
-                        for f in os.listdir(toplevel_MacOS):
-                            if f == os.path.basename(trampoline):
-                                # don't move the trampoline script we just made!
-                                continue
-                            fromwhere = os.path.join(toplevel_MacOS, f)
-                            towhere   = os.path.join(here, f)
-                            print "Moving %s => %s" % \
-                                  (self.relpath(fromwhere, relbase),
-                                   self.relpath(towhere, relbase))
-                            # now do it, only without relativizing paths
-                            os.rename(fromwhere, towhere)
-
-                        # NOTE: the -S argument to strip causes it to keep
-                        # enough info for annotated backtraces (i.e. function
-                        # names in the crash log). 'strip' with no arguments
-                        # yields a slightly smaller binary but makes crash
-                        # logs mostly useless. This may be desirable for the
-                        # final release. Or not.
-                        if ("package" in self.args['actions'] or 
-                            "unpacked" in self.args['actions']):
-                            self.run_command(
-                                ['strip', '-S', self.dst_path_of('Second Life')])
-
-                    with self.prefix(dst="Resources"):
-                        # defer cross-platform file copies until we're in the right
-                        # nested Resources directory
-                        super(DarwinManifest, self).construct()
-
-                        with self.prefix(src=self.icon_path(), dst="") :
-                            self.path(viewer_icon)
-
-                        with self.prefix(src=relpkgdir, dst=""):
-                            self.path("libndofdev.dylib")
-                            self.path("libhunspell-1.3.0.dylib")   
-
-                        with self.prefix("cursors_mac"):
-                            self.path("*.tif")
-
-                        self.path("licenses-mac.txt", dst="licenses.txt")
-                        self.path("featuretable_mac.txt")
-                        self.path("SecondLife.nib")
-
-                        with self.prefix(src=pkgdir,dst=""):
-                            self.path("ca-bundle.crt")
-
-                        self.path("SecondLife.nib")
-
-                        # Translations
-                        self.path("English.lproj/language.txt")
-                        self.replace_in(src="English.lproj/InfoPlist.strings",
-                                        dst="English.lproj/InfoPlist.strings",
-                                        searchdict={'%%VERSION%%':'.'.join(self.args['version'])}
-                                        )
-                        self.path("German.lproj")
-                        self.path("Japanese.lproj")
-                        self.path("Korean.lproj")
-                        self.path("da.lproj")
-                        self.path("es.lproj")
-                        self.path("fr.lproj")
-                        self.path("hu.lproj")
-                        self.path("it.lproj")
-                        self.path("nl.lproj")
-                        self.path("pl.lproj")
-                        self.path("pt.lproj")
-                        self.path("ru.lproj")
-                        self.path("tr.lproj")
-                        self.path("uk.lproj")
-                        self.path("zh-Hans.lproj")
-
-                        def path_optional(src, dst):
-                            """
-                            For a number of our self.path() calls, not only do we want
-                            to deal with the absence of src, we also want to remember
-                            which were present. Return either an empty list (absent)
-                            or a list containing dst (present). Concatenate these
-                            return values to get a list of all libs that are present.
-                            """
-                            # This was simple before we started needing to pass
-                            # wildcards. Fortunately, self.path() ends up appending a
-                            # (source, dest) pair to self.file_list for every expanded
-                            # file processed. Remember its size before the call.
-                            oldlen = len(self.file_list)
-                            self.path(src, dst)
-                            # The dest appended to self.file_list has been prepended
-                            # with self.get_dst_prefix(). Strip it off again.
-                            added = [os.path.relpath(d, self.get_dst_prefix())
-                                     for s, d in self.file_list[oldlen:]]
-                            if not added:
-                                print "Skipping %s" % dst
-                            return added
-
-                        # dylibs is a list of all the .dylib files we expect to need
-                        # in our bundled sub-apps. For each of these we'll create a
-                        # symlink from sub-app/Contents/Resources to the real .dylib.
-                        # Need to get the llcommon dll from any of the build directories as well.
-                        libfile_parent = self.get_dst_prefix()
-                        libfile = "libllcommon.dylib"
-                        dylibs = path_optional(self.find_existing_file(os.path.join(os.pardir,
-                                                                       "llcommon",
-                                                                       self.args['configuration'],
-                                                                       libfile),
-                                                                       os.path.join(relpkgdir, libfile)),
-                                               dst=libfile)
-
-                        for libfile in (
-                                        "libapr-1.0.dylib",
-                                        "libaprutil-1.0.dylib",
-                                        "libcollada14dom.dylib",
-                                        "libexpat.1.dylib",
-                                        "libexception_handler.dylib",
-                                        "libGLOD.dylib",
-                                        # libnghttp2.dylib is a symlink to
-                                        # libnghttp2.major.dylib, which is a symlink to
-                                        # libnghttp2.version.dylib. Get all of them.
-                                        "libnghttp2.*dylib",
-                                        ):
-                            dylibs += path_optional(os.path.join(relpkgdir, libfile), libfile)
-
-                        # SLVoice and vivox lols, no symlinks needed
-                        for libfile in (
-                                        'libortp.dylib',
-                                        'libsndfile.dylib',
-                                        'libvivoxoal.dylib',
-                                        'libvivoxsdk.dylib',
-                                        'libvivoxplatform.dylib',
-                                        'SLVoice',
-                                        ):
-                            self.path2basename(relpkgdir, libfile)
-
-                        # dylibs that vary based on configuration
-                        if self.args['configuration'].lower() == 'debug':
-                            for libfile in (
-                                        "libfmodexL.dylib",
-                                        ):
-                                dylibs += path_optional(os.path.join(debpkgdir, libfile), libfile)
-                        else:
-                            for libfile in (
-                                        "libfmodex.dylib",
-                                        ):
-                                dylibs += path_optional(os.path.join(relpkgdir, libfile), libfile)
-
-                        # our apps
-                        executable_path = {}
-                        for app_bld_dir, app in (("mac_crash_logger", "mac-crash-logger.app"),
-                                                 # plugin launcher
-                                                 (os.path.join("llplugin", "slplugin"), "SLPlugin.app"),
-                                                 ):
-                            self.path2basename(os.path.join(os.pardir,
-                                                            app_bld_dir, self.args['configuration']),
-                                               app)
-                            executable_path[app] = \
-                                self.dst_path_of(os.path.join(app, "Contents", "MacOS"))
-
-                            # our apps dependencies on shared libs
-                            # for each app, for each dylib we collected in dylibs,
-                            # create a symlink to the real copy of the dylib.
-                            with self.prefix(dst=os.path.join(app, "Contents", "Resources")):
-                                for libfile in dylibs:
-                                    self.relsymlinkf(os.path.join(libfile_parent, libfile))
-
-                        # Dullahan helper apps go inside SLPlugin.app
-                        with self.prefix(dst=os.path.join(
-                            "SLPlugin.app", "Contents", "Frameworks")):
-
-                            frameworkname = 'Chromium Embedded Framework'
-
-                            # This code constructs a relative symlink from the
-                            # target framework folder back to the real CEF framework.
-                            # It needs to be relative so that the symlink still works when
-                            # (as is normal) the user moves the app bundle out of the DMG
-                            # and into the /Applications folder. Note we pass catch=False,
-                            # letting the uncaught exception terminate the process, since
-                            # without this symlink, Second Life web media can't possibly work.
-
-                            # It might seem simpler just to symlink Frameworks back to
-                            # the parent of Chromimum Embedded Framework.framework. But
-                            # that would create a symlink cycle, which breaks our
-                            # packaging step. So make a symlink from Chromium Embedded
-                            # Framework.framework to the directory of the same name, which
-                            # is NOT an ancestor of the symlink.
-
-                            # from SLPlugin.app/Contents/Frameworks/Chromium Embedded
-                            # Framework.framework back to
-                            # $viewer_app/Contents/Frameworks/Chromium Embedded Framework.framework
-                            SLPlugin_framework = self.relsymlinkf(CEF_framework, catch=False)
-
-                            # copy DullahanHelper.app
-                            self.path2basename(relpkgdir, 'DullahanHelper.app')
-
-                            # and fix that up with a Frameworks/CEF symlink too
-                            with self.prefix(dst=os.path.join(
-                                'DullahanHelper.app', 'Contents', 'Frameworks')):
-                                # from Dullahan Helper.app/Contents/Frameworks/Chromium Embedded
-                                # Framework.framework back to
-                                # SLPlugin.app/Contents/Frameworks/Chromium Embedded Framework.framework
-                                # Since SLPlugin_framework is itself a
-                                # symlink, don't let relsymlinkf() resolve --
-                                # explicitly call relpath(symlink=True) and
-                                # create that symlink here.
-                                DullahanHelper_framework = \
-                                    self.symlinkf(self.relpath(SLPlugin_framework, symlink=True),
-                                                  catch=False)
-
-                            # change_command includes install_name_tool, the
-                            # -change subcommand and the old framework rpath
-                            # stamped into the executable. To use it with
-                            # run_command(), we must still append the new
-                            # framework path and the pathname of the
-                            # executable to change.
-                            change_command = [
-                                'install_name_tool', '-change',
-                                '@rpath/Frameworks/Chromium Embedded Framework.framework/Chromium Embedded Framework']
-
-                            with self.prefix(dst=os.path.join(
-                                'DullahanHelper.app', 'Contents', 'MacOS')):
-                                # Now self.get_dst_prefix() is, at runtime,
-                                # @executable_path. Locate the helper app
-                                # framework (which is a symlink) from here.
-                                newpath = os.path.join(
-                                    '@executable_path',
-                                    self.relpath(DullahanHelper_framework, symlink=True),
-                                    frameworkname)
-                                # and restamp the DullahanHelper executable
-                                self.run_command(
-                                    change_command +
-                                    [newpath, self.dst_path_of('DullahanHelper')])
-
-                        # SLPlugin plugins
-                        with self.prefix(dst="llplugin"):
-                            dylibexecutable = 'media_plugin_cef.dylib'
-                            self.path2basename("../media_plugins/cef/" + self.args['configuration'],
-                                               dylibexecutable)
-
-                            # Do this install_name_tool *after* media plugin is copied over.
-                            # Locate the framework lib executable -- relative to
-                            # SLPlugin.app/Contents/MacOS, which will be our
-                            # @executable_path at runtime!
-                            newpath = os.path.join(
-                                '@executable_path',
-                                self.relpath(SLPlugin_framework, executable_path["SLPlugin.app"],
-                                             symlink=True),
-                                frameworkname)
-                            # restamp media_plugin_cef.dylib
-                            self.run_command(
-                                change_command +
-                                [newpath, self.dst_path_of(dylibexecutable)])
+                    self.path("secondlife.icns")
+
+                # Copy in the updater script and helper modules
+                self.path(src=os.path.join(pkgdir, 'VMP'), dst="updater")
+
+                with self.prefix(src="", dst=os.path.join("updater", "icons")):
+                    self.path2basename(self.icon_path(), "secondlife.ico")
+                    with self.prefix(src="vmp_icons", dst=""):
+                        self.path("*.png")
+                        self.path("*.gif")
 
-                            # copy LibVLC plugin itself
-                            self.path2basename("../media_plugins/libvlc/" + self.args['configuration'],
-                                               "media_plugin_libvlc.dylib")
+                with self.prefix(src=relpkgdir, dst=""):
+                    self.path("libndofdev.dylib")
+                    self.path("libhunspell-1.3.0.dylib")   
 
-                            # copy LibVLC dynamic libraries
-                            with self.prefix(src=relpkgdir, dst="lib"):
-                                self.path( "libvlc*.dylib*" )
-                                # copy LibVLC plugins folder
-                                with self.prefix(src='plugins', dst=""):
-                                    self.path( "*.dylib" )
-                                    self.path( "plugins.dat" )
+                with self.prefix(src_dst="cursors_mac"):
+                    self.path("*.tif")
+
+                self.path("licenses-mac.txt", dst="licenses.txt")
+                self.path("featuretable_mac.txt")
+                self.path("SecondLife.nib")
+
+                with self.prefix(src=pkgdir,dst=""):
+                    self.path("ca-bundle.crt")
+
+                # Translations
+                self.path("English.lproj/language.txt")
+                self.replace_in(src="English.lproj/InfoPlist.strings",
+                                dst="English.lproj/InfoPlist.strings",
+                                searchdict={'%%VERSION%%':'.'.join(self.args['version'])}
+                                )
+                self.path("German.lproj")
+                self.path("Japanese.lproj")
+                self.path("Korean.lproj")
+                self.path("da.lproj")
+                self.path("es.lproj")
+                self.path("fr.lproj")
+                self.path("hu.lproj")
+                self.path("it.lproj")
+                self.path("nl.lproj")
+                self.path("pl.lproj")
+                self.path("pt.lproj")
+                self.path("ru.lproj")
+                self.path("tr.lproj")
+                self.path("uk.lproj")
+                self.path("zh-Hans.lproj")
+
+                def path_optional(src, dst):
+                    """
+                    For a number of our self.path() calls, not only do we want
+                    to deal with the absence of src, we also want to remember
+                    which were present. Return either an empty list (absent)
+                    or a list containing dst (present). Concatenate these
+                    return values to get a list of all libs that are present.
+                    """
+                    # This was simple before we started needing to pass
+                    # wildcards. Fortunately, self.path() ends up appending a
+                    # (source, dest) pair to self.file_list for every expanded
+                    # file processed. Remember its size before the call.
+                    oldlen = len(self.file_list)
+                    self.path(src, dst)
+                    # The dest appended to self.file_list has been prepended
+                    # with self.get_dst_prefix(). Strip it off again.
+                    added = [os.path.relpath(d, self.get_dst_prefix())
+                             for s, d in self.file_list[oldlen:]]
+                    if not added:
+                        print "Skipping %s" % dst
+                    return added
+
+                # dylibs is a list of all the .dylib files we expect to need
+                # in our bundled sub-apps. For each of these we'll create a
+                # symlink from sub-app/Contents/Resources to the real .dylib.
+                # Need to get the llcommon dll from any of the build directories as well.
+                libfile_parent = self.get_dst_prefix()
+                libfile = "libllcommon.dylib"
+                dylibs = path_optional(self.find_existing_file(os.path.join(os.pardir,
+                                                               "llcommon",
+                                                               self.args['configuration'],
+                                                               libfile),
+                                                               os.path.join(relpkgdir, libfile)),
+                                       dst=libfile)
+
+                for libfile in (
+                                "libapr-1.0.dylib",
+                                "libaprutil-1.0.dylib",
+                                "libcollada14dom.dylib",
+                                "libexpat.1.dylib",
+                                "libexception_handler.dylib",
+                                "libGLOD.dylib",
+                                # libnghttp2.dylib is a symlink to
+                                # libnghttp2.major.dylib, which is a symlink to
+                                # libnghttp2.version.dylib. Get all of them.
+                                "libnghttp2.*dylib",
+                                ):
+                    dylibs += path_optional(os.path.join(relpkgdir, libfile), libfile)
+
+                # SLVoice and vivox lols, no symlinks needed
+                for libfile in (
+                                'libortp.dylib',
+                                'libsndfile.dylib',
+                                'libvivoxoal.dylib',
+                                'libvivoxsdk.dylib',
+                                'libvivoxplatform.dylib',
+                                'SLVoice',
+                                ):
+                    self.path2basename(relpkgdir, libfile)
+
+                # dylibs that vary based on configuration
+                if self.args['configuration'].lower() == 'debug':
+                    for libfile in (
+                                "libfmodexL.dylib",
+                                ):
+                        dylibs += path_optional(os.path.join(debpkgdir, libfile), libfile)
+                else:
+                    for libfile in (
+                                "libfmodex.dylib",
+                                ):
+                        dylibs += path_optional(os.path.join(relpkgdir, libfile), libfile)
+
+                # our apps
+                executable_path = {}
+                for app_bld_dir, app in (("mac_crash_logger", "mac-crash-logger.app"),
+                                         # plugin launcher
+                                         (os.path.join("llplugin", "slplugin"), "SLPlugin.app"),
+                                         ):
+                    self.path2basename(os.path.join(os.pardir,
+                                                    app_bld_dir, self.args['configuration']),
+                                       app)
+                    executable_path[app] = \
+                        self.dst_path_of(os.path.join(app, "Contents", "MacOS"))
+
+                    # our apps dependencies on shared libs
+                    # for each app, for each dylib we collected in dylibs,
+                    # create a symlink to the real copy of the dylib.
+                    with self.prefix(dst=os.path.join(app, "Contents", "Resources")):
+                        for libfile in dylibs:
+                            self.relsymlinkf(os.path.join(libfile_parent, libfile))
+
+                # Dullahan helper apps go inside SLPlugin.app
+                with self.prefix(dst=os.path.join(
+                    "SLPlugin.app", "Contents", "Frameworks")):
+
+                    frameworkname = 'Chromium Embedded Framework'
+
+                    # This code constructs a relative symlink from the
+                    # target framework folder back to the real CEF framework.
+                    # It needs to be relative so that the symlink still works when
+                    # (as is normal) the user moves the app bundle out of the DMG
+                    # and into the /Applications folder. Note we pass catch=False,
+                    # letting the uncaught exception terminate the process, since
+                    # without this symlink, Second Life web media can't possibly work.
+
+                    # It might seem simpler just to symlink Frameworks back to
+                    # the parent of Chromimum Embedded Framework.framework. But
+                    # that would create a symlink cycle, which breaks our
+                    # packaging step. So make a symlink from Chromium Embedded
+                    # Framework.framework to the directory of the same name, which
+                    # is NOT an ancestor of the symlink.
+
+                    # from SLPlugin.app/Contents/Frameworks/Chromium Embedded
+                    # Framework.framework back to
+                    # $viewer_app/Contents/Frameworks/Chromium Embedded Framework.framework
+                    SLPlugin_framework = self.relsymlinkf(CEF_framework, catch=False)
+
+                    # copy DullahanHelper.app
+                    self.path2basename(relpkgdir, 'DullahanHelper.app')
+
+                    # and fix that up with a Frameworks/CEF symlink too
+                    with self.prefix(dst=os.path.join(
+                        'DullahanHelper.app', 'Contents', 'Frameworks')):
+                        # from Dullahan Helper.app/Contents/Frameworks/Chromium Embedded
+                        # Framework.framework back to
+                        # SLPlugin.app/Contents/Frameworks/Chromium Embedded Framework.framework
+                        # Since SLPlugin_framework is itself a
+                        # symlink, don't let relsymlinkf() resolve --
+                        # explicitly call relpath(symlink=True) and
+                        # create that symlink here.
+                        DullahanHelper_framework = \
+                            self.symlinkf(self.relpath(SLPlugin_framework, symlink=True),
+                                          catch=False)
+
+                    # change_command includes install_name_tool, the
+                    # -change subcommand and the old framework rpath
+                    # stamped into the executable. To use it with
+                    # run_command(), we must still append the new
+                    # framework path and the pathname of the
+                    # executable to change.
+                    change_command = [
+                        'install_name_tool', '-change',
+                        '@rpath/Frameworks/Chromium Embedded Framework.framework/Chromium Embedded Framework']
+
+                    with self.prefix(dst=os.path.join(
+                        'DullahanHelper.app', 'Contents', 'MacOS')):
+                        # Now self.get_dst_prefix() is, at runtime,
+                        # @executable_path. Locate the helper app
+                        # framework (which is a symlink) from here.
+                        newpath = os.path.join(
+                            '@executable_path',
+                            self.relpath(DullahanHelper_framework, symlink=True),
+                            frameworkname)
+                        # and restamp the DullahanHelper executable
+                        self.run_command(
+                            change_command +
+                            [newpath, self.dst_path_of('DullahanHelper')])
+
+                # SLPlugin plugins
+                with self.prefix(dst="llplugin"):
+                    dylibexecutable = 'media_plugin_cef.dylib'
+                    self.path2basename("../media_plugins/cef/" + self.args['configuration'],
+                                       dylibexecutable)
+
+                    # Do this install_name_tool *after* media plugin is copied over.
+                    # Locate the framework lib executable -- relative to
+                    # SLPlugin.app/Contents/MacOS, which will be our
+                    # @executable_path at runtime!
+                    newpath = os.path.join(
+                        '@executable_path',
+                        self.relpath(SLPlugin_framework, executable_path["SLPlugin.app"],
+                                     symlink=True),
+                        frameworkname)
+                    # restamp media_plugin_cef.dylib
+                    self.run_command(
+                        change_command +
+                        [newpath, self.dst_path_of(dylibexecutable)])
+
+                    # copy LibVLC plugin itself
+                    self.path2basename("../media_plugins/libvlc/" + self.args['configuration'],
+                                       "media_plugin_libvlc.dylib")
+
+                    # copy LibVLC dynamic libraries
+                    with self.prefix(src=relpkgdir, dst="lib"):
+                        self.path( "libvlc*.dylib*" )
+                        # copy LibVLC plugins folder
+                        with self.prefix(src='plugins', dst=""):
+                            self.path( "*.dylib" )
+                            self.path( "plugins.dat" )
 
     def package_finish(self):
         global CHANNEL_VENDOR_BASE
@@ -1416,10 +1361,7 @@ def package_finish(self):
                             else:
                                 print >> sys.stderr, "Maximum codesign attempts exceeded; giving up"
                                 raise
-                    self.run_command(['spctl', '-a', '-texec', '-vv', app_in_dmg])
-
-            imagename="SecondLife_" + '_'.join(self.args['version'])
-
+                    self.run_command(['spctl', '-a', '-texec', '-vvvv', app_in_dmg])
 
         finally:
             # Unmount the image even if exceptions from any of the above 
@@ -1458,29 +1400,25 @@ def construct(self):
         debpkgdir = os.path.join(pkgdir, "lib", "debug")
 
         self.path("licenses-linux.txt","licenses.txt")
-        with self.prefix("linux_tools", dst=""):
+        with self.prefix("linux_tools"):
             self.path("client-readme.txt","README-linux.txt")
             self.path("client-readme-voice.txt","README-linux-voice.txt")
             self.path("client-readme-joystick.txt","README-linux-joystick.txt")
             self.path("wrapper.sh","secondlife")
-            with self.prefix(src="", dst="etc"):
+            with self.prefix(dst="etc"):
                 self.path("handle_secondlifeprotocol.sh")
                 self.path("register_secondlifeprotocol.sh")
                 self.path("refresh_desktop_app_entry.sh")
                 self.path("launch_url.sh")
             self.path("install.sh")
 
-        with self.prefix(src="", dst="bin"):
+        with self.prefix(dst="bin"):
             self.path("secondlife-bin","do-not-directly-run-secondlife-bin")
             self.path("../linux_crash_logger/linux-crash-logger","linux-crash-logger.bin")
             self.path2basename("../llplugin/slplugin", "SLPlugin") 
             #this copies over the python wrapper script, associated utilities and required libraries, see SL-321, SL-322 and SL-323
             with self.prefix(src="../viewer_components/manager", dst=""):
-                self.path("SL_Launcher")
                 self.path("*.py")
-            with self.prefix(src=os.path.join("lib", "python", "llbase"), dst="llbase"):
-                self.path("*.py")
-                self.path("_cllsd.so")         
 
         # recurses, packaged again
         self.path("res-sdl")
@@ -1488,9 +1426,9 @@ def construct(self):
         # Get the icons based on the channel type
         icon_path = self.icon_path()
         print "DEBUG: icon_path '%s'" % icon_path
-        with self.prefix(src=icon_path, dst="") :
+        with self.prefix(src=icon_path) :
             self.path("secondlife_256.png","secondlife_icon.png")
-            with self.prefix(src="",dst="res-sdl") :
+            with self.prefix(dst="res-sdl") :
                 self.path("secondlife_256.BMP","ll_icon.BMP")
 
         # plugins
@@ -1512,7 +1450,7 @@ def construct(self):
 
         self.path("featuretable_linux.txt")
 
-        with self.prefix(src=pkgdir,dst=""):
+        with self.prefix(src=pkgdir):
             self.path("ca-bundle.crt")
 
     def package_finish(self):
@@ -1555,7 +1493,7 @@ def strip_binaries(self):
             self.run_command(
                 ["find"] +
                 [os.path.join(self.get_dst_prefix(), dir) for dir in ('bin', 'lib')] +
-                ['-type', 'f', '!', '-name', '*.py', '!', '-name', 'SL_Launcher',
+                ['-type', 'f', '!', '-name', '*.py',
                  '!', '-name', 'update_install', '-exec', 'strip', '-S', '{}', ';'])
 
 class Linux_i686_Manifest(LinuxManifest):
@@ -1568,7 +1506,7 @@ def construct(self):
         relpkgdir = os.path.join(pkgdir, "lib", "release")
         debpkgdir = os.path.join(pkgdir, "lib", "debug")
 
-        with self.prefix(relpkgdir, dst="lib"):
+        with self.prefix(src=relpkgdir, dst="lib"):
             self.path("libapr-1.so")
             self.path("libapr-1.so.0")
             self.path("libapr-1.so.0.4.5")
@@ -1654,4 +1592,8 @@ def construct(self):
 ################################################################
 
 if __name__ == "__main__":
-    main()
+    extra_arguments = [
+        dict(name='bugsplat', description="""BugSplat database to which to post crashes,
+             if BugSplat crash reporting is desired""", default=''),
+        ]
+    main(extra=extra_arguments)
diff --git a/indra/test/test.cpp b/indra/test/test.cpp
index bdeaeec970aed8053c3f1007b6f1e68343c24956..861ec1d942fac8120aac7435dbf6fdbe5106ca7b 100644
--- a/indra/test/test.cpp
+++ b/indra/test/test.cpp
@@ -534,7 +534,6 @@ int main(int argc, char **argv)
 		LLError::setDefaultLevel(LLError::LEVEL_DEBUG);
 	}	
 	LLError::setFatalFunction(wouldHaveCrashed);
-	LLError::setPrintLocation(true);
 	std::string test_app_name(argv[0]);
 	std::string test_log = test_app_name + ".log";
 	LLFile::remove(test_log);
diff --git a/indra/viewer_components/login/lllogin.cpp b/indra/viewer_components/login/lllogin.cpp
index c767d52c7bd3aac30f2d33ec681ac549cf3d42b1..9193d32b498ff0494e430097ee121f0bac653a7c 100644
--- a/indra/viewer_components/login/lllogin.cpp
+++ b/indra/viewer_components/login/lllogin.cpp
@@ -128,6 +128,15 @@ void LLLogin::Impl::connect(const std::string& uri, const LLSD& login_params)
     LL_DEBUGS("LLLogin") << " connected with  uri '" << uri << "', login_params " << login_params << LL_ENDL;	
 }
 
+namespace {
+// Instantiate this rendezvous point at namespace scope so it's already
+// present no matter how early the updater might post to it.
+// Use an LLEventMailDrop, which has future-like semantics: regardless of the
+// relative order in which post() or listen() are called, it delivers each
+// post() event to its listener(s) until one of them consumes that event.
+static LLEventMailDrop sSyncPoint("LoginSync");
+}
+
 void LLLogin::Impl::loginCoro(std::string uri, LLSD login_params)
 {
     LLSD printable_params = login_params;
@@ -219,7 +228,44 @@ void LLLogin::Impl::loginCoro(std::string uri, LLSD login_params)
         }
         else
         {
-            sendProgressEvent("offline", "fail.login", mAuthResponse["responses"]);
+            // Synchronize here with the updater. We synchronize here rather
+            // than in the fail.login handler, which actually examines the
+            // response from login.cgi, because here we are definitely in a
+            // coroutine and can definitely use suspendUntilBlah(). Whoever's
+            // listening for fail.login might not be.
+
+            // If the reason for login failure is that we must install a
+            // required update, we definitely want to pass control to the
+            // updater to manage that for us. We'll handle any other login
+            // failure ourselves, as usual. We figure that no matter where you
+            // are in the world, or what kind of network you're on, we can
+            // reasonably expect the Viewer Version Manager to respond more or
+            // less as quickly as login.cgi. This synchronization is only
+            // intended to smooth out minor races between the two services.
+            // But what if the updater crashes? Use a timeout so that
+            // eventually we'll tire of waiting for it and carry on as usual.
+            // Given the above, it can be a fairly short timeout, at least
+            // from a human point of view.
+
+            // Since sSyncPoint is an LLEventMailDrop, we DEFINITELY want to
+            // consume the posted event.
+            LLCoros::OverrideConsuming oc(true);
+            // Timeout should produce the isUndefined() object passed here.
+            LL_DEBUGS("LLLogin") << "Login failure, waiting for sync from updater" << LL_ENDL;
+            LLSD updater = llcoro::suspendUntilEventOnWithTimeout(sSyncPoint, 10, LLSD());
+            if (updater.isUndefined())
+            {
+                LL_WARNS("LLLogin") << "Failed to hear from updater, proceeding with fail.login"
+                                    << LL_ENDL;
+            }
+            else
+            {
+                LL_DEBUGS("LLLogin") << "Got responses from updater and login.cgi" << LL_ENDL;
+            }
+            // Let the fail.login handler deal with empty updater response.
+            LLSD responses(mAuthResponse["responses"]);
+            responses["updater"] = updater;
+            sendProgressEvent("offline", "fail.login", responses);
         }
         return;             // Done!
     }
@@ -249,10 +295,10 @@ void LLLogin::Impl::loginCoro(std::string uri, LLSD login_params)
     // *NOTE: The response from LLXMLRPCListener's Poller::poll method returns an
     // llsd with no "responses" node. To make the output from an incomplete login symmetrical 
     // to success, add a data/message and data/reason fields.
-    LLSD error_response;
-    error_response["reason"] = mAuthResponse["status"];
-    error_response["errorcode"] = mAuthResponse["errorcode"];
-    error_response["message"] = mAuthResponse["error"];
+    LLSD error_response(LLSDMap
+                        ("reason",    mAuthResponse["status"])
+                        ("errorcode", mAuthResponse["errorcode"])
+                        ("message",   mAuthResponse["error"]));
     if(mAuthResponse.has("certificate"))
     {
         error_response["certificate"] = mAuthResponse["certificate"];
diff --git a/indra/win_crash_logger/CMakeLists.txt b/indra/win_crash_logger/CMakeLists.txt
index 144d037a319e69436a60d2999adc836f1c23d920..4fba26ab2f0af23918237c84a716997884abbd08 100644
--- a/indra/win_crash_logger/CMakeLists.txt
+++ b/indra/win_crash_logger/CMakeLists.txt
@@ -89,7 +89,6 @@ target_link_libraries(windows-crash-logger
     ${GOOGLE_PERFTOOLS_LIBRARIES}
     user32
     gdi32
-    ole32
     oleaut32
     wininet
     Wldap32