diff --git a/.github/release.yaml b/.github/release.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..5af13f2ca6b0c289d44bec432de45cf8470caad9
--- /dev/null
+++ b/.github/release.yaml
@@ -0,0 +1,18 @@
+changelog:
+  exclude:
+    labels:
+      - ignore-for-release
+    authors:
+      - dependabot 
+  categories:
+    - title: Breaking Changes 🛠
+      labels:
+        - semver-major
+        - breaking-change
+    - title: New Features 🎉
+      labels:
+        - semver-minor
+        - enhancement
+    - title: Other Changes
+      labels:
+        - '*'
diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml
index 17c0ace02fc3fd46b3f4105560ef9aee9ee2e859..d626eef38d7fe3fff3c604537838c95b7fdb560c 100644
--- a/.github/workflows/pre-commit.yaml
+++ b/.github/workflows/pre-commit.yaml
@@ -11,7 +11,7 @@ jobs:
   pre-commit:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
       - uses: actions/setup-python@v4
         with:
           python-version: 3.x
diff --git a/.gitignore b/.gitignore
index 903a02a8c9f314e50f8d6a5d769b83c3c7a3ab58..4c910ec89690225dbb8d54806f6547e9e805fc90 100755
--- a/.gitignore
+++ b/.gitignore
@@ -82,3 +82,4 @@ web/config.*
 web/locale.*
 web/secondlife.com.*
 /Pipfile.lock
+.env
diff --git a/autobuild.xml b/autobuild.xml
index bd07899a5c3d2d78472086ab3db1a4ea4eaa5c8b..e2eb42220b4490ed7a170b41cbca646d6213379a 100644
--- a/autobuild.xml
+++ b/autobuild.xml
@@ -1378,22 +1378,6 @@
             <key>name</key>
             <string>darwin64</string>
           </map>
-          <key>windows</key>
-          <map>
-            <key>archive</key>
-            <map>
-              <key>creds</key>
-              <string>gitlab</string>
-              <key>hash</key>
-              <string>f0d5a4733498fbc779bed2639ea5a14cf2cf888c7e74c446e24df00d593e69c2edce17592ea69da7fff3ef8070672534d074a7654d3e18c691dcef5cc4c4ec1e</string>
-              <key>hash_algorithm</key>
-              <string>blake2b</string>
-              <key>url</key>
-              <string>https://git.alchemyviewer.org/api/v4/projects/192/packages/generic/llphysicsextensions_tpv/1.0.577418/llphysicsextensions_tpv-1.0.577418-windows-577418.tar.bz2</string>
-            </map>
-            <key>name</key>
-            <string>windows</string>
-          </map>
           <key>windows64</key>
           <map>
             <key>archive</key>
@@ -2075,18 +2059,6 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors</string>
             <key>name</key>
             <string>linux64</string>
           </map>
-          <key>windows</key>
-          <map>
-            <key>archive</key>
-            <map>
-              <key>hash</key>
-              <string>6e0ed41653955afe8eeb8945776cf07b</string>
-              <key>url</key>
-              <string>https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/98683/871560/slvoice-4.10.0000.32327.5fc3fe7c.571099-windows-571099.tar.bz2</string>
-            </map>
-            <key>name</key>
-            <string>windows</string>
-          </map>
           <key>windows64</key>
           <map>
             <key>archive</key>
@@ -2385,18 +2357,6 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors</string>
             <key>name</key>
             <string>darwin64</string>
           </map>
-          <key>windows</key>
-          <map>
-            <key>archive</key>
-            <map>
-              <key>hash</key>
-              <string>58eea384be49ba756ce9c5e66669540b</string>
-              <key>url</key>
-              <string>https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/101318/891520/vulkan_gltf-1-windows-572743.tar.bz2</string>
-            </map>
-            <key>name</key>
-            <string>windows</string>
-          </map>
           <key>windows64</key>
           <map>
             <key>archive</key>
diff --git a/buildscripts_support_functions b/buildscripts_support_functions
new file mode 100644
index 0000000000000000000000000000000000000000..557d2f80fbe12300d5f558e9ee889e84b0c32799
--- /dev/null
+++ b/buildscripts_support_functions
@@ -0,0 +1,60 @@
+# standalone functions from sling-buildscripts
+
+set_build_number_to_revision()
+{
+    record_event "buildNumber $revision"
+}
+
+record_event()
+{
+    echo "=== $@"
+}
+
+begin_section()
+{
+    record_event "START $*"
+    sections+=("$*")
+}
+
+end_section()
+{
+    # accommodate dumb Mac bash 3, which doesn't understand array[-1]
+    local last=$(( ${#sections[@]} - 1 ))
+    record_event "END ${*:-${sections[$last]}}"
+    unset "sections[$last]"
+}
+
+record_success()
+{
+    record_event "SUCCESS $*"
+}
+
+record_failure()
+{
+    record_event "FAILURE $*" >&2
+}
+
+fatal()
+{
+    record_failure "$@"
+    finalize false
+    exit 1
+}
+
+# redefined fail for backward compatibility
+alias fail=fatal
+
+pass()
+{
+  exit 0
+}
+
+export -f set_build_number_to_revision
+export -f record_event
+export -f begin_section
+export -f end_section
+export -f record_success
+export -f record_failure
+export -f fatal
+export -f pass
+export sections
diff --git a/indra/lib/python/indra/util/llmanifest.py b/indra/lib/python/indra/util/llmanifest.py
index cb6fd0365ccfb02c1f16e91dfc2f9292251bde94..24e47e01af7b48fdcecc1080d64a4c24e5e46672 100755
--- a/indra/lib/python/indra/util/llmanifest.py
+++ b/indra/lib/python/indra/util/llmanifest.py
@@ -40,6 +40,7 @@
 import operator
 import os
 import re
+import shlex
 import shutil
 import subprocess
 import sys
@@ -539,15 +540,15 @@ def ensure_dst_dir(self, reldir):
         self.cmakedirs(path)
         return path
 
-    def run_command(self, command):
+    def run_command(self, command, **kwds):
         """ 
         Runs an external command.  
         Raises ManifestError exception if the command returns a nonzero status.
         """
-        print("Running command:", command)
+        print("Running command:", shlex.join(command))
         sys.stdout.flush()
         try:
-            subprocess.check_call(command)
+            subprocess.check_call(command, **kwds)
         except subprocess.CalledProcessError as err:
             raise ManifestError( "Command %s returned non-zero status (%s)"
                                 % (command, err.returncode) )
diff --git a/indra/llaudio/llaudioengine.h b/indra/llaudio/llaudioengine.h
index 0e20de469ab5fa724d3ea549694303fb9f36c64c..d1c3dab9b3f1afc34f4b40281a8de26b9a634777 100644
--- a/indra/llaudio/llaudioengine.h
+++ b/indra/llaudio/llaudioengine.h
@@ -31,6 +31,7 @@
 #include <array>
 #include <list>
 #include <map>
+#include <array>
 
 #include "v3math.h"
 #include "v3dmath.h"
diff --git a/indra/llaudio/llaudioengine_openal.h b/indra/llaudio/llaudioengine_openal.h
index a215a466cbfcab3fad768e1a8a2e534551e248fe..12666f892b8ff9bac91479a35908c88bcb4a2347 100644
--- a/indra/llaudio/llaudioengine_openal.h
+++ b/indra/llaudio/llaudioengine_openal.h
@@ -42,6 +42,7 @@ class LLAudioEngine_OpenAL final : public LLAudioEngine
 
         virtual bool init(void *user_data, const std::string &app_title);
         virtual std::string getDriverName(bool verbose);
+        virtual LLStreamingAudioInterface* createDefaultStreamingAudioImpl() const { return nullptr; }
 		virtual void allocateListener();
 
 		virtual void shutdown();
@@ -54,7 +55,6 @@ class LLAudioEngine_OpenAL final : public LLAudioEngine
 		/*virtual*/ bool initWind();
 		/*virtual*/ void cleanupWind();
 		/*virtual*/ void updateWind(LLVector3 direction, F32 camera_altitude);
-		/*virtual*/ LLStreamingAudioInterface* createDefaultStreamingAudioImpl() const { return nullptr; }
 
 	private:
         typedef S16 WIND_SAMPLE_T;
diff --git a/indra/llcommon/llcoros.cpp b/indra/llcommon/llcoros.cpp
index 3f100b6d95637bb9fcaf896b6ad1915f07489fd9..e936646a956446654ebe7376a3d252c2a42bb396 100644
--- a/indra/llcommon/llcoros.cpp
+++ b/indra/llcommon/llcoros.cpp
@@ -123,11 +123,7 @@ LLCoros::LLCoros():
     // Previously we used
     // boost::context::guarded_stack_allocator::default_stacksize();
     // empirically this is insufficient.
-#if ADDRESS_SIZE == 64
-    mStackSize(512*1024),
-#else
-    mStackSize(256*1024),
-#endif
+    mStackSize(768*1024),
     // mCurrent does NOT own the current CoroData instance -- it simply
     // points to it. So initialize it with a no-op deleter.
     mCurrent{ [](CoroData*){} }
diff --git a/indra/llcommon/llerror.cpp b/indra/llcommon/llerror.cpp
index 51f8112c620506b9c3d63c35a711dfafe1462201..fb2e6d6ef120a974d592257507058c870daa7828 100644
--- a/indra/llcommon/llerror.cpp
+++ b/indra/llcommon/llerror.cpp
@@ -1615,5 +1615,18 @@ namespace LLError
     }
 }
 
-
-
+void crashdriver(void (*callback)(int*))
+{
+    // The LLERROR_CRASH macro used to have inline code of the form:
+    //int* make_me_crash = NULL;
+    //*make_me_crash = 0;
+
+    // But compilers are getting smart enough to recognize that, so we must
+    // assign to an address supplied by a separate source file. We could do
+    // the assignment here in crashdriver() -- but then BugSplat would group
+    // all LL_ERRS() crashes as the fault of this one function, instead of
+    // identifying the specific LL_ERRS() source line. So instead, do the
+    // assignment in a lambda in the caller's source. We just provide the
+    // nullptr target.
+    callback(nullptr);
+}
diff --git a/indra/llcommon/llerror.h b/indra/llcommon/llerror.h
index 4342d2a695e9ad5b63b677311facc76ba04f02f6..68963e84eb1c87180b2430da8e872723db031b3e 100644
--- a/indra/llcommon/llerror.h
+++ b/indra/llcommon/llerror.h
@@ -425,12 +425,9 @@ typedef LLError::NoClassInfo _LL_CLASS_TO_LOG;
 #define LL_NEWLINE '\n'
 
 // Use this only in LL_ERRS or in a place that LL_ERRS may not be used
-#define LLERROR_CRASH               \
-{                                   \
-    int* make_me_crash = NULL;      \
-    /* coverity[var_deref_op] */    \
-    *make_me_crash = 0;             \
-    exit(*make_me_crash);           \
+#define LLERROR_CRASH                                   \
+{                                                       \
+    crashdriver([](int* ptr){ *ptr = 0; exit(*ptr); }); \
 }
 
 #define LL_ENDL                                         \
@@ -507,6 +504,9 @@ typedef LLError::NoClassInfo _LL_CLASS_TO_LOG;
 #define LL_VLOGS(level, ...)      llvlog(level, false, ##__VA_ARGS__)
 #define LL_VLOGS_ONCE(level, ...) llvlog(level, true,  ##__VA_ARGS__)
 
+// used by LLERROR_CRASH
+void crashdriver(void (*)(int*));
+
 /*
 // Check at run-time whether logging is enabled, without generating output.
 Resist the temptation to add a function like this because it incurs the
diff --git a/indra/llcommon/llleap.cpp b/indra/llcommon/llleap.cpp
index c9886fbd6beed5d3c1dcec56f90d7dff20243c6f..051384f342919801d3f9910b9cf63cc0e3f1c0f2 100644
--- a/indra/llcommon/llleap.cpp
+++ b/indra/llcommon/llleap.cpp
@@ -381,6 +381,17 @@ class LLLeapImpl: public LLLeap
             // Read all remaining bytes and log.
             LL_INFOS("LLLeap") << mDesc << ": " << rest << LL_ENDL;
         }
+        /*--------------------------- diagnostic ---------------------------*/
+        else if (data["eof"].asBoolean())
+        {
+            LL_DEBUGS("LLLeap") << mDesc << " ended, no partial line" << LL_ENDL;
+        }
+        else
+        {
+            LL_DEBUGS("LLLeap") << mDesc << " (still running, " << childerr.size()
+                                << " bytes pending)" << LL_ENDL;
+        }
+        /*------------------------- end diagnostic -------------------------*/
         return false;
     }
 
diff --git a/indra/llcommon/llprocessor.cpp b/indra/llcommon/llprocessor.cpp
index 78d833817cd6f74ef4705556d584511f28711442..6942655025c56e6aef7c3456e284ffeecaa9d1ba 100644
--- a/indra/llcommon/llprocessor.cpp
+++ b/indra/llcommon/llprocessor.cpp
@@ -772,7 +772,7 @@ class LLProcessorInfoDarwinImpl : public LLProcessorInfoImpl
 		__cpuid(0x1, eax, ebx, ecx, edx);
 		if(feature_infos[0] != (S32)edx)
 		{
-			LL_ERRS() << "machdep.cpu.feature_bits doesn't match expected cpuid result!" << LL_ENDL;
+			LL_WARNS() << "machdep.cpu.feature_bits doesn't match expected cpuid result!" << LL_ENDL;
 		} 
 #endif // LL_RELEASE_FOR_DOWNLOAD 	
 
diff --git a/indra/llcommon/tests/llleap_test.cpp b/indra/llcommon/tests/llleap_test.cpp
index 3ae48a25328eea1bc44d58e62fe0871d184925c2..7197dedfbf27aa7b019e451341005831296a03e7 100644
--- a/indra/llcommon/tests/llleap_test.cpp
+++ b/indra/llcommon/tests/llleap_test.cpp
@@ -17,8 +17,6 @@
 // std headers
 #include <functional>
 // external library headers
-#include <boost/assign/list_of.hpp>
-#include <boost/phoenix/core/argument.hpp>
 // other Linden headers
 #include "../test/lltut.h"
 #include "../test/namedtempfile.h"
@@ -30,10 +28,6 @@
 #include "stringize.h"
 #include "StringVec.h"
 
-using boost::assign::list_of;
-
-StringVec sv(const StringVec& listof) { return listof; }
-
 #if defined(LL_WINDOWS)
 #define sleep(secs) _sleep((secs) * 1000)
 
@@ -104,17 +98,12 @@ namespace tut
         llleap_data():
             reader(".py",
                    // This logic is adapted from vita.viewerclient.receiveEvent()
-                   boost::phoenix::placeholders::arg1 <<
+                   [](std::ostream& out){ out <<
                    "import re\n"
                    "import os\n"
                    "import sys\n"
                    "\n"
-                   "try:\n"
-                   // new freestanding llsd package
-                   "    import llsd\n"
-                   "except ImportError:\n"
-                   // older llbase.llsd module
-                   "    from llbase import llsd\n"
+                   "import llsd\n"
                    "\n"
                    "class ProtocolError(Exception):\n"
                    "    def __init__(self, msg, data):\n"
@@ -193,7 +182,7 @@ namespace tut
                    "def request(pump, data):\n"
                    "    # we expect 'data' is a dict\n"
                    "    data['reply'] = _reply\n"
-                   "    send(pump, data)\n"),
+                   "    send(pump, data)\n";}),
             // Get the actual pathname of the NamedExtTempFile and trim off
             // the ".py" extension. (We could cache reader.getName() in a
             // separate member variable, but I happen to know getName() just
@@ -218,14 +207,14 @@ namespace tut
     void object::test<1>()
     {
         set_test_name("multiple LLLeap instances");
-        NamedTempFile script("py",
-                             "import time\n"
-                             "time.sleep(1)\n");
+        NamedExtTempFile script("py",
+                                "import time\n"
+                                "time.sleep(1)\n");
         LLLeapVector instances;
         instances.push_back(LLLeap::create(get_test_name(),
-                                           sv(list_of(PYTHON)(script.getName())))->getWeak());
+                                           StringVec{PYTHON, script.getName()})->getWeak());
         instances.push_back(LLLeap::create(get_test_name(),
-                                           sv(list_of(PYTHON)(script.getName())))->getWeak());
+                                           StringVec{PYTHON, script.getName()})->getWeak());
         // In this case we're simply establishing that two LLLeap instances
         // can coexist without throwing exceptions or bombing in any other
         // way. Wait for them to terminate.
@@ -236,10 +225,10 @@ namespace tut
     void object::test<2>()
     {
         set_test_name("stderr to log");
-        NamedTempFile script("py",
-                             "import sys\n"
-                             "sys.stderr.write('''Hello from Python!\n"
-                             "note partial line''')\n");
+        NamedExtTempFile script("py",
+                                "import sys\n"
+                                "sys.stderr.write('''Hello from Python!\n"
+                                "note partial line''')\n");
         StringVec vcommand{ PYTHON, script.getName() };
         CaptureLog log(LLError::LEVEL_INFO);
         waitfor(LLLeap::create(get_test_name(), vcommand));
@@ -251,11 +240,11 @@ namespace tut
     void object::test<3>()
     {
         set_test_name("bad stdout protocol");
-        NamedTempFile script("py",
-                             "print('Hello from Python!')\n");
+        NamedExtTempFile script("py",
+                                "print('Hello from Python!')\n");
         CaptureLog log(LLError::LEVEL_WARN);
         waitfor(LLLeap::create(get_test_name(),
-                               sv(list_of(PYTHON)(script.getName()))));
+                               StringVec{PYTHON, script.getName()}));
         ensure_contains("error log line",
                         log.messageWith("invalid protocol"), "Hello from Python!");
     }
@@ -264,13 +253,13 @@ namespace tut
     void object::test<4>()
     {
         set_test_name("leftover stdout");
-        NamedTempFile script("py",
-                             "import sys\n"
-                             // note lack of newline
-                             "sys.stdout.write('Hello from Python!')\n");
+        NamedExtTempFile script("py",
+                                "import sys\n"
+                                // note lack of newline
+                                "sys.stdout.write('Hello from Python!')\n");
         CaptureLog log(LLError::LEVEL_WARN);
         waitfor(LLLeap::create(get_test_name(),
-                               sv(list_of(PYTHON)(script.getName()))));
+                               StringVec{PYTHON, script.getName()}));
         ensure_contains("error log line",
                         log.messageWith("Discarding"), "Hello from Python!");
     }
@@ -279,12 +268,12 @@ namespace tut
     void object::test<5>()
     {
         set_test_name("bad stdout len prefix");
-        NamedTempFile script("py",
-                             "import sys\n"
-                             "sys.stdout.write('5a2:something')\n");
+        NamedExtTempFile script("py",
+                                "import sys\n"
+                                "sys.stdout.write('5a2:something')\n");
         CaptureLog log(LLError::LEVEL_WARN);
         waitfor(LLLeap::create(get_test_name(),
-                               sv(list_of(PYTHON)(script.getName()))));
+                               StringVec{PYTHON, script.getName()}));
         ensure_contains("error log line",
                         log.messageWith("invalid protocol"), "5a2:");
     }
@@ -386,17 +375,18 @@ namespace tut
         set_test_name("round trip");
         AckAPI api;
         Result result;
-        NamedTempFile script("py",
-                             boost::phoenix::placeholders::arg1 <<
-                             "from " << reader_module << " import *\n"
-                             // make a request on our little API
-                             "request(pump='" << api.getName() << "', data={})\n"
-                             // wait for its response
-                             "resp = get()\n"
-                             "result = '' if resp == dict(pump=replypump(), data='ack')\\\n"
-                             "            else 'bad: ' + str(resp)\n"
-                             "send(pump='" << result.getName() << "', data=result)\n");
-        waitfor(LLLeap::create(get_test_name(), sv(list_of(PYTHON)(script.getName()))));
+        NamedExtTempFile script("py",
+                                [&](std::ostream& out){ out <<
+                                "from " << reader_module << " import *\n"
+                                // make a request on our little API
+                                "request(pump='" << api.getName() << "', data={})\n"
+                                // wait for its response
+                                "resp = get()\n"
+                                "result = '' if resp == dict(pump=replypump(), data='ack')\\\n"
+                                "            else 'bad: ' + str(resp)\n"
+                                "send(pump='" << result.getName() << "', data=result)\n";});
+        waitfor(LLLeap::create(get_test_name(),
+                               StringVec{PYTHON, script.getName()}));
         result.ensure();
     }
 
@@ -424,38 +414,38 @@ namespace tut
         // iterations etc. in OS pipes and the LLLeap/LLProcess implementation.
         ReqIDAPI api;
         Result result;
-        NamedTempFile script("py",
-                             boost::phoenix::placeholders::arg1 <<
-                             "import sys\n"
-                             "from " << reader_module << " import *\n"
-                             // Note that since reader imports llsd, this
-                             // 'import *' gets us llsd too.
-                             "sample = llsd.format_notation(dict(pump='" <<
-                             api.getName() << "', data=dict(reqid=999999, reply=replypump())))\n"
-                             // The whole packet has length prefix too: "len:data"
-                             "samplen = len(str(len(sample))) + 1 + len(sample)\n"
-                             // guess how many messages it will take to
-                             // accumulate BUFFERED_LENGTH
-                             "count = int(" << BUFFERED_LENGTH << "/samplen)\n"
-                             "print('Sending %s requests' % count, file=sys.stderr)\n"
-                             "for i in range(count):\n"
-                             "    request('" << api.getName() << "', dict(reqid=i))\n"
-                             // The assumption in this specific test that
-                             // replies will arrive in the same order as
-                             // requests is ONLY valid because the API we're
-                             // invoking sends replies instantly. If the API
-                             // had to wait for some external event before
-                             // sending its reply, replies could arrive in
-                             // arbitrary order, and we'd have to tick them
-                             // off from a set.
-                             "result = ''\n"
-                             "for i in range(count):\n"
-                             "    resp = get()\n"
-                             "    if resp['data']['reqid'] != i:\n"
-                             "        result = 'expected reqid=%s in %s' % (i, resp)\n"
-                             "        break\n"
-                             "send(pump='" << result.getName() << "', data=result)\n");
-        waitfor(LLLeap::create(get_test_name(), sv(list_of(PYTHON)(script.getName()))),
+        NamedExtTempFile script("py",
+                                [&](std::ostream& out){ out <<
+                                "import sys\n"
+                                "from " << reader_module << " import *\n"
+                                // Note that since reader imports llsd, this
+                                // 'import *' gets us llsd too.
+                                "sample = llsd.format_notation(dict(pump='" <<
+                                api.getName() << "', data=dict(reqid=999999, reply=replypump())))\n"
+                                // The whole packet has length prefix too: "len:data"
+                                "samplen = len(str(len(sample))) + 1 + len(sample)\n"
+                                // guess how many messages it will take to
+                                // accumulate BUFFERED_LENGTH
+                                "count = int(" << BUFFERED_LENGTH << "/samplen)\n"
+                                "print('Sending %s requests' % count, file=sys.stderr)\n"
+                                "for i in range(count):\n"
+                                "    request('" << api.getName() << "', dict(reqid=i))\n"
+                                // The assumption in this specific test that
+                                // replies will arrive in the same order as
+                                // requests is ONLY valid because the API we're
+                                // invoking sends replies instantly. If the API
+                                // had to wait for some external event before
+                                // sending its reply, replies could arrive in
+                                // arbitrary order, and we'd have to tick them
+                                // off from a set.
+                                "result = ''\n"
+                                "for i in range(count):\n"
+                                "    resp = get()\n"
+                                "    if resp['data']['reqid'] != i:\n"
+                                "        result = 'expected reqid=%s in %s' % (i, resp)\n"
+                                "        break\n"
+                                "send(pump='" << result.getName() << "', data=result)\n";});
+        waitfor(LLLeap::create(get_test_name(), StringVec{PYTHON, script.getName()}),
                 300);               // needs more realtime than most tests
         result.ensure();
     }
@@ -467,65 +457,62 @@ namespace tut
     {
         ReqIDAPI api;
         Result result;
-        NamedTempFile script("py",
-                             boost::phoenix::placeholders::arg1 <<
-                             "import sys\n"
-                             "from " << reader_module << " import *\n"
-                             // Generate a very large string value.
-                             "desired = int(sys.argv[1])\n"
-                             // 7 chars per item: 6 digits, 1 comma
-                             "count = int((desired - 50)/7)\n"
-                             "large = ''.join('%06d,' % i for i in range(count))\n"
-                             // Pass 'large' as reqid because we know the API
-                             // will echo reqid, and we want to receive it back.
-                             "request('" << api.getName() << "', dict(reqid=large))\n"
-                             "try:\n"
-                             "    resp = get()\n"
-                             "except ParseError as e:\n"
-                             "    # try to find where e.data diverges from expectation\n"
-                             // Normally we'd expect a 'pump' key in there,
-                             // too, with value replypump(). But Python
-                             // serializes keys in a different order than C++,
-                             // so incoming data start with 'data'.
-                             // Truthfully, though, if we get as far as 'pump'
-                             // before we find a difference, something's very
-                             // strange.
-                             "    expect = llsd.format_notation(dict(data=dict(reqid=large)))\n"
-                             "    chunk = 40\n"
-                             "    for offset in range(0, max(len(e.data), len(expect)), chunk):\n"
-                             "        if e.data[offset:offset+chunk] != \\\n"
-                             "           expect[offset:offset+chunk]:\n"
-                             "            print('Offset %06d: expect %r,\\n'\\\n"
-                             "                                '                  get %r' %\\\n"
-                             "                                (offset,\n"
-                             "                                 expect[offset:offset+chunk],\n"
-                             "                                 e.data[offset:offset+chunk]),\n"
-                             "                                 file=sys.stderr)\n"
-                             "            break\n"
-                             "    else:\n"
-                             "        print('incoming data matches expect?!', file=sys.stderr)\n"
-                             "    send('" << result.getName() << "', '%s: %s' % (e.__class__.__name__, e))\n"
-                             "    sys.exit(1)\n"
-                             "\n"
-                             "echoed = resp['data']['reqid']\n"
-                             "if echoed == large:\n"
-                             "    send('" << result.getName() << "', '')\n"
-                             "    sys.exit(0)\n"
-                             // Here we know echoed did NOT match; try to find where
-                             "for i in range(count):\n"
-                             "    start = 7*i\n"
-                             "    end   = 7*(i+1)\n"
-                             "    if end > len(echoed)\\\n"
-                             "    or echoed[start:end] != large[start:end]:\n"
-                             "        send('" << result.getName() << "',\n"
-                             "             'at offset %s, expected %r but got %r' %\n"
-                             "             (start, large[start:end], echoed[start:end]))\n"
-                             "sys.exit(1)\n");
+        NamedExtTempFile script("py",
+                                [&](std::ostream& out){ out <<
+                                "import sys\n"
+                                "from " << reader_module << " import *\n"
+                                // Generate a very large string value.
+                                "desired = int(sys.argv[1])\n"
+                                // 7 chars per item: 6 digits, 1 comma
+                                "count = int((desired - 50)/7)\n"
+                                "large = ''.join('%06d,' % i for i in range(count))\n"
+                                // Pass 'large' as reqid because we know the API
+                                // will echo reqid, and we want to receive it back.
+                                "request('" << api.getName() << "', dict(reqid=large))\n"
+                                "try:\n"
+                                "    resp = get()\n"
+                                "except ParseError as e:\n"
+                                "    # try to find where e.data diverges from expectation\n"
+                                // Normally we'd expect a 'pump' key in there,
+                                // too, with value replypump(). But Python
+                                // serializes keys in a different order than C++,
+                                // so incoming data start with 'data'.
+                                // Truthfully, though, if we get as far as 'pump'
+                                // before we find a difference, something's very
+                                // strange.
+                                "    expect = llsd.format_notation(dict(data=dict(reqid=large)))\n"
+                                "    chunk = 40\n"
+                                "    for offset in range(0, max(len(e.data), len(expect)), chunk):\n"
+                                "        if e.data[offset:offset+chunk] != \\\n"
+                                "           expect[offset:offset+chunk]:\n"
+                                "            print('Offset %06d: expect %r,\\n'\\\n"
+                                "                                '                  get %r' %\\\n"
+                                "                                (offset,\n"
+                                "                                 expect[offset:offset+chunk],\n"
+                                "                                 e.data[offset:offset+chunk]),\n"
+                                "                                 file=sys.stderr)\n"
+                                "            break\n"
+                                "    else:\n"
+                                "        print('incoming data matches expect?!', file=sys.stderr)\n"
+                                "    send('" << result.getName() << "', '%s: %s' % (e.__class__.__name__, e))\n"
+                                "    sys.exit(1)\n"
+                                "\n"
+                                "echoed = resp['data']['reqid']\n"
+                                "if echoed == large:\n"
+                                "    send('" << result.getName() << "', '')\n"
+                                "    sys.exit(0)\n"
+                                // Here we know echoed did NOT match; try to find where
+                                "for i in range(count):\n"
+                                "    start = 7*i\n"
+                                "    end   = 7*(i+1)\n"
+                                "    if end > len(echoed)\\\n"
+                                "    or echoed[start:end] != large[start:end]:\n"
+                                "        send('" << result.getName() << "',\n"
+                                "             'at offset %s, expected %r but got %r' %\n"
+                                "             (start, large[start:end], echoed[start:end]))\n"
+                                "sys.exit(1)\n";});
         waitfor(LLLeap::create(test_name,
-                               sv(list_of
-                                  (PYTHON)
-                                  (script.getName())
-                                  (stringize(size)))),
+                               StringVec{PYTHON, script.getName(), stringize(size)}),
                 180);               // try a longer timeout
         result.ensure();
     }
diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp
index 9056a76d062709d54017f7dd540fd65f2b16888a..1281fbe7d50953aff4ed081d82bda70187e77ee2 100644
--- a/indra/llcommon/tests/llprocess_test.cpp
+++ b/indra/llcommon/tests/llprocess_test.cpp
@@ -150,8 +150,38 @@ struct PythonProcessLauncher
     /// Launch Python script; verify that it launched
     void launch()
     {
-        mPy = LLProcess::create(mParams);
-        tut::ensure(STRINGIZE("Couldn't launch " << mDesc << " script"), bool(mPy));
+        try
+        {
+            mPy = LLProcess::create(mParams);
+            tut::ensure(STRINGIZE("Couldn't launch " << mDesc << " script"), bool(mPy));
+        }
+        catch (const tut::failure&)
+        {
+            // On Windows, if APR_LOG is set, our version of APR's
+            // apr_create_proc() logs to the specified file. If this test
+            // failed, try to report that log.
+            const char* APR_LOG = getenv("APR_LOG");
+            if (APR_LOG && *APR_LOG)
+            {
+                std::ifstream inf(APR_LOG);
+                if (! inf.is_open())
+                {
+                    LL_WARNS() << "Couldn't open '" << APR_LOG << "'" << LL_ENDL;
+                }
+                else
+                {
+                    LL_WARNS() << "==============================" << LL_ENDL;
+                    LL_WARNS() << "From '" << APR_LOG << "':" << LL_ENDL;
+                    std::string line;
+                    while (std::getline(inf, line))
+                    {
+                        LL_WARNS() << line << LL_ENDL;
+                    }
+                    LL_WARNS() << "==============================" << LL_ENDL;
+                }
+            }
+            throw;
+        }
     }
 
     /// Run Python script and wait for it to complete.
@@ -190,7 +220,7 @@ struct PythonProcessLauncher
     LLProcess::Params mParams;
     LLProcessPtr mPy;
     std::string mDesc;
-    NamedTempFile mScript;
+    NamedExtTempFile mScript;
 };
 
 /// convenience function for PythonProcessLauncher::run()
@@ -213,30 +243,26 @@ static std::string python_out(const std::string& desc, const CONTENT& script)
 class NamedTempDir: public boost::noncopyable
 {
 public:
-    // Use python() function to create a temp directory: I've found
-    // nothing in either Boost.Filesystem or APR quite like Python's
-    // tempfile.mkdtemp().
-    // Special extra bonus: on Mac, mkdtemp() reports a pathname
-    // starting with /var/folders/something, whereas that's really a
-    // symlink to /private/var/folders/something. Have to use
-    // realpath() to compare properly.
     NamedTempDir():
-        mPath(python_out("mkdtemp()",
-                         "from __future__ import with_statement\n"
-                         "import os.path, sys, tempfile\n"
-                         "with open(sys.argv[1], 'w') as f:\n"
-                         "    f.write(os.path.normcase(os.path.normpath(os.path.realpath(tempfile.mkdtemp()))))\n"))
-    {}
+        mPath(NamedTempFile::temp_path()),
+        mCreated(boost::filesystem::create_directories(mPath))
+    {
+        mPath = boost::filesystem::canonical(mPath);
+    }
 
     ~NamedTempDir()
     {
-        aprchk(apr_dir_remove(mPath.c_str(), gAPRPoolp));
+        if (mCreated)
+        {
+            boost::filesystem::remove_all(mPath);
+        }
     }
 
-    std::string getName() const { return mPath; }
+    std::string getName() const { return mPath.string(); }
 
 private:
-    std::string mPath;
+    boost::filesystem::path mPath;
+    bool mCreated;
 };
 
 /*****************************************************************************
@@ -354,7 +380,7 @@ namespace tut
         set_test_name("raw APR nonblocking I/O");
 
         // Create a script file in a temporary place.
-        NamedTempFile script("py",
+        NamedExtTempFile script("py",
             "from __future__ import print_function" EOL
             "import sys" EOL
             "import time" EOL
@@ -562,7 +588,13 @@ namespace tut
                                  "    f.write(os.path.normcase(os.path.normpath(os.getcwd())))\n");
         // Before running, call setWorkingDirectory()
         py.mParams.cwd = tempdir.getName();
-        ensure_equals("os.getcwd()", py.run_read(), tempdir.getName());
+        std::string expected{ tempdir.getName() };
+#if LL_WINDOWS
+        // SIGH, don't get tripped up by "C:" != "c:" --
+        // but on the Mac, using tolower() fails because "/users" != "/Users"!
+        expected = utf8str_tolower(expected);
+#endif
+        ensure_equals("os.getcwd()", py.run_read(), expected);
     }
 
     template<> template<>
diff --git a/indra/llcommon/tests/llrand_test.cpp b/indra/llcommon/tests/llrand_test.cpp
index 383e6f9e0a926fdd04fa68d3a76848b2aa0a9aa6..ac5a33d0ba0a9049eb5f7d44c3767ba6837c8dc0 100644
--- a/indra/llcommon/tests/llrand_test.cpp
+++ b/indra/llcommon/tests/llrand_test.cpp
@@ -29,7 +29,23 @@
 #include "../test/lltut.h"
 
 #include "../llrand.h"
+#include "stringize.h"
 
+// In llrand.h, every function is documented to return less than the high end
+// -- specifically, because you can pass a negative extent, they're documented
+// never to return a value equal to the extent.
+// So that we don't need two different versions of ensure_in_range(), when
+// testing extent < 0, negate the return value and the extent before passing
+// into ensure_in_range().
+template <typename NUMBER>
+void ensure_in_range(const std::string_view& name,
+                     NUMBER value, NUMBER low, NUMBER high)
+{
+    auto failmsg{ stringize(name, " >= ", low, " (", value, ')') };
+    tut::ensure(failmsg, (value >= low));
+    failmsg = stringize(name, " < ", high, " (", value, ')');
+    tut::ensure(failmsg, (value < high));
+}
 
 namespace tut
 {
@@ -44,84 +60,65 @@ namespace tut
 	template<> template<>
 	void random_object_t::test<1>()
 	{
-		F32 number = 0.0f;
 		for(S32 ii = 0; ii < 100000; ++ii)
 		{
-			number = ll_frand();
-			ensure("frand >= 0", (number >= 0.0f));
-			ensure("frand < 1", (number < 1.0f));
+			ensure_in_range("frand", ll_frand(), 0.0f, 1.0f);
 		}
 	}
 
 	template<> template<>
 	void random_object_t::test<2>()
 	{
-		F64 number = 0.0f;
 		for(S32 ii = 0; ii < 100000; ++ii)
 		{
-			number = ll_drand();
-			ensure("drand >= 0", (number >= 0.0));
-			ensure("drand < 1", (number < 1.0));
+			ensure_in_range("drand", ll_drand(), 0.0, 1.0);
 		}
 	}
 
 	template<> template<>
 	void random_object_t::test<3>()
 	{
-		F32 number = 0.0f;
 		for(S32 ii = 0; ii < 100000; ++ii)
 		{
-			number = ll_frand(2.0f) - 1.0f;
-			ensure("frand >= 0", (number >= -1.0f));
-			ensure("frand < 1", (number <= 1.0f));
+			ensure_in_range("frand(2.0f)", ll_frand(2.0f) - 1.0f, -1.0f, 1.0f);
 		}
 	}
 
 	template<> template<>
 	void random_object_t::test<4>()
 	{
-		F32 number = 0.0f;
 		for(S32 ii = 0; ii < 100000; ++ii)
 		{
-			number = ll_frand(-7.0);
-			ensure("drand <= 0", (number <= 0.0));
-			ensure("drand > -7", (number > -7.0));
+			// Negate the result so we don't have to allow a templated low-end
+			// comparison as well.
+			ensure_in_range("-frand(-7.0)", -ll_frand(-7.0), 0.0f, 7.0f);
 		}
 	}
 
 	template<> template<>
 	void random_object_t::test<5>()
 	{
-		F64 number = 0.0f;
 		for(S32 ii = 0; ii < 100000; ++ii)
 		{
-			number = ll_drand(-2.0);
-			ensure("drand <= 0", (number <= 0.0));
-			ensure("drand > -2", (number > -2.0));
+			ensure_in_range("-drand(-2.0)", -ll_drand(-2.0), 0.0, 2.0);
 		}
 	}
 
 	template<> template<>
 	void random_object_t::test<6>()
 	{
-		S32 number = 0;
 		for(S32 ii = 0; ii < 100000; ++ii)
 		{
-			number = ll_rand(100);
-			ensure("rand >= 0", (number >= 0));
-			ensure("rand < 100", (number < 100));
+			ensure_in_range("rand(100)", ll_rand(100), 0, 100);
 		}
 	}
 
 	template<> template<>
 	void random_object_t::test<7>()
 	{
-		S32 number = 0;
 		for(S32 ii = 0; ii < 100000; ++ii)
 		{
-			number = ll_rand(-127);
-			ensure("rand <= 0", (number <= 0));
-			ensure("rand > -127", (number > -127));
+			ensure_in_range("-rand(-127)", -ll_rand(-127), 0, 127);
 		}
 	}
 }
diff --git a/indra/llcommon/tests/llsdserialize_test.cpp b/indra/llcommon/tests/llsdserialize_test.cpp
index 042536cae100ee04474d89219b3851898545db31..ac40125f752df2a4af8d734b8a5db22cb6f56b42 100644
--- a/indra/llcommon/tests/llsdserialize_test.cpp
+++ b/indra/llcommon/tests/llsdserialize_test.cpp
@@ -45,10 +45,6 @@ typedef U32 uint32_t;
 #endif
 
 #include "boost/range.hpp"
-#include "boost/bind.hpp"
-#include "boost/phoenix/bind/bind_function.hpp"
-#include "boost/phoenix/core/argument.hpp"
-using namespace boost::phoenix;
 
 #include "llsd.h"
 #include "llsdserialize.h"
@@ -56,9 +52,11 @@ using namespace boost::phoenix;
 #include "llformat.h"
 #include "llmemorystream.h"
 
+#include "../test/hexdump.h"
 #include "../test/lltut.h"
 #include "../test/namedtempfile.h"
 #include "stringize.h"
+#include "StringVec.h"
 #include <functional>
 
 typedef std::function<void(const LLSD& data, std::ostream& str)> FormatterFunction;
@@ -1795,16 +1793,12 @@ namespace tut
     // helper for TestPythonCompatible
     static std::string import_llsd("import os.path\n"
                                    "import sys\n"
-                                   "try:\n"
-                                   // new freestanding llsd package
-                                   "    import llsd\n"
-                                   "except ImportError:\n"
-                                   // older llbase.llsd module
-                                   "    from llbase import llsd\n");
+                                   "import llsd\n");
 
     // helper for TestPythonCompatible
-    template <typename CONTENT>
-    void python(const std::string& desc, const CONTENT& script, int expect=0)
+    template <typename CONTENT, typename... ARGS>
+    void python_expect(const std::string& desc, const CONTENT& script, int expect=0,
+                       ARGS&&... args)
     {
         auto PYTHON(LLStringUtil::getenv("PYTHON"));
         ensure("Set $PYTHON to the Python interpreter", !PYTHON.empty());
@@ -1815,7 +1809,8 @@ namespace tut
         std::string q("\"");
         std::string qPYTHON(q + PYTHON + q);
         std::string qscript(q + scriptfile.getName() + q);
-        int rc = _spawnl(_P_WAIT, PYTHON.c_str(), qPYTHON.c_str(), qscript.c_str(), NULL);
+        int rc = _spawnl(_P_WAIT, PYTHON.c_str(), qPYTHON.c_str(), qscript.c_str(),
+                         std::forward<ARGS>(args)..., NULL);
         if (rc == -1)
         {
             char buffer[256];
@@ -1831,6 +1826,10 @@ namespace tut
         LLProcess::Params params;
         params.executable = PYTHON;
         params.args.add(scriptfile.getName());
+        for (const std::string& arg : StringVec{ std::forward<ARGS>(args)... })
+        {
+            params.args.add(arg);
+        }
         LLProcessPtr py(LLProcess::create(params));
         ensure(STRINGIZE("Couldn't launch " << desc << " script"), bool(py));
         // Implementing timeout would mean messing with alarm() and
@@ -1865,6 +1864,14 @@ namespace tut
 #endif
     }
 
+    // helper for TestPythonCompatible
+    template <typename CONTENT, typename... ARGS>
+    void python(const std::string& desc, const CONTENT& script, ARGS&&... args)
+    {
+        // plain python() expects rc 0
+        python_expect(desc, script, 0, std::forward<ARGS>(args)...);
+    }
+
     struct TestPythonCompatible
     {
         TestPythonCompatible() {}
@@ -1879,10 +1886,10 @@ namespace tut
     void TestPythonCompatibleObject::test<1>()
     {
         set_test_name("verify python()");
-        python("hello",
-               "import sys\n"
-               "sys.exit(17)\n",
-               17);                 // expect nonzero rc
+        python_expect("hello",
+                      "import sys\n"
+                      "sys.exit(17)\n",
+                      17);                 // expect nonzero rc
     }
 
     template<> template<>
@@ -1898,7 +1905,7 @@ namespace tut
     static void writeLLSDArray(const FormatterFunction& serialize,
                                std::ostream& out, const LLSD& array)
     {
-        for (const LLSD& item : llsd::inArray(array))
+        for (const LLSD& item: llsd::inArray(array))
         {
             // It's important to delimit the entries in this file somehow
             // because, although Python's llsd.parse() can accept a file
@@ -1913,7 +1920,14 @@ namespace tut
             auto buffstr{ buffer.str() };
             int bufflen{ static_cast<int>(buffstr.length()) };
             out.write(reinterpret_cast<const char*>(&bufflen), sizeof(bufflen));
+            LL_DEBUGS() << "Wrote length: "
+                        << hexdump(reinterpret_cast<const char*>(&bufflen),
+                                   sizeof(bufflen))
+                        << LL_ENDL;
             out.write(buffstr.c_str(), buffstr.length());
+            LL_DEBUGS() << "Wrote data:   "
+                        << hexmix(buffstr.c_str(), buffstr.length())
+                        << LL_ENDL;
         }
     }
 
@@ -1942,10 +1956,10 @@ namespace tut
             "    else:\n"
             "        raise AssertionError('Too many data items')\n";
 
-        // Create an llsdXXXXXX file containing 'data' serialized to
-        // notation.
+        // Create an llsdXXXXXX file containing 'data' serialized per
+        // FormatterFunction.
         NamedTempFile file("llsd",
-                           // NamedTempFile's boost::function constructor
+                           // NamedTempFile's function constructor
                            // takes a callable. To this callable it passes the
                            // std::ostream with which it's writing the
                            // NamedTempFile.
@@ -1953,34 +1967,50 @@ namespace tut
                            (std::ostream& out)
                            { writeLLSDArray(serialize, out, cdata); });
 
-        python("read C++ " + desc,
-               placeholders::arg1 <<
-               import_llsd <<
-               "from functools import partial\n"
-               "import io\n"
-               "import struct\n"
-               "lenformat = struct.Struct('i')\n"
-               "def parse_each(inf):\n"
-               "    for rawlen in iter(partial(inf.read, lenformat.size), b''):\n"
-               "        len = lenformat.unpack(rawlen)[0]\n"
-               // Since llsd.parse() has no max_bytes argument, instead of
-               // passing the input stream directly to parse(), read the item
-               // into a distinct bytes object and parse that.
-               "        data = inf.read(len)\n"
-               "        try:\n"
-               "            frombytes = llsd.parse(data)\n"
-               "        except llsd.LLSDParseError as err:\n"
-               "            print(f'*** {err}')\n"
-               "            print(f'Bad content:\\n{data!r}')\n"
-               "            raise\n"
-               // Also try parsing from a distinct stream.
-               "        stream = io.BytesIO(data)\n"
-               "        fromstream = llsd.parse(stream)\n"
-               "        assert frombytes == fromstream\n"
-               "        yield frombytes\n"
-               << pydata <<
-               // Don't forget raw-string syntax for Windows pathnames.
-               "verify(parse_each(open(r'" << file.getName() << "', 'rb')))\n");
+        // 'debug' starts empty because it's intended as an output file
+        NamedTempFile debug("debug", "");
+
+        try
+        {
+            python("read C++ " + desc,
+                   [&](std::ostream& out){ out <<
+                   import_llsd <<
+                   "from functools import partial\n"
+                   "import io\n"
+                   "import struct\n"
+                   "lenformat = struct.Struct('i')\n"
+                   "def parse_each(inf):\n"
+                   "    for rawlen in iter(partial(inf.read, lenformat.size), b''):\n"
+                   "        print('Read length:', ''.join(('%02x' % b) for b in rawlen),\n"
+                   "              file=debug)\n"
+                   "        len = lenformat.unpack(rawlen)[0]\n"
+                   // Since llsd.parse() has no max_bytes argument, instead of
+                   // passing the input stream directly to parse(), read the item
+                   // into a distinct bytes object and parse that.
+                   "        data = inf.read(len)\n"
+                   "        print('Read data:  ', repr(data), file=debug)\n"
+                   "        try:\n"
+                   "            frombytes = llsd.parse(data)\n"
+                   "        except llsd.LLSDParseError as err:\n"
+                   "            print(f'*** {err}')\n"
+                   "            print(f'Bad content:\\n{data!r}')\n"
+                   "            raise\n"
+                   // Also try parsing from a distinct stream.
+                   "        stream = io.BytesIO(data)\n"
+                   "        fromstream = llsd.parse(stream)\n"
+                   "        assert frombytes == fromstream\n"
+                   "        yield frombytes\n"
+                   << pydata <<
+                   // Don't forget raw-string syntax for Windows pathnames.
+                   "debug = open(r'" << debug.getName() << "', 'w')\n"
+                   "verify(parse_each(open(r'" << file.getName() << "', 'rb')))\n";});
+        }
+        catch (const failure&)
+        {
+            LL_DEBUGS() << "Script debug output:" << LL_ENDL;
+            debug.peep_log();
+            throw;
+        }
     }
 
     template<> template<>
@@ -2067,7 +2097,7 @@ namespace tut
         NamedTempFile file("llsd", "");
 
         python("Python " + pyformatter,
-               placeholders::arg1 <<
+               [&](std::ostream& out){ out <<
                import_llsd <<
                "import struct\n"
                "lenformat = struct.Struct('i')\n"
@@ -2085,7 +2115,7 @@ namespace tut
                "    for item in DATA:\n"
                "        serialized = llsd." << pyformatter << "(item)\n"
                "        f.write(lenformat.pack(len(serialized)))\n"
-               "        f.write(serialized)\n");
+               "        f.write(serialized)\n";});
 
         std::ifstream inf(file.getName().c_str());
         LLSD item;
diff --git a/indra/llcommon/tests/workqueue_test.cpp b/indra/llcommon/tests/workqueue_test.cpp
index 41aa858084720cd53735766dbdbd4acb8d25f5fa..df16f4a46eaecefaa63b3a243ec6b1380ad8bfce 100644
--- a/indra/llcommon/tests/workqueue_test.cpp
+++ b/indra/llcommon/tests/workqueue_test.cpp
@@ -83,7 +83,11 @@ namespace tut
         // signal the work item that it can quit; consider LLOneShotCond.
         LLCond<Shared> data;
         auto start = WorkSchedule::TimePoint::clock::now();
-        auto interval = 100ms;
+        // 2s seems like a long time to wait, since it directly impacts the
+        // duration of this test program. Unfortunately GitHub's Mac runners
+        // are pretty wimpy, and we're getting spurious "too late" errors just
+        // because the thread doesn't wake up as soon as we want.
+        auto interval = 2s;
         queue.postEvery(
             interval,
             [&data, count = 0]
diff --git a/indra/llinventory/llsettingsbase.cpp b/indra/llinventory/llsettingsbase.cpp
index 83225cafb1b17bb3d77ddc23dc57f0216f09a839..30249493be22ca4363741b21183caf82ee4d2bfe 100644
--- a/indra/llinventory/llsettingsbase.cpp
+++ b/indra/llinventory/llsettingsbase.cpp
@@ -31,6 +31,7 @@
 #include <algorithm>
 
 #include "llsdserialize.h"
+#include <boost/bind.hpp>
 
 //=========================================================================
 namespace
diff --git a/indra/llinventory/llsettingssky.cpp b/indra/llinventory/llsettingssky.cpp
index 5a6cc93468097324dae0dd2082543d321f863d00..6ad2e9c260b733090e02fc675cb4e203e67df750 100644
--- a/indra/llinventory/llsettingssky.cpp
+++ b/indra/llinventory/llsettingssky.cpp
@@ -31,6 +31,7 @@
 #include "lltrace.h"
 #include "llfasttimer.h"
 #include "v3colorutil.h"
+#include <boost/bind.hpp>
 
 
 //=========================================================================
diff --git a/indra/llinventory/llsettingswater.cpp b/indra/llinventory/llsettingswater.cpp
index 7fd48a1496fa8bf82d0e53835d1dc082fd5d1947..42e062716dd0cba4b17c22ec4298c9c6687ba280 100644
--- a/indra/llinventory/llsettingswater.cpp
+++ b/indra/llinventory/llsettingswater.cpp
@@ -32,6 +32,7 @@
 #include "llfasttimer.h"
 #include "v3colorutil.h"
 #include "indra_constants.h"
+#include <boost/bind.hpp>
 
 const std::string LLSettingsWater::SETTING_BLUR_MULTIPLIER("blur_multiplier");
 const std::string LLSettingsWater::SETTING_FOG_COLOR("water_fog_color");
diff --git a/indra/llmessage/tests/test_llsdmessage_peer.py b/indra/llmessage/tests/test_llsdmessage_peer.py
index 8e9b6c09e730f7e2051409489a860d57e4fc206f..ff8f40a1449d6d7a6a7486dd4768b6853967a9c6 100755
--- a/indra/llmessage/tests/test_llsdmessage_peer.py
+++ b/indra/llmessage/tests/test_llsdmessage_peer.py
@@ -33,7 +33,6 @@
 import sys
 from http.server import HTTPServer, BaseHTTPRequestHandler
 
-from llsd.fastest_elementtree import parse as xml_parse
 import llsd
 from testrunner import freeport, run, debug, VERBOSE
 import time
diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt
index caebdc956547d9a060664ae48a80a06850727ee4..4a16acea9882176390f37acccc5f4de6d1e03010 100644
--- a/indra/newview/CMakeLists.txt
+++ b/indra/newview/CMakeLists.txt
@@ -2392,20 +2392,15 @@ if (PACKAGE AND (RELEASE_CRASH_REPORTING OR NON_RELEASE_CRASH_REPORTING) AND VIE
       add_dependencies(generate_symbols ${VIEWER_BINARY_NAME})
     endif (WINDOWS)
     if (DARWIN)
-      add_custom_command(OUTPUT "${VIEWER_SYMBOL_FILE}"
-        # See above comments about "tar ...j"
-        COMMAND "tar"
+      # Have to run dsymutil first, then pack up the resulting .dSYM directory
+      add_custom_command(OUTPUT "${VIEWER_APP_DSYM}"
+        COMMAND "dsymutil"
         ARGS
-          "cjf"
-          "${VIEWER_SYMBOL_FILE}"
-          "-C"
-          "${VIEWER_APP_DSYM}/.."
-          "${product}.dSYM"
-        DEPENDS "${VIEWER_APP_DSYM}"
-        COMMENT "Packing dSYM into ${VIEWER_SYMBOL_FILE}"
+          ${VIEWER_APP_EXE}
+        COMMENT "Generating ${VIEWER_APP_DSYM}"
         )
-      add_custom_target(dsym_tarball DEPENDS "${VIEWER_SYMBOL_FILE}")
-      add_dependencies(dsym_tarball ${VIEWER_BINARY_NAME})
+      add_custom_target(dsym_generate DEPENDS "${VIEWER_APP_DSYM}")
+      add_dependencies(dsym_generate ${VIEWER_BINARY_NAME})
       add_custom_command(OUTPUT "${VIEWER_APP_XCARCHIVE}"
         COMMAND "zip"
         ARGS
@@ -2423,24 +2418,22 @@ if (PACKAGE AND (RELEASE_CRASH_REPORTING OR NON_RELEASE_CRASH_REPORTING) AND VIE
       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}"
+        DEPENDS "${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)
+      add_dependencies(generate_symbols dsym_xcarchive)
     endif (DARWIN)
     if (LINUX)
       # TBD
     endif (LINUX)
-  endif (USE_BUGSPLAT)
 
-  # for both Bugsplat and Breakpad
-  add_dependencies(llpackage generate_symbols)
+    add_dependencies(llpackage generate_symbols)
+  endif (USE_BUGSPLAT)
 endif ()
 
 if (LL_TESTS)
diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp
index e43426b22221bed2785999d960ffc9916bb80847..dbc53f329c5f83456cdaeba1d0b92eb1cfe739c8 100644
--- a/indra/newview/llappviewer.cpp
+++ b/indra/newview/llappviewer.cpp
@@ -3174,8 +3174,10 @@ LLSD LLAppViewer::getViewerInfo() const
 	// LLFloaterAbout.
 	LLSD info;
 	auto& versionInfo(LLVersionInfo::instance());
+	// With GitHub builds, the build number is too big to fit in a 32-bit int,
+	// and LLSD doesn't deal with integers wider than int. Use string.
 	info["VIEWER_VERSION"] = llsd::array(versionInfo.getMajor(), versionInfo.getMinor(),
-										 versionInfo.getPatch(), versionInfo.getBuild());
+										 versionInfo.getPatch(), stringize(versionInfo.getBuild()));
 	info["VIEWER_VERSION_STR"] = versionInfo.getVersion();
 	info["CHANNEL"] = versionInfo.getChannel();
 	info["ADDRESS_SIZE"] = ADDRESS_SIZE;
@@ -3553,7 +3555,7 @@ void LLAppViewer::writeSystemInfo()
 	gDebugInfo["ClientInfo"]["MajorVersion"] = LLVersionInfo::instance().getMajor();
 	gDebugInfo["ClientInfo"]["MinorVersion"] = LLVersionInfo::instance().getMinor();
 	gDebugInfo["ClientInfo"]["PatchVersion"] = LLVersionInfo::instance().getPatch();
-	gDebugInfo["ClientInfo"]["BuildVersion"] = LLVersionInfo::instance().getBuild();
+	gDebugInfo["ClientInfo"]["BuildVersion"] = std::to_string(LLVersionInfo::instance().getBuild());
 	gDebugInfo["ClientInfo"]["AddressSize"] = LLVersionInfo::instance().getAddressSize();
 
 	gDebugInfo["CAFilename"] = gDirUtilp->getCAFile();
@@ -5521,7 +5523,7 @@ void LLAppViewer::handleLoginComplete()
 	gDebugInfo["ClientInfo"]["MajorVersion"] = LLVersionInfo::instance().getMajor();
 	gDebugInfo["ClientInfo"]["MinorVersion"] = LLVersionInfo::instance().getMinor();
 	gDebugInfo["ClientInfo"]["PatchVersion"] = LLVersionInfo::instance().getPatch();
-	gDebugInfo["ClientInfo"]["BuildVersion"] = LLVersionInfo::instance().getBuild();
+	gDebugInfo["ClientInfo"]["BuildVersion"] = std::to_string(LLVersionInfo::instance().getBuild());
 
 	LLParcel* parcel = LLViewerParcelMgr::getInstance()->getAgentParcel();
 	if ( parcel && parcel->getMusicURL()[0])
diff --git a/indra/newview/llcurrencyuimanager.cpp b/indra/newview/llcurrencyuimanager.cpp
index 6385f72665a4d6cc14496c0024d0580dc509f5c6..5ddfd521a49b0cd0e5b4e809cbbc9472584c9812 100644
--- a/indra/newview/llcurrencyuimanager.cpp
+++ b/indra/newview/llcurrencyuimanager.cpp
@@ -46,6 +46,7 @@
 #include "llviewernetwork.h"
 #include "llviewerregion.h"
 #include "llpanel.h"
+#include "stringize.h"
 
 
 const F64 CURRENCY_ESTIMATE_FREQUENCY = 2.0;
@@ -159,7 +160,7 @@ void LLCurrencyUIManager::Impl::updateCurrencyInfo()
 		mLocalCurrencyEstimated = true;
 		return;
 	}
-	
+
 	LLXMLRPCValue keywordArgs = LLXMLRPCValue::createStruct();
 	keywordArgs.appendString("agentId", gAgent.getID().asString());
 	keywordArgs.appendString(
@@ -171,8 +172,10 @@ void LLCurrencyUIManager::Impl::updateCurrencyInfo()
 	keywordArgs.appendInt("viewerMajorVersion", LLVersionInfo::instance().getMajor());
 	keywordArgs.appendInt("viewerMinorVersion", LLVersionInfo::instance().getMinor());
 	keywordArgs.appendInt("viewerPatchVersion", LLVersionInfo::instance().getPatch());
-	keywordArgs.appendInt("viewerBuildVersion", LLVersionInfo::instance().getBuild());
-	
+	// With GitHub builds, the build number is too big to fit in a 32-bit int,
+	// and XMLRPC_VALUE doesn't deal with integers wider than int. Use string.
+	keywordArgs.appendString("viewerBuildVersion", stringize(LLVersionInfo::instance().getBuild()));
+
 	LLXMLRPCValue params = LLXMLRPCValue::createArray();
 	params.append(keywordArgs);
 
@@ -246,7 +249,9 @@ void LLCurrencyUIManager::Impl::startCurrencyBuy(const std::string& password)
 	keywordArgs.appendInt("viewerMajorVersion", LLVersionInfo::instance().getMajor());
 	keywordArgs.appendInt("viewerMinorVersion", LLVersionInfo::instance().getMinor());
 	keywordArgs.appendInt("viewerPatchVersion", LLVersionInfo::instance().getPatch());
-	keywordArgs.appendInt("viewerBuildVersion", LLVersionInfo::instance().getBuild());
+	// With GitHub builds, the build number is too big to fit in a 32-bit int,
+	// and XMLRPC_VALUE doesn't deal with integers wider than int. Use string.
+	keywordArgs.appendString("viewerBuildVersion", stringize(LLVersionInfo::instance().getBuild()));
 
 	LLXMLRPCValue params = LLXMLRPCValue::createArray();
 	params.append(keywordArgs);
diff --git a/indra/newview/llenvironment.h b/indra/newview/llenvironment.h
index 0933b959909cd9342a5643553d0dde9e6331cd5c..0d028409fe795c9b4c453aa00f883ca2ff89513a 100644
--- a/indra/newview/llenvironment.h
+++ b/indra/newview/llenvironment.h
@@ -42,6 +42,8 @@
 
 #include <boost/signals2.hpp>
 
+#include <array>
+
 //-------------------------------------------------------------------------
 class LLViewerCamera;
 class LLParcel;
diff --git a/indra/newview/llimview.cpp b/indra/newview/llimview.cpp
index 19fa1c6f0dc80b7dbccecefe84862bce8d74363d..e7a0b69f5efd88683831415b3b6bcf20f3402199 100644
--- a/indra/newview/llimview.cpp
+++ b/indra/newview/llimview.cpp
@@ -78,6 +78,8 @@
 #include "rlvcommon.h"
 // [/RLVa:KB]
 
+#include <array>
+
 const static std::string ADHOC_NAME_SUFFIX(" Conference");
 
 const static std::string NEARBY_P2P_BY_OTHER("nearby_P2P_by_other");
diff --git a/indra/newview/llinventorygallery.cpp b/indra/newview/llinventorygallery.cpp
index 4626099054792512babd039908c3d89610c1a82c..872f3814f7bb8dd3ce4946236f982757280bea4e 100644
--- a/indra/newview/llinventorygallery.cpp
+++ b/indra/newview/llinventorygallery.cpp
@@ -2390,11 +2390,18 @@ void LLInventoryGallery::startDrag()
 {
     std::vector<EDragAndDropType> types;
     uuid_vec_t ids;
+	// ALCHMERGE
+    LLToolDragAndDrop::ESource src = LLToolDragAndDrop::SOURCE_AGENT;
     for (LLUUID& selected_id : mSelectedItemIDs)
     {
         const LLInventoryItem* item = gInventory.getItem(selected_id);
         if (item)
         {
+            if (item->getPermissions().getOwner() == ALEXANDRIA_LINDEN_ID)
+            {
+                src = LLToolDragAndDrop::SOURCE_LIBRARY;
+            }
+
             EDragAndDropType type = LLViewerAssetType::lookupDragAndDropType(item->getType());
             types.push_back(type);
             ids.push_back(selected_id);
@@ -2404,6 +2411,11 @@ void LLInventoryGallery::startDrag()
         if (cat && gInventory.isObjectDescendentOf(selected_id, gInventory.getRootFolderID())
             && !LLFolderType::lookupIsProtectedType((cat)->getPreferredType()))
         {
+            if (cat->getOwnerID() == ALEXANDRIA_LINDEN_ID)
+            {
+                src = LLToolDragAndDrop::SOURCE_LIBRARY;
+            }
+
             EDragAndDropType type = LLViewerAssetType::lookupDragAndDropType(cat->getType());
             types.push_back(type);
             ids.push_back(selected_id);
diff --git a/indra/newview/lllogchat.cpp b/indra/newview/lllogchat.cpp
index b7eee15909c174ce6d73148a3f2057ce9a832408..0bbddcce61e54a515cbf65f723744e980646ec9c 100644
--- a/indra/newview/lllogchat.cpp
+++ b/indra/newview/lllogchat.cpp
@@ -41,6 +41,7 @@
 
 #include <boost/algorithm/string/trim.hpp>
 #include <boost/algorithm/string/replace.hpp>
+#include <boost/regex.hpp>
 #include <boost/date_time/gregorian/gregorian.hpp>
 #include <boost/date_time/posix_time/posix_time.hpp>
 #include <boost/date_time/local_time_adjustor.hpp>
diff --git a/indra/newview/llpanellogin.cpp b/indra/newview/llpanellogin.cpp
index fcfac312b59d71eb64b4c0f87587a800607780ab..fedd1e6df0cbdc8aa96524095ca395a63d7fd950 100644
--- a/indra/newview/llpanellogin.cpp
+++ b/indra/newview/llpanellogin.cpp
@@ -65,6 +65,7 @@
 #include "lltrans.h"
 #include "llglheaders.h"
 #include "llpanelloginlistener.h"
+#include "stringize.h"
 
 #include <boost/algorithm/string.hpp>
 
@@ -859,9 +860,8 @@ void LLPanelLogin::loadLoginPage()
 	}
 
 	// Channel and Version
-	params["version"] = llformat("%s (%d)",
-								 LLVersionInfo::instance().getShortVersion().c_str(),
-								 LLVersionInfo::instance().getBuild());
+	params["version"] = stringize(LLVersionInfo::instance().getShortVersion(), " (",
+								  LLVersionInfo::instance().getBuild(), ')');
 	params["channel"] = LLVersionInfo::instance().getChannel();
 
 	// Grid
diff --git a/indra/newview/lltranslate.cpp b/indra/newview/lltranslate.cpp
index 3916a69808010d02431da7528cac1e9fb3eec621..723d24bec6f5ab77b37a7b6f759dbe468824ee7e 100644
--- a/indra/newview/lltranslate.cpp
+++ b/indra/newview/lltranslate.cpp
@@ -38,6 +38,7 @@
 #include "llcoros.h"
 #include "llcorehttputil.h"
 #include "llurlregistry.h"
+#include "stringize.h"
 
 #include <nlohmann/json.hpp>
 
@@ -160,12 +161,12 @@ void LLTranslationAPIHandler::verifyKeyCoro(LLTranslate::EService service, LLSD
     LLCore::HttpHeaders::ptr_t httpHeaders(std::make_shared<LLCore::HttpHeaders>());
 
 
-    std::string user_agent = llformat("%s %d.%d.%d (%d)",
-        LLVersionInfo::instance().getChannel().c_str(),
-        LLVersionInfo::instance().getMajor(),
-        LLVersionInfo::instance().getMinor(),
-        LLVersionInfo::instance().getPatch(),
-        LLVersionInfo::instance().getBuild());
+    std::string user_agent = stringize(
+        LLVersionInfo::instance().getChannel(), ' ',
+        LLVersionInfo::instance().getMajor(), '.',
+        LLVersionInfo::instance().getMinor(), '.',
+        LLVersionInfo::instance().getPatch(), " (",
+        LLVersionInfo::instance().getBuild(), ')');
 
     initHttpHeader(httpHeaders, user_agent, key);
 
@@ -215,12 +216,12 @@ void LLTranslationAPIHandler::translateMessageCoro(LanguagePair_t fromTo, std::s
     LLCore::HttpHeaders::ptr_t httpHeaders(std::make_shared<LLCore::HttpHeaders>());
 
 
-    std::string user_agent = llformat("%s %d.%d.%d (%d)",
-        LLVersionInfo::instance().getChannel().c_str(),
-        LLVersionInfo::instance().getMajor(),
-        LLVersionInfo::instance().getMinor(),
-        LLVersionInfo::instance().getPatch(),
-        LLVersionInfo::instance().getBuild());
+    std::string user_agent = stringize(
+        LLVersionInfo::instance().getChannel(), ' ',
+        LLVersionInfo::instance().getMajor(), '.',
+        LLVersionInfo::instance().getMinor(), '.',
+        LLVersionInfo::instance().getPatch(), " (",
+        LLVersionInfo::instance().getBuild(), ')');
 
     initHttpHeader(httpHeaders, user_agent);
     httpOpts->setSSLVerifyPeer(false);
diff --git a/indra/newview/llversioninfo.cpp b/indra/newview/llversioninfo.cpp
index 4c8c73fa188089b0a013f791f5cdeaf53421b6c0..5c49eae2cf250b9c22864bc7b480e7cfc254d4e9 100644
--- a/indra/newview/llversioninfo.cpp
+++ b/indra/newview/llversioninfo.cpp
@@ -70,7 +70,7 @@ void LLVersionInfo::initSingleton()
 	// fully constructed; such calls don't really belong in the constructor.
 
 	// cache the version string
-	version = STRINGIZE(getShortVersion() << "." << getBuild());
+	version = stringize(getShortVersion(), ".", getBuild());
 }
 
 LLVersionInfo::~LLVersionInfo()
@@ -92,7 +92,7 @@ S32 LLVersionInfo::getPatch()
 	return LL_VIEWER_VERSION_PATCH;
 }
 
-S32 LLVersionInfo::getBuild()
+U64 LLVersionInfo::getBuild()
 {
 	return LL_VIEWER_VERSION_BUILD;
 }
diff --git a/indra/newview/llversioninfo.h b/indra/newview/llversioninfo.h
index 1f72fe58c9ce9293808d8dd0ba109cd2031e5c10..4ac31e5cb39b31ab95753b0b9b5adc1ebd6f2169 100644
--- a/indra/newview/llversioninfo.h
+++ b/indra/newview/llversioninfo.h
@@ -61,7 +61,7 @@ class LLVersionInfo final : public LLSingleton<LLVersionInfo>
 	S32 getPatch();
 
 	/// return the build number as an integer
-	S32 getBuild();
+	U64 getBuild();
 
 	/// return the full viewer version as a string like "2.0.0.200030"
 	const std::string& getVersion();
diff --git a/indra/newview/llweb.cpp b/indra/newview/llweb.cpp
index c4d873dd22bc42c2ec140567f070ae9c5cdc3a65..9afe332025ce19ba5f512df59f2fbefdd02bc48d 100644
--- a/indra/newview/llweb.cpp
+++ b/indra/newview/llweb.cpp
@@ -160,7 +160,7 @@ std::string LLWeb::expandURLSubstitutions(const std::string &url,
 	substitution["VERSION_MAJOR"] = LLVersionInfo::instance().getMajor();
 	substitution["VERSION_MINOR"] = LLVersionInfo::instance().getMinor();
 	substitution["VERSION_PATCH"] = LLVersionInfo::instance().getPatch();
-	substitution["VERSION_BUILD"] = LLVersionInfo::instance().getBuild();
+	substitution["VERSION_BUILD"] = std::to_string(LLVersionInfo::instance().getBuild());
 	substitution["CHANNEL"] = LLVersionInfo::instance().getChannel();
 	substitution["GRID"] = LLGridManager::getInstance()->getGridId();
 	substitution["GRID_LOWERCASE"] = utf8str_tolower(LLGridManager::getInstance()->getGridId());
diff --git a/indra/newview/llxmlrpctransaction.cpp b/indra/newview/llxmlrpctransaction.cpp
index 4a6e4bd82aad92c6ee505826b552afef7d13c3ea..99df6ae97af683b855c28889ab7082924702624e 100644
--- a/indra/newview/llxmlrpctransaction.cpp
+++ b/indra/newview/llxmlrpctransaction.cpp
@@ -42,6 +42,7 @@
 #include "bufferarray.h"
 #include "llversioninfo.h"
 #include "llviewercontrol.h"
+#include "stringize.h"
 
 // Have to include these last to avoid queue redefinition!
 
@@ -394,14 +395,14 @@ void LLXMLRPCTransaction::Impl::init(XMLRPC_REQUEST request, bool useGzip, const
 
 	httpHeaders->append(HTTP_OUT_HEADER_CONTENT_TYPE, HTTP_CONTENT_TEXT_XML);
 
-    std::string user_agent = llformat("%s %d.%d.%d (%d)",
-        LLVersionInfo::instance().getChannel().c_str(),
-        LLVersionInfo::instance().getMajor(),
-        LLVersionInfo::instance().getMinor(),
-        LLVersionInfo::instance().getPatch(),
-        LLVersionInfo::instance().getBuild());
+	std::string user_agent = stringize(
+		LLVersionInfo::instance().getChannel(), ' ',
+		LLVersionInfo::instance().getMajor(), '.',
+		LLVersionInfo::instance().getMinor(), '.',
+		LLVersionInfo::instance().getPatch(), " (",
+		LLVersionInfo::instance().getBuild(), ')');
 
-    httpHeaders->append(HTTP_OUT_HEADER_USER_AGENT, user_agent);
+	httpHeaders->append(HTTP_OUT_HEADER_USER_AGENT, user_agent);
 
 	///* Setting the DNS cache timeout to -1 disables it completely.
 	//This might help with bug #503 */
diff --git a/indra/newview/rlvcommon.cpp b/indra/newview/rlvcommon.cpp
index fc9fcb2ee573746ac667d8bb956deca62e258c0e..a7ba58f900aebda10851e2ce99d097b20ebd57fc 100644
--- a/indra/newview/rlvcommon.cpp
+++ b/indra/newview/rlvcommon.cpp
@@ -434,7 +434,7 @@ std::string RlvStrings::getVersion(const LLUUID& idRlvObject, bool fLegacy)
 
 std::string RlvStrings::getVersionAbout()
 {
-	return llformat("RLV v%d.%d.%d / RLVa v%d.%d.%d.%d",
+	return fmt::format(FMT_STRING("RLV v{:d}.{:d}.{:d} / RLVa v{:d}.{:d}.{:d}.{:d}"),
 	                RLV_VERSION_MAJOR, RLV_VERSION_MINOR, RLV_VERSION_PATCH, RLVa_VERSION_MAJOR, RLVa_VERSION_MINOR, RLVa_VERSION_PATCH, LLVersionInfo::instance().getBuild());
 }
 
diff --git a/indra/test/hexdump.h b/indra/test/hexdump.h
new file mode 100644
index 0000000000000000000000000000000000000000..dd7cbaaa3c56eecd1f05612be20137d8de678a5d
--- /dev/null
+++ b/indra/test/hexdump.h
@@ -0,0 +1,97 @@
+/**
+ * @file   hexdump.h
+ * @author Nat Goodspeed
+ * @date   2023-09-08
+ * @brief  Provide hexdump() and hexmix() ostream formatters
+ * 
+ * $LicenseInfo:firstyear=2023&license=viewerlgpl$
+ * Copyright (c) 2023, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_HEXDUMP_H)
+#define LL_HEXDUMP_H
+
+#include <cctype>
+#include <iomanip>
+#include <iostream>
+#include <string_view>
+
+// Format a given byte string as 2-digit hex values, no separators
+// Usage: std::cout << hexdump(somestring) << ...
+class hexdump
+{
+public:
+    hexdump(const std::string_view& data):
+        hexdump(data.data(), data.length())
+    {}
+
+    hexdump(const char* data, size_t len):
+        hexdump(reinterpret_cast<const unsigned char*>(data), len)
+    {}
+
+    hexdump(const unsigned char* data, size_t len):
+        mData(data, data + len)
+    {}
+
+    friend std::ostream& operator<<(std::ostream& out, const hexdump& self)
+    {
+        auto oldfmt{ out.flags() };
+        auto oldfill{ out.fill() };
+        out.setf(std::ios_base::hex, std::ios_base::basefield);
+        out.fill('0');
+        for (auto c : self.mData)
+        {
+            out << std::setw(2) << unsigned(c);
+        }
+        out.setf(oldfmt, std::ios_base::basefield);
+        out.fill(oldfill);
+        return out;
+    }
+
+private:
+    std::vector<unsigned char> mData;
+};
+
+// Format a given byte string as a mix of printable characters and, for each
+// non-printable character, "\xnn"
+// Usage: std::cout << hexmix(somestring) << ...
+class hexmix
+{
+public:
+    hexmix(const std::string_view& data):
+        mData(data)
+    {}
+
+    hexmix(const char* data, size_t len):
+        mData(data, len)
+    {}
+
+    friend std::ostream& operator<<(std::ostream& out, const hexmix& self)
+    {
+        auto oldfmt{ out.flags() };
+        auto oldfill{ out.fill() };
+        out.setf(std::ios_base::hex, std::ios_base::basefield);
+        out.fill('0');
+        for (auto c : self.mData)
+        {
+            // std::isprint() must be passed an unsigned char!
+            if (std::isprint(static_cast<unsigned char>(c)))
+            {
+                out << c;
+            }
+            else
+            {
+                out << "\\x" << std::setw(2) << unsigned(c);
+            }
+        }
+        out.setf(oldfmt, std::ios_base::basefield);
+        out.fill(oldfill);
+        return out;
+    }
+
+private:
+    std::string mData;
+};
+
+#endif /* ! defined(LL_HEXDUMP_H) */
diff --git a/indra/test/namedtempfile.h b/indra/test/namedtempfile.h
index 7d59cad32c25618934b9f32b392d7c162170e3c3..ad14cebbd1e916369811b5f6b77d2c57d214d82b 100644
--- a/indra/test/namedtempfile.h
+++ b/indra/test/namedtempfile.h
@@ -13,15 +13,16 @@
 #define LL_NAMEDTEMPFILE_H
 
 #include "llerror.h"
-#include "llapr.h"
-#include "apr_file_io.h"
+#include "llstring.h"
+#include "stringize.h"
 #include <string>
-#include <boost/function.hpp>
-#include <boost/phoenix/core/argument.hpp>
-#include <boost/phoenix/operator/bitwise.hpp>
+#include <boost/filesystem.hpp>
+#include <boost/filesystem/fstream.hpp>
 #include <boost/noncopyable.hpp>
+#include <functional>
 #include <iostream>
 #include <sstream>
+#include <string_view>
 
 /**
  * Create a text file with specified content "somewhere in the
@@ -31,134 +32,123 @@ class NamedTempFile: public boost::noncopyable
 {
     LOG_CLASS(NamedTempFile);
 public:
-    NamedTempFile(const std::string& pfx, const std::string& content, apr_pool_t* pool=gAPRPoolp):
-        mPool(pool)
+    NamedTempFile(const std::string_view& pfx,
+                  const std::string_view& content,
+                  const std::string_view& sfx=std::string_view(""))
     {
-        createFile(pfx, boost::phoenix::placeholders::arg1 << content);
+        createFile(pfx, [&content](std::ostream& out){ out << content; }, sfx);
     }
 
-    // Disambiguate when passing string literal
-    NamedTempFile(const std::string& pfx, const char* content, apr_pool_t* pool=gAPRPoolp):
-        mPool(pool)
+    // Disambiguate when passing string literal -- unclear why a string
+    // literal should be ambiguous wrt std::string_view and Streamer
+    NamedTempFile(const std::string_view& pfx,
+                  const char* content,
+                  const std::string_view& sfx=std::string_view(""))
     {
-        createFile(pfx, boost::phoenix::placeholders::arg1 << content);
+        createFile(pfx, [&content](std::ostream& out){ out << content; }, sfx);
     }
 
     // Function that accepts an ostream ref and (presumably) writes stuff to
     // it, e.g.:
     // (boost::phoenix::placeholders::arg1 << "the value is " << 17 << '\n')
-    typedef boost::function<void(std::ostream&)> Streamer;
+    typedef std::function<void(std::ostream&)> Streamer;
 
-    NamedTempFile(const std::string& pfx, const Streamer& func, apr_pool_t* pool=gAPRPoolp):
-        mPool(pool)
+    NamedTempFile(const std::string_view& pfx,
+                  const Streamer& func,
+                  const std::string_view& sfx=std::string_view(""))
     {
-        createFile(pfx, func);
+        createFile(pfx, func, sfx);
     }
 
     virtual ~NamedTempFile()
     {
-        ll_apr_assert_status(apr_file_remove(mPath.c_str(), mPool));
+        boost::filesystem::remove(mPath);
     }
 
-    virtual std::string getName() const { return mPath; }
+    std::string getName() const { return mPath.string(); }
 
-    void peep()
+    template <typename CALLABLE>
+    void peep_via(CALLABLE&& callable) const
     {
-        std::cout << "File '" << mPath << "' contains:\n";
-        std::ifstream reader(mPath.c_str());
+        std::forward<CALLABLE>(callable)(stringize("File '", mPath, "' contains:"));
+        boost::filesystem::ifstream reader(mPath, std::ios::binary);
         std::string line;
         while (std::getline(reader, line))
-            std::cout << line << '\n';
-        std::cout << "---\n";
+            std::forward<CALLABLE>(callable)(line);
+        std::forward<CALLABLE>(callable)("---");
+    }
+
+    void peep_log() const
+    {
+        peep_via([](const std::string& line){ LL_DEBUGS() << line << LL_ENDL; });
+    }
+
+    void peep(std::ostream& out=std::cout) const
+    {
+        peep_via([&out](const std::string& line){ out << line << '\n'; });
+    }
+
+    friend std::ostream& operator<<(std::ostream& out, const NamedTempFile& self)
+    {
+        self.peep(out);
+        return out;
+    }
+
+    static boost::filesystem::path temp_path(const std::string_view& pfx="",
+                                             const std::string_view& sfx="")
+    {
+        // This variable is set by GitHub actions and is the recommended place
+        // to put temp files belonging to an actions job.
+        const char* RUNNER_TEMP = getenv("RUNNER_TEMP");
+        boost::filesystem::path tempdir{
+            // if RUNNER_TEMP is set and not empty
+            (RUNNER_TEMP && *RUNNER_TEMP)?
+            boost::filesystem::path(RUNNER_TEMP) : // use RUNNER_TEMP if available
+            boost::filesystem::temp_directory_path()}; // else canonical temp dir
+        boost::filesystem::path tempname{
+            // use filename template recommended by unique_path() doc, but
+            // with underscores instead of hyphens: some use cases involve
+            // temporary Python scripts
+            tempdir / stringize(pfx, "%%%%_%%%%_%%%%_%%%%", sfx) };
+        return boost::filesystem::unique_path(tempname);
     }
 
 protected:
-    void createFile(const std::string& pfx, const Streamer& func)
+    void createFile(const std::string_view& pfx,
+                    const Streamer& func,
+                    const std::string_view& sfx)
     {
         // Create file in a temporary place.
-        const char* tempdir = NULL;
-        ll_apr_assert_status(apr_temp_dir_get(&tempdir, mPool));
-
-        // Construct a temp filename template in that directory.
-        char *tempname = NULL;
-        ll_apr_assert_status(apr_filepath_merge(&tempname,
-                                                tempdir,
-                                                (pfx + "XXXXXX").c_str(),
-                                                0,
-                                                mPool));
-
-        // Create a temp file from that template.
-        apr_file_t* fp = NULL;
-        ll_apr_assert_status(apr_file_mktemp(&fp,
-                                             tempname,
-                                             APR_CREATE | APR_WRITE | APR_EXCL,
-                                             mPool));
-        // apr_file_mktemp() alters tempname with the actual name. Not until
-        // now is it valid to capture as our mPath.
-        mPath = tempname;
-
+        mPath = temp_path(pfx, sfx);
+        boost::filesystem::ofstream out{ mPath, std::ios::binary };
         // Write desired content.
-        std::ostringstream out;
-        // Stream stuff to it.
         func(out);
-
-        std::string data(out.str());
-        apr_size_t writelen(data.length());
-        ll_apr_assert_status(apr_file_write(fp, data.c_str(), &writelen));
-        ll_apr_assert_status(apr_file_close(fp));
-        llassert_always(writelen == data.length());
     }
 
-    std::string mPath;
-    apr_pool_t* mPool;
+    boost::filesystem::path mPath;
 };
 
 /**
  * Create a NamedTempFile with a specified filename extension. This is useful
  * when, for instance, you must be able to use the file in a Python import
  * statement.
- *
- * A NamedExtTempFile actually has two different names. We retain the original
- * no-extension name as a placeholder in the temp directory to ensure
- * uniqueness; to that we link the name plus the desired extension. Naturally,
- * both must be removed on destruction.
  */
 class NamedExtTempFile: public NamedTempFile
 {
     LOG_CLASS(NamedExtTempFile);
 public:
-    NamedExtTempFile(const std::string& ext, const std::string& content, apr_pool_t* pool=gAPRPoolp):
-        NamedTempFile(remove_dot(ext), content, pool),
-        mLink(mPath + ensure_dot(ext))
-    {
-        linkto(mLink);
-    }
+    NamedExtTempFile(const std::string& ext, const std::string_view& content):
+        NamedTempFile(remove_dot(ext), content, ensure_dot(ext))
+    {}
 
     // Disambiguate when passing string literal
-    NamedExtTempFile(const std::string& ext, const char* content, apr_pool_t* pool=gAPRPoolp):
-        NamedTempFile(remove_dot(ext), content, pool),
-        mLink(mPath + ensure_dot(ext))
-    {
-        linkto(mLink);
-    }
-
-    NamedExtTempFile(const std::string& ext, const Streamer& func, apr_pool_t* pool=gAPRPoolp):
-        NamedTempFile(remove_dot(ext), func, pool),
-        mLink(mPath + ensure_dot(ext))
-    {
-        linkto(mLink);
-    }
+    NamedExtTempFile(const std::string& ext, const char* content):
+        NamedTempFile(remove_dot(ext), content, ensure_dot(ext))
+    {}
 
-    virtual ~NamedExtTempFile()
-    {
-        ll_apr_assert_status(apr_file_remove(mLink.c_str(), mPool));
-    }
-
-    // Since the caller has gone to the trouble to create the name with the
-    // extension, that should be the name we return. In this class, mPath is
-    // just a placeholder to ensure that future createFile() calls won't
-    // collide.
-    virtual std::string getName() const { return mLink; }
+    NamedExtTempFile(const std::string& ext, const Streamer& func):
+        NamedTempFile(remove_dot(ext), func, ensure_dot(ext))
+    {}
 
     static std::string ensure_dot(const std::string& ext)
     {
@@ -175,7 +165,7 @@ class NamedExtTempFile: public NamedTempFile
         {
             return ext;
         }
-        return std::string(".") + ext;
+        return "." + ext;
     }
 
     static std::string remove_dot(const std::string& ext)
@@ -187,19 +177,6 @@ class NamedExtTempFile: public NamedTempFile
         }
         return ext.substr(found);
     }
-
-private:
-    void linkto(const std::string& path)
-    {
-        // This method assumes that since mPath (without extension) is
-        // guaranteed by apr_file_mktemp() to be unique, then (mPath + any
-        // extension) is also unique. This is likely, though not guaranteed:
-        // files could be created in the same temp directory other than by
-        // this class.
-        ll_apr_assert_status(apr_file_link(mPath.c_str(), path.c_str()));
-    }
-
-    std::string mLink;
 };
 
 #endif /* ! defined(LL_NAMEDTEMPFILE_H) */
diff --git a/indra/test/test.cpp b/indra/test/test.cpp
index 1e9833c4b0adddb5c6507c627faac957557b3ab0..06ae583d76dfa98c16f58089a0a6e5ebc4051e90 100644
--- a/indra/test/test.cpp
+++ b/indra/test/test.cpp
@@ -90,10 +90,10 @@ class LLReplayLog
 class RecordToTempFile : public LLError::Recorder, public boost::noncopyable
 {
 public:
-	RecordToTempFile(apr_pool_t* pPool)
+	RecordToTempFile()
 		: LLError::Recorder(),
 		boost::noncopyable(),
-		mTempFile("log", "", pPool),
+		mTempFile("log", ""),
 		mFile(mTempFile.getName().c_str())
 	{
 	}
@@ -134,11 +134,11 @@ class RecordToTempFile : public LLError::Recorder, public boost::noncopyable
 class LLReplayLogReal: public LLReplayLog, public boost::noncopyable
 {
 public:
-	LLReplayLogReal(LLError::ELevel level, apr_pool_t* pool)
+	LLReplayLogReal(LLError::ELevel level)
 		: LLReplayLog(),
 		boost::noncopyable(),
 		mOldSettings(LLError::saveAndResetSettings()),
-		mRecorder(new RecordToTempFile(pool))
+		mRecorder(new RecordToTempFile())
 	{
 		LLError::setFatalFunction(wouldHaveCrashed);
 		LLError::setDefaultLevel(level);
@@ -611,7 +611,7 @@ int main(int argc, char **argv)
 		if (LOGFAIL && *LOGFAIL)
 		{
 			LLError::ELevel level = LLError::decodeLevel(LOGFAIL);
-			replayer.reset(new LLReplayLogReal(level, gAPRPoolp));
+			replayer.reset(new LLReplayLogReal(level));
 		}
 	}
 	LLError::setFatalFunction(wouldHaveCrashed);