diff --git a/autobuild.xml b/autobuild.xml
index 10895b34d2e193207bebec708610044e29031eac..c90ffe493cc8b6b16162145127868fb126301bc2 100644
--- a/autobuild.xml
+++ b/autobuild.xml
@@ -3231,9 +3231,9 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors</string>
             <key>archive</key>
             <map>
               <key>hash</key>
-              <string>fc3291e515f11e8324407949fb53cb53</string>
+              <string>c4d56d3e942169661a598035a9fbd533</string>
               <key>url</key>
-              <string>http://automated-builds-secondlife-com.s3.amazonaws.com/ct2/11161/62517/viewer_manager-1.0.511006-darwin64-511006.tar.bz2</string>
+              <string>http://automated-builds-secondlife-com.s3.amazonaws.com/ct2/12112/71187/viewer_manager-1.0.511688-darwin64-511688.tar.bz2</string>
             </map>
             <key>name</key>
             <string>darwin64</string>
@@ -3255,9 +3255,9 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors</string>
             <key>archive</key>
             <map>
               <key>hash</key>
-              <string>93421dfd7486517350c434cf568d0914</string>
+              <string>61f324d880eaa303669b99e28e6cf64c</string>
               <key>url</key>
-              <string>http://automated-builds-secondlife-com.s3.amazonaws.com/ct2/11163/62529/viewer_manager-1.0.511006-windows-511006.tar.bz2</string>
+              <string>http://automated-builds-secondlife-com.s3.amazonaws.com/ct2/12113/71193/viewer_manager-1.0.511688-windows-511688.tar.bz2</string>
             </map>
             <key>name</key>
             <string>windows</string>
@@ -3268,7 +3268,7 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors</string>
         <key>source_type</key>
         <string>hg</string>
         <key>version</key>
-        <string>1.0.511006</string>
+        <string>1.0.511688</string>
       </map>
       <key>vlc-bin</key>
       <map>
diff --git a/indra/lib/python/indra/util/llmanifest.py b/indra/lib/python/indra/util/llmanifest.py
index 7050ce43b73ffe33a836115cc6f341fd30a4c496..598261e3d7000772b1fc57d7c3c09671db44a108 100755
--- a/indra/lib/python/indra/util/llmanifest.py
+++ b/indra/lib/python/indra/util/llmanifest.py
@@ -529,7 +529,7 @@ def run_command(self, command):
         print "Running command:", command
         sys.stdout.flush()
         try:
-            subprocess.check_call(command, shell=True)
+            subprocess.check_call(command)
         except subprocess.CalledProcessError as err:
             raise ManifestError( "Command %s returned non-zero status (%s)"
                                 % (command, err.returncode) )
@@ -545,6 +545,7 @@ def created_path(self, path):
     def put_in_file(self, contents, dst, src=None):
         # write contents as dst
         dst_path = self.dst_path_of(dst)
+        self.cmakedirs(os.path.dirname(dst_path))
         f = open(dst_path, "wb")
         try:
             f.write(contents)
diff --git a/indra/llcommon/llfile.cpp b/indra/llcommon/llfile.cpp
index 7b559861bb71696dabc19a912e5b8fc11b230d7a..8aa41035b9b07ec17e6a80c96675a5480818f600 100644
--- a/indra/llcommon/llfile.cpp
+++ b/indra/llcommon/llfile.cpp
@@ -182,7 +182,14 @@ int	LLFile::mkdir(const std::string& dirname, int perms)
 	int rc = ::mkdir(dirname.c_str(), (mode_t)perms);
 #endif
 	// We often use mkdir() to ensure the existence of a directory that might
-	// already exist. Don't spam the log if it does.
+	// already exist. There is no known case in which we want to call out as
+	// an error the requested directory already existing.
+	if (rc < 0 && errno == EEXIST)
+	{
+		// this is not the error you want, move along
+		return 0;
+	}
+	// anything else might be a problem
 	return warnif("mkdir", dirname, rc, EEXIST);
 }
 
diff --git a/indra/llcommon/llfile.h b/indra/llcommon/llfile.h
index 37eb75881c2c4e8ec4449ecc1871a5bdba696d0f..ba935b8714173ef64ce714df99fdb4f2c7de3b41 100644
--- a/indra/llcommon/llfile.h
+++ b/indra/llcommon/llfile.h
@@ -69,6 +69,7 @@ class LL_COMMON_API LLFile
 
 	// perms is a permissions mask like 0777 or 0700.  In most cases it will
 	// be overridden by the user's umask.  It is ignored on Windows.
+	// mkdir() considers "directory already exists" to be SUCCESS.
 	static	int		mkdir(const std::string& filename, int perms = 0700);
 
 	static	int		rmdir(const std::string& filename);
diff --git a/indra/llui/llspellcheck.cpp b/indra/llui/llspellcheck.cpp
index 5a5260033710318bd9575e37a70abd038e24b23a..7f64743e99070cd5d8f461d7b50ba4f6dffa96f1 100644
--- a/indra/llui/llspellcheck.cpp
+++ b/indra/llui/llspellcheck.cpp
@@ -406,10 +406,7 @@ const std::string LLSpellChecker::getDictionaryAppPath()
 const std::string LLSpellChecker::getDictionaryUserPath()
 {
 	std::string dict_path = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, DICT_DIR, "");
-	if (!gDirUtilp->fileExists(dict_path))
-	{
-		LLFile::mkdir(dict_path);
-	}
+	LLFile::mkdir(dict_path);
 	return dict_path;
 }
 
diff --git a/indra/llvfs/lldir.cpp b/indra/llvfs/lldir.cpp
index b845de71fa3b1aaa31deeae356d936f252478fd5..2069888774fce47c55f47291f9d9cf4df26c23fb 100644
--- a/indra/llvfs/lldir.cpp
+++ b/indra/llvfs/lldir.cpp
@@ -597,7 +597,7 @@ std::string LLDir::getExpandedFilename(ELLPath location, const std::string& subd
 				<< "': prefix is empty, possible bad filename" << LL_ENDL;
 	}
 
-	std::string expanded_filename = add(add(prefix, subdir1), subdir2);
+	std::string expanded_filename = add(prefix, subdir1, subdir2);
 	if (expanded_filename.empty() && in_filename.empty())
 	{
 		return "";
@@ -693,7 +693,7 @@ void LLDir::walkSearchSkinDirs(const std::string& subdir,
 		std::string subdir_path(add(skindir, subdir));
 		BOOST_FOREACH(std::string subsubdir, subsubdirs)
 		{
-			std::string full_path(add(add(subdir_path, subsubdir), filename));
+			std::string full_path(add(subdir_path, subsubdir, filename));
 			if (fileExists(full_path))
 			{
 				function(subsubdir, full_path);
@@ -1052,13 +1052,6 @@ void LLDir::dumpCurrentDirectories()
 	LL_DEBUGS("AppInit","Directories") << "  SkinDir:               " << getSkinDir() << LL_ENDL;
 }
 
-std::string LLDir::add(const std::string& path, const std::string& name) const
-{
-	std::string destpath(path);
-	append(destpath, name);
-	return destpath;
-}
-
 void LLDir::append(std::string& destpath, const std::string& name) const
 {
 	// Delegate question of whether we need a separator to helper method.
diff --git a/indra/llvfs/lldir.h b/indra/llvfs/lldir.h
index b219c6e29fb00e6b4a197fc495e2bdc0876d576a..e233413a7f422a7010a7fa1eccc5a5d8bd5e6701 100644
--- a/indra/llvfs/lldir.h
+++ b/indra/llvfs/lldir.h
@@ -202,9 +202,28 @@ class LLDir
 	/// Append specified @a name to @a destpath, separated by getDirDelimiter()
 	/// if both are non-empty.
 	void append(std::string& destpath, const std::string& name) const;
-	/// Append specified @a name to @a path, separated by getDirDelimiter()
-	/// if both are non-empty. Return result, leaving @a path unmodified.
-	std::string add(const std::string& path, const std::string& name) const;
+	/// Variadic form: append @a name0 and @a name1 and arbitrary other @a
+	/// names to @a destpath, separated by getDirDelimiter() as needed.
+	template <typename... NAMES>
+	void append(std::string& destpath, const std::string& name0, const std::string& name1,
+				const NAMES& ... names) const
+	{
+		// In a typical recursion case, we'd accept (destpath, name0, names).
+		// We accept (destpath, name0, name1, names) because it's important to
+		// delegate the two-argument case to the non-template implementation.
+		append(destpath, name0);
+		append(destpath, name1, names...);
+	}
+
+	/// Append specified @a names to @a path, separated by getDirDelimiter()
+	/// as needed. Return result, leaving @a path unmodified.
+	template <typename... NAMES>
+	std::string add(const std::string& path, const NAMES& ... names) const
+	{
+		std::string destpath(path);
+		append(destpath, names...);
+		return destpath;
+	}
 
 protected:
 	// Does an add() or append() call need a directory delimiter?
diff --git a/indra/llvfs/lldir_linux.cpp b/indra/llvfs/lldir_linux.cpp
index 7a4034c22878dfe45d673dff231aaec3ab87deb6..a9f3166d41ba19699a22e673f8d1d4b35382218f 100644
--- a/indra/llvfs/lldir_linux.cpp
+++ b/indra/llvfs/lldir_linux.cpp
@@ -185,41 +185,29 @@ void LLDir_Linux::initAppDirs(const std::string &app_name,
 	int res = LLFile::mkdir(mOSUserAppDir);
 	if (res == -1)
 	{
-		if (errno != EEXIST)
-		{
-			LL_WARNS() << "Couldn't create app user dir " << mOSUserAppDir << LL_ENDL;
-			LL_WARNS() << "Default to base dir" << mOSUserDir << LL_ENDL;
-			mOSUserAppDir = mOSUserDir;
-		}
+		LL_WARNS() << "Couldn't create app user dir " << mOSUserAppDir << LL_ENDL;
+		LL_WARNS() << "Default to base dir" << mOSUserDir << LL_ENDL;
+		mOSUserAppDir = mOSUserDir;
 	}
 
 	res = LLFile::mkdir(getExpandedFilename(LL_PATH_LOGS,""));
 	if (res == -1)
 	{
-		if (errno != EEXIST)
-		{
-			LL_WARNS() << "Couldn't create LL_PATH_LOGS dir " << getExpandedFilename(LL_PATH_LOGS,"") << LL_ENDL;
-		}
+		LL_WARNS() << "Couldn't create LL_PATH_LOGS dir " << getExpandedFilename(LL_PATH_LOGS,"") << LL_ENDL;
 	}
 	
 	res = LLFile::mkdir(getExpandedFilename(LL_PATH_USER_SETTINGS,""));
 	if (res == -1)
 	{
-		if (errno != EEXIST)
-		{
-			LL_WARNS() << "Couldn't create LL_PATH_USER_SETTINGS dir " << getExpandedFilename(LL_PATH_USER_SETTINGS,"") << LL_ENDL;
-		}
+		LL_WARNS() << "Couldn't create LL_PATH_USER_SETTINGS dir " << getExpandedFilename(LL_PATH_USER_SETTINGS,"") << LL_ENDL;
 	}
-	
+
 	res = LLFile::mkdir(getExpandedFilename(LL_PATH_CACHE,""));
 	if (res == -1)
 	{
-		if (errno != EEXIST)
-		{
-			LL_WARNS() << "Couldn't create LL_PATH_CACHE dir " << getExpandedFilename(LL_PATH_CACHE,"") << LL_ENDL;
-		}
+		LL_WARNS() << "Couldn't create LL_PATH_CACHE dir " << getExpandedFilename(LL_PATH_CACHE,"") << LL_ENDL;
 	}
-	
+
 	mCAFile = getExpandedFilename(LL_PATH_APP_SETTINGS, "CA.pem");
 }
 
diff --git a/indra/llvfs/lldir_solaris.cpp b/indra/llvfs/lldir_solaris.cpp
index b43b2f27ce0a00576a83f33f5f6bddf75456bb4c..d60237bacc09f47a4d6bbb50d212af4e6c8f653d 100644
--- a/indra/llvfs/lldir_solaris.cpp
+++ b/indra/llvfs/lldir_solaris.cpp
@@ -203,41 +203,29 @@ void LLDir_Solaris::initAppDirs(const std::string &app_name,
 	int res = LLFile::mkdir(mOSUserAppDir);
 	if (res == -1)
 	{
-		if (errno != EEXIST)
-		{
-			LL_WARNS() << "Couldn't create app user dir " << mOSUserAppDir << LL_ENDL;
-			LL_WARNS() << "Default to base dir" << mOSUserDir << LL_ENDL;
-			mOSUserAppDir = mOSUserDir;
-		}
+		LL_WARNS() << "Couldn't create app user dir " << mOSUserAppDir << LL_ENDL;
+		LL_WARNS() << "Default to base dir" << mOSUserDir << LL_ENDL;
+		mOSUserAppDir = mOSUserDir;
 	}
 
 	res = LLFile::mkdir(getExpandedFilename(LL_PATH_LOGS,""));
 	if (res == -1)
 	{
-		if (errno != EEXIST)
-		{
-			LL_WARNS() << "Couldn't create LL_PATH_LOGS dir " << getExpandedFilename(LL_PATH_LOGS,"") << LL_ENDL;
-		}
+		LL_WARNS() << "Couldn't create LL_PATH_LOGS dir " << getExpandedFilename(LL_PATH_LOGS,"") << LL_ENDL;
 	}
 	
 	res = LLFile::mkdir(getExpandedFilename(LL_PATH_USER_SETTINGS,""));
 	if (res == -1)
 	{
-		if (errno != EEXIST)
-		{
-			LL_WARNS() << "Couldn't create LL_PATH_USER_SETTINGS dir " << getExpandedFilename(LL_PATH_USER_SETTINGS,"") << LL_ENDL;
-		}
+		LL_WARNS() << "Couldn't create LL_PATH_USER_SETTINGS dir " << getExpandedFilename(LL_PATH_USER_SETTINGS,"") << LL_ENDL;
 	}
-	
+
 	res = LLFile::mkdir(getExpandedFilename(LL_PATH_CACHE,""));
 	if (res == -1)
 	{
-		if (errno != EEXIST)
-		{
-			LL_WARNS() << "Couldn't create LL_PATH_CACHE dir " << getExpandedFilename(LL_PATH_CACHE,"") << LL_ENDL;
-		}
+		LL_WARNS() << "Couldn't create LL_PATH_CACHE dir " << getExpandedFilename(LL_PATH_CACHE,"") << LL_ENDL;
 	}
-	
+
 	mCAFile = getExpandedFilename(LL_PATH_APP_SETTINGS, "CA.pem");
 }
 
diff --git a/indra/llvfs/lldir_win32.cpp b/indra/llvfs/lldir_win32.cpp
index ebc8fdca33051815cb7ec79eb8760056360136d7..5485349c837009a9084a88b131db97b422822f36 100644
--- a/indra/llvfs/lldir_win32.cpp
+++ b/indra/llvfs/lldir_win32.cpp
@@ -31,7 +31,8 @@
 #include "lldir_win32.h"
 #include "llerror.h"
 #include "llrand.h"		// for gLindenLabRandomNumber
-#include "shlobj.h"
+#include <shlobj.h>
+#include <fstream>
 
 #include <direct.h>
 #include <errno.h>
@@ -44,28 +45,25 @@ DWORD GetDllVersion(LPCTSTR lpszDllName);
 
 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
-	SHGetSpecialFolderPath(NULL, w_str, CSIDL_APPDATA, TRUE);
-
-	mOSUserDir = utf16str_to_utf8str(llutf16string(w_str));
+	// 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"));
 
 	// We want cache files to go on the local disk, even if the
 	// user is on a network with a "roaming profile".
 	//
-	// On XP this is:
-	//   C:\Docments and Settings\James\Local Settings\Application Data
 	// On Vista this is:
 	//   C:\Users\James\AppData\Local
 	//
 	// We used to store the cache in AppData\Roaming, and the installer
 	// cleans up that version on upgrade.  JC
-	SHGetSpecialFolderPath(NULL, w_str, CSIDL_LOCAL_APPDATA, TRUE);
-	mOSCacheDir = utf16str_to_utf8str(llutf16string(w_str));
+	mOSCacheDir = ll_safe_string(getenv("LOCALAPPDATA"));
 
+	WCHAR w_str[MAX_PATH];
 	if (GetTempPath(MAX_PATH, w_str))
 	{
 		if (wcslen(w_str))	/* Flawfinder: ignore */ 
@@ -73,12 +71,54 @@ LLDir_Win32::LLDir_Win32()
 			w_str[wcslen(w_str)-1] = '\0'; /* Flawfinder: ignore */ // remove trailing slash
 		}
 		mTempDir = utf16str_to_utf8str(llutf16string(w_str));
+
+		if (mOSUserDir.empty())
+		{
+			mOSUserDir = mTempDir;
+		}
+
+		if (mOSCacheDir.empty())
+		{
+			mOSCacheDir = mTempDir;
+		}
 	}
 	else
 	{
 		mTempDir = mOSUserDir;
 	}
 
+/*==========================================================================*|
+	// Now that we've got mOSUserDir, one way or another, let's see how we did
+	// with our environment variables.
+	{
+		auto report = [this](std::ostream& out){
+			out << "mOSUserDir  = '" << mOSUserDir  << "'\n"
+				<< "mOSCacheDir = '" << mOSCacheDir << "'\n"
+				<< "mTempDir    = '" << mTempDir    << "'" << std::endl;
+		};
+		int res = LLFile::mkdir(mOSUserDir);
+		if (res == -1)
+		{
+			// If we couldn't even create the directory, just blurt to stderr
+			report(std::cerr);
+		}
+		else
+		{
+			// successfully created logdir, plunk a log file there
+			std::string logfilename(add(mOSUserDir, "lldir.log"));
+			std::ofstream logfile(logfilename.c_str());
+			if (! logfile.is_open())
+			{
+				report(std::cerr);
+			}
+			else
+			{
+				report(logfile);
+			}
+		}
+	}
+|*==========================================================================*/
+
 //	fprintf(stderr, "mTempDir = <%s>",mTempDir);
 
 	// Set working directory, for LLDir::getWorkingDir()
@@ -124,7 +164,7 @@ LLDir_Win32::LLDir_Win32()
 	// 'llplugin' need to go somewhere else.
 	// alas... this also gets called during static initialization 
 	// time due to the construction of gDirUtil in lldir.cpp.
-	if(! LLFile::isdir(mAppRODataDir + mDirDelimiter + "skins"))
+	if(! LLFile::isdir(add(mAppRODataDir, "skins")))
 	{
 		// What? No skins in the working dir?
 		// Try the executable's directory.
@@ -133,7 +173,7 @@ LLDir_Win32::LLDir_Win32()
 
 //	LL_INFOS() << "mAppRODataDir = " << mAppRODataDir << LL_ENDL;
 
-	mSkinBaseDir = mAppRODataDir + mDirDelimiter + "skins";
+	mSkinBaseDir = add(mAppRODataDir, "skins");
 
 	// Build the default cache directory
 	mDefaultCacheDir = buildSLOSCacheDir();
@@ -142,13 +182,10 @@ LLDir_Win32::LLDir_Win32()
 	int res = LLFile::mkdir(mDefaultCacheDir);
 	if (res == -1)
 	{
-		if (errno != EEXIST)
-		{
-			LL_WARNS() << "Couldn't create LL_PATH_CACHE dir " << mDefaultCacheDir << LL_ENDL;
-		}
+		LL_WARNS() << "Couldn't create LL_PATH_CACHE dir " << mDefaultCacheDir << LL_ENDL;
 	}
 
-	mLLPluginDir = mExecutableDir + mDirDelimiter + "llplugin";
+	mLLPluginDir = add(mExecutableDir, "llplugin");
 }
 
 LLDir_Win32::~LLDir_Win32()
@@ -164,52 +201,38 @@ void LLDir_Win32::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;
-	mOSUserAppDir = mOSUserDir;
-	mOSUserAppDir += "\\";
-	mOSUserAppDir += app_name;
+	mOSUserAppDir = add(mOSUserDir, app_name);
 
 	int res = LLFile::mkdir(mOSUserAppDir);
 	if (res == -1)
 	{
-		if (errno != EEXIST)
-		{
-			LL_WARNS() << "Couldn't create app user dir " << mOSUserAppDir << LL_ENDL;
-			LL_WARNS() << "Default to base dir" << mOSUserDir << LL_ENDL;
-			mOSUserAppDir = mOSUserDir;
-		}
+		LL_WARNS() << "Couldn't create app user dir " << mOSUserAppDir << LL_ENDL;
+		LL_WARNS() << "Default to base dir" << mOSUserDir << LL_ENDL;
+		mOSUserAppDir = mOSUserDir;
 	}
 	//dumpCurrentDirectories();
 
 	res = LLFile::mkdir(getExpandedFilename(LL_PATH_LOGS,""));
 	if (res == -1)
 	{
-		if (errno != EEXIST)
-		{
-			LL_WARNS() << "Couldn't create LL_PATH_LOGS dir " << getExpandedFilename(LL_PATH_LOGS,"") << LL_ENDL;
-		}
+		LL_WARNS() << "Couldn't create LL_PATH_LOGS dir " << getExpandedFilename(LL_PATH_LOGS,"") << LL_ENDL;
 	}
-	
+
 	res = LLFile::mkdir(getExpandedFilename(LL_PATH_USER_SETTINGS,""));
 	if (res == -1)
 	{
-		if (errno != EEXIST)
-		{
-			LL_WARNS() << "Couldn't create LL_PATH_USER_SETTINGS dir " << getExpandedFilename(LL_PATH_USER_SETTINGS,"") << LL_ENDL;
-		}
+		LL_WARNS() << "Couldn't create LL_PATH_USER_SETTINGS dir " << getExpandedFilename(LL_PATH_USER_SETTINGS,"") << LL_ENDL;
 	}
-	
+
 	res = LLFile::mkdir(getExpandedFilename(LL_PATH_CACHE,""));
 	if (res == -1)
 	{
-		if (errno != EEXIST)
-		{
-			LL_WARNS() << "Couldn't create LL_PATH_CACHE dir " << getExpandedFilename(LL_PATH_CACHE,"") << LL_ENDL;
-		}
+		LL_WARNS() << "Couldn't create LL_PATH_CACHE dir " << getExpandedFilename(LL_PATH_CACHE,"") << LL_ENDL;
 	}
-	
+
 	mCAFile = getExpandedFilename(LL_PATH_APP_SETTINGS, "CA.pem");
 }
 
diff --git a/indra/media_plugins/libvlc/media_plugin_libvlc.cpp b/indra/media_plugins/libvlc/media_plugin_libvlc.cpp
index 048e7675f8a6006d010db3376af99b224dc0ffb8..80702a10797229208690e1b2a557284d28aa2fb7 100644
--- a/indra/media_plugins/libvlc/media_plugin_libvlc.cpp
+++ b/indra/media_plugins/libvlc/media_plugin_libvlc.cpp
@@ -37,6 +37,11 @@
 #include "vlc/vlc.h"
 #include "vlc/libvlc_version.h"
 
+#if LL_WINDOWS
+// needed for waveOut call - see below for description
+#include <mmsystem.h>
+#endif
+
 ////////////////////////////////////////////////////////////////////////////////
 //
 class MediaPluginLibVLC :
@@ -55,6 +60,7 @@ class MediaPluginLibVLC :
 	void playMedia();
 	void resetVLC();
 	void setVolume(const F64 volume);
+	void setVolumeVLC();
 	void updateTitle(const char* title);
 
 	static void* lock(void* data, void** p_pixels);
@@ -221,6 +227,7 @@ void MediaPluginLibVLC::eventCallbacks(const libvlc_event_t* event, void* ptr)
 	case libvlc_MediaPlayerPlaying:
 		parent->mDuration = (float)(libvlc_media_get_duration(parent->mLibVLCMedia)) / 1000.0f;
 		parent->mVlcStatus = STATUS_PLAYING;
+		parent->setVolumeVLC();
 		break;
 
 	case libvlc_MediaPlayerPaused:
@@ -394,28 +401,58 @@ void MediaPluginLibVLC::updateTitle(const char* title)
 	sendMessage(message);
 }
 
-////////////////////////////////////////////////////////////////////////////////
-//
-void MediaPluginLibVLC::setVolume(const F64 volume)
+void MediaPluginLibVLC::setVolumeVLC()
 {
-	mCurVolume = volume;
-
 	if (mLibVLCMediaPlayer)
 	{
-		int result = libvlc_audio_set_volume(mLibVLCMediaPlayer, (int)(volume * 100));
-		if (result != 0)
+		int vlc_vol = (int)(mCurVolume * 100);
+
+		int result = libvlc_audio_set_volume(mLibVLCMediaPlayer, vlc_vol);
+		if (result == 0)
+		{
+			// volume change was accepted by LibVLC
+		}
+		else
 		{
-			// volume wasn't set but not much to be done here
+			// volume change was NOT accepted by LibVLC and not actioned
 		}
+
+#if LL_WINDOWS
+		// https ://jira.secondlife.com/browse/MAINT-8119
+		// CEF media plugin uses code in media_plugins/cef/windows_volume_catcher.cpp to
+		// set the actual output volume of the plugin process since there is no API in 
+		// CEF to otherwise do this.
+		// There are explicit calls to change the volume in LibVLC but sometimes they
+		// are ignored SLPlugin.exe process volume is set to 0 so you never heard audio
+		// from the VLC media stream.
+		// The right way to solve this is to move the volume catcher stuff out of 
+		// the CEF plugin and into it's own folder under media_plugins and have it referenced
+		// by both CEF and VLC. That's for later. The code there boils down to this so for 
+                // now, as we approach a release, the less risky option is to do it directly vs
+                // calls to volume catcher code.
+		DWORD left_channel = (DWORD)(mCurVolume * 65535.0f);
+		DWORD right_channel = (DWORD)(mCurVolume * 65535.0f);
+		DWORD hw_volume = left_channel << 16 | right_channel;
+		waveOutSetVolume(NULL, hw_volume);
+#endif
 	}
 	else
 	{
 		// volume change was requested but VLC wasn't ready.
-		// that's okay thought because we saved the value in mCurVolume and 
+		// that's okay though because we saved the value in mCurVolume and 
 		// the next volume change after the VLC system is initilzied  will set it
 	}
 }
 
+////////////////////////////////////////////////////////////////////////////////
+//
+void MediaPluginLibVLC::setVolume(const F64 volume)
+{
+	mCurVolume = volume;
+
+	setVolumeVLC();
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 //
 void MediaPluginLibVLC::receiveMessage(const char* message_string)
diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml
index c0e963c228d6fc3ee17dda27cb8e7eaf075add61..4a4f4bfc61946333a53ff25c2a8573d7bc5fdd01 100644
--- a/indra/newview/app_settings/settings.xml
+++ b/indra/newview/app_settings/settings.xml
@@ -12332,7 +12332,7 @@
     <key>UpdaterMaximumBandwidth</key>
     <map>
       <key>Comment</key>
-      <string>Maximum allowable downstream bandwidth for updater service (kilo bits per second)</string>
+      <string>Obsolete: this parameter is no longer used.</string>
       <key>Persist</key>
       <integer>1</integer>
       <key>Type</key>
diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp
index 225ade295727c04561c22781f3b6e869ef84f76f..95e5cbe09e1080a63d9bb3e93001472e2f563918 100644
--- a/indra/newview/llappviewer.cpp
+++ b/indra/newview/llappviewer.cpp
@@ -4134,10 +4134,7 @@ void dumpVFSCaches()
 	S32 res = LLFile::mkdir("StaticVFSDump");
 	if (res == -1)
 	{
-		if (errno != EEXIST)
-		{
-			LL_WARNS() << "Couldn't create dir StaticVFSDump" << LL_ENDL;
-		}
+		LL_WARNS() << "Couldn't create dir StaticVFSDump" << LL_ENDL;
 	}
 	SetCurrentDirectory(utf8str_to_utf16str("StaticVFSDump").c_str());
 	gStaticVFS->dumpFiles();
@@ -4151,10 +4148,7 @@ void dumpVFSCaches()
 	res = LLFile::mkdir("VFSDump");
 	if (res == -1)
 	{
-		if (errno != EEXIST)
-		{
-			LL_WARNS() << "Couldn't create dir VFSDump" << LL_ENDL;
-		}
+		LL_WARNS() << "Couldn't create dir VFSDump" << LL_ENDL;
 	}
 	SetCurrentDirectory(utf8str_to_utf16str("VFSDump").c_str());
 	gVFS->dumpFiles();
diff --git a/indra/newview/llpresetsmanager.cpp b/indra/newview/llpresetsmanager.cpp
index 76d721ecdc44ddd90e3f27345e83fc005b983c29..96818d5a21ab839d861a285f73cd707c883951ff 100644
--- a/indra/newview/llpresetsmanager.cpp
+++ b/indra/newview/llpresetsmanager.cpp
@@ -78,16 +78,10 @@ std::string LLPresetsManager::getPresetsDir(const std::string& subdirectory)
 	std::string presets_path = gDirUtilp->getExpandedFilename(LL_PATH_PER_SL_ACCOUNT, PRESETS_DIR);
 	std::string full_path;
 
-	if (!gDirUtilp->fileExists(presets_path))
-	{
-		LLFile::mkdir(presets_path);
-	}
+	LLFile::mkdir(presets_path);
 
 	full_path = gDirUtilp->getExpandedFilename(LL_PATH_PER_SL_ACCOUNT, PRESETS_DIR, subdirectory);
-	if (!gDirUtilp->fileExists(full_path))
-	{
-		LLFile::mkdir(full_path);
-	}
+	LLFile::mkdir(full_path);
 
 	return full_path;
 }
diff --git a/indra/newview/lltexturecache.cpp b/indra/newview/lltexturecache.cpp
index 435d8333450c24c42d6085c9ae545936edca1e22..371da5d0a280c5802539cb1208dc359136b2d491 100644
--- a/indra/newview/lltexturecache.cpp
+++ b/indra/newview/lltexturecache.cpp
@@ -1054,11 +1054,11 @@ S64 LLTextureCache::initCache(ELLPath location, S64 max_size, BOOL texture_cache
 			return max_size ;
 		}
 	}
-	
+
 	if (!mReadOnly)
 	{
 		LLFile::mkdir(mTexturesDirName);
-		
+
 		const char* subdirs = "0123456789abcdef";
 		for (S32 i=0; i<16; i++)
 		{
@@ -1602,11 +1602,11 @@ void LLTextureCache::clearCorruptedCache()
 
 	closeHeaderEntriesFile();//close possible file handler
 	purgeAllTextures(false) ; //clear the cache.
-	
+
 	if (!mReadOnly) //regenerate the directory tree if not exists.
 	{
 		LLFile::mkdir(mTexturesDirName);
-		
+
 		const char* subdirs = "0123456789abcdef";
 		for (S32 i=0; i<16; i++)
 		{
diff --git a/indra/newview/skins/default/xui/en/mime_types.xml b/indra/newview/skins/default/xui/en/mime_types.xml
index 8a810f32a63c4ea981480500fffb9cd570d2045f..de9ac4247f079121b48649e90934499b0f8478d7 100644
--- a/indra/newview/skins/default/xui/en/mime_types.xml
+++ b/indra/newview/skins/default/xui/en/mime_types.xml
@@ -526,7 +526,7 @@
 			movie
 		</widgettype>
 		<impl>
-			media_plugin_cef
+			media_plugin_libvlc
 		</impl>
 	</mimetype>
 </mimetypes>
diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py
index 57539077fac941136eeb84b4fa5a6ce770a4bac8..e0c332681e3fcb2d45c5ffc6afe7f6231cb532f6 100755
--- a/indra/newview/viewer_manifest.py
+++ b/indra/newview/viewer_manifest.py
@@ -32,11 +32,13 @@
 import shutil
 import errno
 import json
+import plistlib
+import random
 import re
+import stat
+import subprocess
 import tarfile
 import time
-import random
-import subprocess
 
 viewer_dir = os.path.dirname(__file__)
 # Add indra/lib/python to our path so we don't have to muck with PYTHONPATH.
@@ -290,6 +292,129 @@ def extract_names(self,src):
         random.shuffle(names)
         return ', '.join(names)
 
+    def relsymlinkf(self, src, dst=None, catch=True):
+        """
+        relsymlinkf() is just like symlinkf(), but instead of requiring the
+        caller to pass 'src' as a relative pathname, this method expects 'src'
+        to be absolute, and creates a symlink whose target is the relative
+        path from 'src' to dirname(dst).
+        """
+        dstdir, dst = self._symlinkf_prep_dst(src, dst)
+
+        # Determine the relative path starting from the directory containing
+        # dst to the intended src.
+        src = self.relpath(src, dstdir)
+
+        self._symlinkf(src, dst, catch)
+        return dst
+
+    def symlinkf(self, src, dst=None, catch=True):
+        """
+        Like ln -sf, but uses os.symlink() instead of running ln. This creates
+        a symlink at 'dst' that points to 'src' -- see:
+        https://docs.python.org/2/library/os.html#os.symlink
+
+        If you omit 'dst', this creates a symlink with basename(src) at
+        get_dst_prefix() -- in other words: put a symlink to this pathname
+        here at the current dst prefix.
+
+        'src' must specifically be a *relative* symlink. It makes no sense to
+        create an absolute symlink pointing to some path on the build machine!
+
+        Also:
+        - We prepend 'dst' with the current get_dst_prefix(), so it has similar
+          meaning to associated self.path() calls.
+        - We ensure that the containing directory os.path.dirname(dst) exists
+          before attempting the symlink.
+
+        If you pass catch=False, exceptions will be propagated instead of
+        caught.
+        """
+        dstdir, dst = self._symlinkf_prep_dst(src, dst)
+        self._symlinkf(src, dst, catch)
+        return dst
+
+    def _symlinkf_prep_dst(self, src, dst):
+        # helper for relsymlinkf() and symlinkf()
+        if dst is None:
+            dst = os.path.basename(src)
+        dst = os.path.join(self.get_dst_prefix(), dst)
+        # Seems silly to prepend get_dst_prefix() to dst only to call
+        # os.path.dirname() on it again, but this works even when the passed
+        # 'dst' is itself a pathname.
+        dstdir = os.path.dirname(dst)
+        self.cmakedirs(dstdir)
+        return (dstdir, dst)
+
+    def _symlinkf(self, src, dst, catch):
+        # helper for relsymlinkf() and symlinkf()
+        # the passed src must be relative
+        if os.path.isabs(src):
+            raise ManifestError("Do not symlinkf(absolute %r, asis=True)" % src)
+
+        # The outer catch is the one that reports failure even after attempted
+        # recovery.
+        try:
+            # At the inner layer, recovery may be possible.
+            try:
+                os.symlink(src, dst)
+            except OSError as err:
+                if err.errno != errno.EEXIST:
+                    raise
+                # We could just blithely attempt to remove and recreate the target
+                # file, but that strategy doesn't work so well if we don't have
+                # permissions to remove it. Check to see if it's already the
+                # symlink we want, which is the usual reason for EEXIST.
+                elif os.path.islink(dst):
+                    if os.readlink(dst) == src:
+                        # the requested link already exists
+                        pass
+                    else:
+                        # dst is the wrong symlink; attempt to remove and recreate it
+                        os.remove(dst)
+                        os.symlink(src, dst)
+                elif os.path.isdir(dst):
+                    print "Requested symlink (%s) exists but is a directory; replacing" % dst
+                    shutil.rmtree(dst)
+                    os.symlink(src, dst)
+                elif os.path.exists(dst):
+                    print "Requested symlink (%s) exists but is a file; replacing" % dst
+                    os.remove(dst)
+                    os.symlink(src, dst)
+                else:
+                    # out of ideas
+                    raise
+        except Exception as err:
+            # report
+            print "Can't symlink %r -> %r: %s: %s" % \
+                  (dst, src, err.__class__.__name__, err)
+            # if caller asked us not to catch, re-raise this exception
+            if not catch:
+                raise
+
+    def relpath(self, path, base=None, symlink=False):
+        """
+        Return the relative path from 'base' to the passed 'path'. If base is
+        omitted, self.get_dst_prefix() is assumed. In other words: make a
+        same-name symlink to this path right here in the current dest prefix.
+
+        Normally we resolve symlinks. To retain symlinks, pass symlink=True.
+        """
+        if base is None:
+            base = self.get_dst_prefix()
+
+        # Since we use os.path.relpath() for this, which is purely textual, we
+        # must ensure that both pathnames are absolute.
+        if symlink:
+            # symlink=True means: we know path is (or indirects through) a
+            # symlink, don't resolve, we want to use the symlink.
+            abspath = os.path.abspath
+        else:
+            # symlink=False means to resolve any symlinks we may find
+            abspath = os.path.realpath
+
+        return os.path.relpath(abspath(path), abspath(base))
+
 
 class WindowsManifest(ViewerManifest):
     # We want the platform, per se, for every Windows build to be 'win'. The
@@ -308,6 +433,8 @@ def test_msvcrt_and_copy_action(self, src, dst):
         # This is used to test a dll manifest.
         # It is used as a temporary override during the construct method
         from test_win32_manifest import test_assembly_binding
+        # TODO: This is redundant with LLManifest.copy_action(). Why aren't we
+        # calling copy_action() in conjunction with test_assembly_binding()?
         if src and (os.path.exists(src) or os.path.islink(src)):
             # ensure that destination path exists
             self.cmakedirs(os.path.dirname(dst))
@@ -328,6 +455,8 @@ def test_for_no_msvcrt_manifest_and_copy_action(self, src, dst):
         # It is used as a temporary override during the construct method
         from test_win32_manifest import test_assembly_binding
         from test_win32_manifest import NoManifestException, NoMatchingAssemblyException
+        # TODO: This is redundant with LLManifest.copy_action(). Why aren't we
+        # calling copy_action() in conjunction with test_assembly_binding()?
         if src and (os.path.exists(src) or os.path.islink(src)):
             # ensure that destination path exists
             self.cmakedirs(os.path.dirname(dst))
@@ -356,18 +485,16 @@ def construct(self):
         pkgdir = os.path.join(self.args['build'], os.pardir, 'packages')
         relpkgdir = os.path.join(pkgdir, "lib", "release")
         debpkgdir = os.path.join(pkgdir, "lib", "debug")
-        vmpdir = os.path.join(pkgdir, "VMP")
-        llbasedir = os.path.join(pkgdir, "lib", "python", "llbase")
 
         if self.is_packaging_viewer():
             # 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())
 
-            # include the compiled launcher scripts so that it gets included in the file_list
-            self.path(src='%s/SL_Launcher.exe' % vmpdir, dst="SL_Launcher.exe")
-
-            #IUM is not normally executed directly, just imported.  No exe needed.
-            self.path2basename(vmpdir,"InstallerUserMessage.py")
+            with self.prefix(src=os.path.join(pkgdir, "VMP"), dst=""):
+                # 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")
@@ -378,12 +505,9 @@ def construct(self):
                 self.path("*.gif")
 
             #before, we only needed llbase at build time.  With VMP, we need it at run time.
-            llbase_path = os.path.join(self.get_dst_prefix(),'llbase')
-            if not os.path.exists(llbase_path):
-                os.makedirs(llbase_path)
-            with self.prefix(dst="llbase"):
-                self.path2basename(llbasedir,"*.py")
-                self.path2basename(llbasedir,"_cllsd.so")
+            with self.prefix(src=os.path.join(pkgdir, "lib", "python", "llbase"), dst="llbase"):
+                self.path("*.py")
+                self.path("_cllsd.so")
 
         # Plugin host application
         self.path2basename(os.path.join(os.pardir,
@@ -482,31 +606,19 @@ def construct(self):
                 self.path("media_plugin_example.dll")
 
         # CEF runtime files - debug
-        if self.args['configuration'].lower() == 'debug':
-            with self.prefix(src=os.path.join(os.pardir, 'packages', 'bin', 'debug'), 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")
-        else:
         # CEF runtime files - not debug (release, relwithdebinfo etc.)
-            with self.prefix(src=os.path.join(os.pardir, 'packages', 'bin', 'release'), 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")
+        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"):
@@ -514,7 +626,7 @@ def construct(self):
             self.path("msvcr120.dll")
 
         # CEF files common to all configurations
-        with self.prefix(src=os.path.join(os.pardir, 'packages', 'resources'), dst="llplugin"):
+        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")
@@ -522,7 +634,7 @@ def construct(self):
             self.path("devtools_resources.pak")
             self.path("icudtl.dat")
 
-        with self.prefix(src=os.path.join(os.pardir, 'packages', 'resources', 'locales'), dst=os.path.join('llplugin', 'locales')):
+        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")
@@ -577,10 +689,10 @@ def construct(self):
             self.path("zh-CN.pak")
             self.path("zh-TW.pak")
 
-            with self.prefix(src=os.path.join(os.pardir, 'packages', 'bin', 'release'), dst="llplugin"):
-                self.path("libvlc.dll")
-                self.path("libvlccore.dll")
-                self.path("plugins/")
+        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
         # tag:"crash-logger" here as a cue to the exporter
@@ -713,7 +825,7 @@ def package_finish(self):
         nsis_retry_wait=15
         for attempt in xrange(nsis_attempts):
             try:
-                self.run_command('"' + NSIS_path + '" /V2 ' + self.dst_path_of(tempfile))
+                self.run_command([NSIS_path, '/V2', self.dst_path_of(tempfile)])
             except ManifestError as err:
                 if attempt+1 < nsis_attempts:
                     print >> sys.stderr, "nsis failed, waiting %d seconds before retrying" % nsis_retry_wait
@@ -736,8 +848,7 @@ def sign(self, exe):
         if os.path.exists(sign_py):
             dst_path = self.dst_path_of(exe)
             print "about to run signing of: ", dst_path
-            self.run_command(' '.join((python, self.escape_slashes(sign_py),
-                                       self.escape_slashes(dst_path))))
+            self.run_command([python, sign_py, dst_path])
         else:
             print "Skipping code signing of %s: %s not found" % (exe, sign_py)
 
@@ -765,310 +876,392 @@ def is_packaging_viewer(self):
         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"
+
         # copy over the build result (this is a no-op if run within the xcode script)
-        self.path(self.args['configuration'] + "/Second Life.app", dst="")
+        self.path(os.path.join(self.args['configuration'], toplevel_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")
-        vmpdir = os.path.join(pkgdir, "VMP")
-        llbasedir = os.path.join(pkgdir, "lib", "python", "llbase")
-        requestsdir = os.path.join(pkgdir, "lib", "python", "requests")
-        urllib3dir = os.path.join(pkgdir, "lib", "python", "urllib3")
-        chardetdir = os.path.join(pkgdir, "lib", "python", "chardet")
-        idnadir = os.path.join(pkgdir, "lib", "python", "idna")
 
+        # -------------------- top-level Second Life.app ---------------------
+        # top-level Second Life application is only a container
         with self.prefix(src="", dst="Contents"):  # everything goes in Contents
-            self.path("Info.plist", dst="Info.plist")
-
-            # copy additional libs in <bundle>/Contents/MacOS/
-            self.path(os.path.join(relpkgdir, "libndofdev.dylib"), dst="Resources/libndofdev.dylib")
-            self.path(os.path.join(relpkgdir, "libhunspell-1.3.0.dylib"), dst="Resources/libhunspell-1.3.0.dylib")   
-
-            with self.prefix(dst="MacOS"):              
-                #this copies over the python wrapper script, associated utilities and required libraries, see SL-321, SL-322, SL-323
-                self.path2basename(vmpdir,"SL_Launcher")
-                self.path2basename(vmpdir,"*.py")
-                # certifi will be imported by requests; this is our custom version to get our ca-bundle.crt 
-                certifi_path = os.path.join(self.get_dst_prefix(),'certifi')
-                if not os.path.exists(certifi_path):
-                    os.makedirs(certifi_path)
-                with self.prefix(dst="certifi"):
-                    self.path2basename(os.path.join(vmpdir,"certifi"),"*")
-                # llbase provides our llrest service layer and llsd decoding
-                llbase_path = os.path.join(self.get_dst_prefix(),'llbase')
-                if not os.path.exists(llbase_path):
-                    os.makedirs(llbase_path)
-                with self.prefix(dst="llbase"):
-                    self.path2basename(llbasedir,"*.py")
-                    self.path2basename(llbasedir,"_cllsd.so")
-                #requests module needed by llbase/llrest.py
-                #this is only needed on POSIX, because in Windows we compile it into the EXE
-                requests_path = os.path.join(self.get_dst_prefix(),'requests')
-                if not os.path.exists(requests_path):
-                    os.makedirs(requests_path)
-                with self.prefix(dst="requests"):
-                    self.path2basename(requestsdir,"*")
-                urllib3_path = os.path.join(self.get_dst_prefix(),'urllib3')
-                if not os.path.exists(urllib3_path):
-                    os.makedirs(urllib3_path)
-                with self.prefix(dst="urllib3"):
-                    self.path2basename(urllib3dir,"*")
-                chardet_path = os.path.join(self.get_dst_prefix(),'chardet')
-                if not os.path.exists(chardet_path):
-                    os.makedirs(chardet_path)
-                with self.prefix(dst="chardet"):
-                    self.path2basename(chardetdir,"*")
-                idna_path = os.path.join(self.get_dst_prefix(),'idna')
-                if not os.path.exists(idna_path):
-                    os.makedirs(idna_path)
-                with self.prefix(dst="idna"):
-                    self.path2basename(idnadir,"*")
-
-            # most everything goes in the Resources directory
-            with self.prefix(src="", dst="Resources"):
-                super(DarwinManifest, self).construct()
-
-                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")
-                self.path("ca-bundle.crt")
-
-                icon_path = self.icon_path()
-                with self.prefix(src=icon_path, dst="") :
-                    self.path("secondlife.icns")
-
-                with self.prefix(src=icon_path, dst="vmp_icons"):
-                    self.path("secondlife.ico")
+            # 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
+            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"))
 
-                #VMP Tkinter icons
-                with self.prefix("vmp_icons"):
-                    self.path("*.png")
-                    self.path("*.gif")
-
-                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 = "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
-                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)
-
-                    # 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.
-                    resource_path = self.dst_path_of(os.path.join(app, "Contents", "Resources"))
-                    for libfile in dylibs:
-                        src = os.path.join(os.pardir, os.pardir, os.pardir, libfile)
-                        dst = os.path.join(resource_path, libfile)
-                        try:
-                            symlinkf(src, dst)
-                        except OSError as err:
-                            print "Can't symlink %s -> %s: %s" % (src, dst, err)
-
-                # Dullahan helper apps go inside SLPlugin.app
-                with self.prefix(src="", dst="SLPlugin.app/Contents/Frameworks"):
-                    helperappfile = 'DullahanHelper.app'
-                    self.path2basename(relpkgdir, helperappfile)
-
-                    pluginframeworkpath = self.dst_path_of('Chromium Embedded Framework.framework');
-                    # Putting a Frameworks directory under Contents/MacOS
-                    # isn't canonical, but the path baked into Dullahan
-                    # Helper.app/Contents/MacOS/DullahanHelper is:
-                    # @executable_path/Frameworks/Chromium Embedded Framework.framework/Chromium Embedded Framework
-                    # (notice, not @executable_path/../Frameworks/etc.)
-                    # So we'll create a symlink (below) from there back to the
-                    # Frameworks directory nested under SLPlugin.app.
-                    helperframeworkpath = \
-                        self.dst_path_of('DullahanHelper.app/Contents/MacOS/'
-                                         'Frameworks/Chromium Embedded Framework.framework')
-
-
-                helperexecutablepath = self.dst_path_of('SLPlugin.app/Contents/Frameworks/DullahanHelper.app/Contents/MacOS/DullahanHelper')
-                self.run_command('install_name_tool -change '
-                                 '"@rpath/Frameworks/Chromium Embedded Framework.framework/Chromium Embedded Framework" '
-                                 '"@executable_path/Frameworks/Chromium Embedded Framework.framework/Chromium Embedded Framework" "%s"' % helperexecutablepath)
-
-                # SLPlugin plugins
-                with self.prefix(src="", dst="llplugin"):
-                    self.path2basename("../media_plugins/cef/" + self.args['configuration'],
-                                       "media_plugin_cef.dylib")
-
-                    # copy LibVLC plugin itself
-                    self.path2basename("../media_plugins/libvlc/" + self.args['configuration'],
-                                       "media_plugin_libvlc.dylib")
-
-                    # copy LibVLC dynamic libraries
-                    with self.prefix(src=os.path.join(os.pardir, 'packages', 'lib', 'release' ), dst="lib"):
-                        self.path( "libvlc*.dylib*" )
-
-                    # copy LibVLC plugins folder
-                    with self.prefix(src=os.path.join(os.pardir, 'packages', 'lib', 'release', 'plugins' ), dst="lib"):
-                        self.path( "*.dylib" )
-                        self.path( "plugins.dat" )
-
-
-                # do this install_name_tool *after* media plugin is copied over
-                dylibexecutablepath = self.dst_path_of('llplugin/media_plugin_cef.dylib')
-                self.run_command('install_name_tool -change '
-                                 '"@rpath/Frameworks/Chromium Embedded Framework.framework/Chromium Embedded Framework" '
-                                 '"@executable_path/../Frameworks/Chromium Embedded Framework.framework/Chromium Embedded Framework" "%s"' % dylibexecutablepath)
-
-
-            # CEF framework goes inside Second Life.app/Contents/Frameworks
-            with self.prefix(src="", dst="Frameworks"):
-                frameworkfile="Chromium Embedded Framework.framework"
-                self.path2basename(relpkgdir, frameworkfile)
-
-            # This code constructs a relative path from the
-            # target framework folder back to the location of the symlink.
-            # 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 also call 'raise'
-            # to terminate the process if we get an error since without
-            # this symlink, Second Life web media can't possibly work.
-            # Real Framework folder:
-            #   Second Life.app/Contents/Frameworks/Chromium Embedded Framework.framework/
-            # Location of symlink and why it's relative 
-            #   Second Life.app/Contents/Resources/SLPlugin.app/Contents/Frameworks/Chromium Embedded Framework.framework/
-            # Real Frameworks folder, with the symlink inside the bundled SLPlugin.app (and why it's relative)
-            #   <top level>.app/Contents/Frameworks/Chromium Embedded Framework.framework/
-            #   <top level>.app/Contents/Resources/SLPlugin.app/Contents/Frameworks/Chromium Embedded Framework.framework ->
-            # It might seem simpler just to create a symlink Frameworks 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.
-            frameworkpath = os.path.join(os.pardir, os.pardir, os.pardir,
-                                         os.pardir, "Frameworks",
-                                         "Chromium Embedded Framework.framework")
-            try:
-                # from SLPlugin.app/Contents/Frameworks/Chromium Embedded
-                # Framework.framework back to Second
-                # Life.app/Contents/Frameworks/Chromium Embedded Framework.framework
-                origin, target = pluginframeworkpath, frameworkpath
-                symlinkf(target, origin)
-                # from SLPlugin.app/Contents/Frameworks/Dullahan
-                # Helper.app/Contents/MacOS/Frameworks/Chromium Embedded
-                # Framework.framework back to
-                # SLPlugin.app/Contents/Frameworks/Chromium Embedded Framework.framework
-                self.cmakedirs(os.path.dirname(helperframeworkpath))
-                origin = helperframeworkpath
-                target = os.path.join(os.pardir, frameworkpath)
-                symlinkf(target, origin)
-            except OSError as err:
-                print "Can't symlink %s -> %s: %s" % (origin, target, err)
-                raise
+            with self.prefix(src="", dst="Resources"):
+                # top-level Resources directory should be pretty sparse
+                # need .icns file referenced by top-level 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")
+                        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)])
 
+                            # copy LibVLC plugin itself
+                            self.path2basename("../media_plugins/libvlc/" + self.args['configuration'],
+                                               "media_plugin_libvlc.dylib")
 
-        # 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 %(viewer_binary)r' %
-                             { 'viewer_binary' : self.dst_path_of('Contents/MacOS/Second Life')})
+                            # 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
@@ -1084,9 +1277,10 @@ def package_finish(self):
         # make sure we don't have stale files laying about
         self.remove(sparsename, finalname)
 
-        self.run_command('hdiutil create %(sparse)r -volname %(vol)r -fs HFS+ -type SPARSE -megabytes 1300 -layout SPUD' % {
-                'sparse':sparsename,
-                'vol':volname})
+        self.run_command(['hdiutil', 'create', sparsename,
+                          '-volname', volname, '-fs', 'HFS+',
+                          '-type', 'SPARSE', '-megabytes', '1300',
+                          '-layout', 'SPUD'])
 
         # mount the image and get the name of the mount point and device node
         try:
@@ -1145,18 +1339,18 @@ def package_finish(self):
                 # well, possibly we've mistaken the nature of the problem. In any
                 # case, don't hang up the whole build looping indefinitely, let
                 # the original problem manifest by executing the desired command.
-                self.run_command('SetFile -a V %r' % pathname)
+                self.run_command(['SetFile', '-a', 'V', pathname])
 
             # Create the alias file (which is a resource file) from the .r
-            self.run_command('Rez %r -o %r' %
-                             (self.src_path_of("installers/darwin/release-dmg/Applications-alias.r"),
-                              os.path.join(volpath, "Applications")))
+            self.run_command(
+                ['Rez', self.src_path_of("installers/darwin/release-dmg/Applications-alias.r"),
+                 '-o', os.path.join(volpath, "Applications")])
 
             # Set the alias file's alias and custom icon bits
-            self.run_command('SetFile -a AC %r' % os.path.join(volpath, "Applications"))
+            self.run_command(['SetFile', '-a', 'AC', os.path.join(volpath, "Applications")])
 
             # Set the disk image root's custom icon bit
-            self.run_command('SetFile -a C %r' % volpath)
+            self.run_command(['SetFile', '-a', 'C', volpath])
 
             # Sign the app if requested; 
             # do this in the copy that's in the .dmg so that the extended attributes used by 
@@ -1192,7 +1386,10 @@ def package_finish(self):
                     #       and that it contains the correct cert/key. If a build host is set up with a clean version of macOS Sierra (or later)
                     #       then you will need to change this line (and the one for 'codesign' command below) to point to right place or else
                     #       pull in the cert/key into the default viewer keychain 'viewer.keychain-db' and export it to 'viewer.keychain'
-                    self.run_command('security unlock-keychain -p "%s" "%s/Library/Keychains/viewer.keychain"' % ( keychain_pwd, home_path ) )
+                    viewer_keychain = os.path.join(home_path, 'Library',
+                                                   'Keychains', 'viewer.keychain')
+                    self.run_command(['security', 'unlock-keychain',
+                                      '-p', keychain_pwd, viewer_keychain])
                     signed=False
                     sign_attempts=3
                     sign_retry_wait=15
@@ -1201,11 +1398,9 @@ def package_finish(self):
                             sign_attempts-=1;
                             self.run_command(
                                 # Note: See blurb above about names of keychains
-                               'codesign --verbose --deep --force --keychain "%(home_path)s/Library/Keychains/viewer.keychain" --sign %(identity)r %(bundle)r' % {
-                                   'home_path' : home_path,
-                                   'identity': identity,
-                                   'bundle': app_in_dmg
-                                   })
+                               ['codesign', '--verbose', '--deep', '--force',
+                                '--keychain', viewer_keychain, '--sign', identity,
+                                app_in_dmg])
                             signed=True # if no exception was raised, the codesign worked
                         except ManifestError as err:
                             if sign_attempts:
@@ -1215,18 +1410,19 @@ def package_finish(self):
                             else:
                                 print >> sys.stderr, "Maximum codesign attempts exceeded; giving up"
                                 raise
-                    self.run_command('spctl -a -texec -vv %(bundle)r' % { 'bundle': app_in_dmg })
+                    self.run_command(['spctl', '-a', '-texec', '-vv', app_in_dmg])
 
             imagename="SecondLife_" + '_'.join(self.args['version'])
 
 
         finally:
             # Unmount the image even if exceptions from any of the above 
-            self.run_command('hdiutil detach -force %r' % devfile)
+            self.run_command(['hdiutil', 'detach', '-force', devfile])
 
         print "Converting temp disk image to final disk image"
-        self.run_command('hdiutil convert %(sparse)r -format UDZO -imagekey zlib-level=9 -o %(final)r' % {'sparse':sparsename, 'final':finalname})
-        self.run_command('hdiutil internet-enable -yes %(final)r' % {'final':finalname})
+        self.run_command(['hdiutil', 'convert', sparsename, '-format', 'UDZO',
+                          '-imagekey', 'zlib-level=9', '-o', finalname])
+        self.run_command(['hdiutil', 'internet-enable', '-yes', finalname])
         # get rid of the temp file
         self.package_file = finalname
         self.remove(sparsename)
@@ -1273,18 +1469,15 @@ def construct(self):
             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
-            self.path2basename("../viewer_components/manager","SL_Launcher")
-            self.path2basename("../viewer_components/manager","*.py")
-            llbase_path = os.path.join(self.get_dst_prefix(),'llbase')
-            if not os.path.exists(llbase_path):
-                os.makedirs(llbase_path)
-            with self.prefix(dst="llbase"):
-                self.path2basename("../packages/lib/python/llbase","*.py")
-                self.path2basename("../packages/lib/python/llbase","_cllsd.so")         
-
-        with self.prefix("res-sdl"):
-            self.path("*")
-            # recurse
+            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")
 
         # Get the icons based on the channel type
         icon_path = self.icon_path()
@@ -1295,15 +1488,16 @@ def construct(self):
                 self.path("secondlife_256.BMP","ll_icon.BMP")
 
         # plugins
-        with self.prefix(src="", dst="bin/llplugin"):
-            self.path("../media_plugins/gstreamer010/libmedia_plugin_gstreamer010.so", "libmedia_plugin_gstreamer.so")
-            self.path("../media_plugins/libvlc/libmedia_plugin_libvlc.so", "libmedia_plugin_libvlc.so")
+        with self.prefix(src="../media_plugins", dst="bin/llplugin"):
+            self.path("gstreamer010/libmedia_plugin_gstreamer010.so",
+                      "libmedia_plugin_gstreamer.so")
+            self.path2basename("libvlc", "libmedia_plugin_libvlc.so")
 
-        with self.prefix(src=os.path.join(os.pardir, 'packages', 'lib', 'vlc', 'plugins'), dst="bin/llplugin/vlc/plugins"):
+        with self.prefix(src=os.path.join(pkgdir, 'lib', 'vlc', 'plugins'), dst="bin/llplugin/vlc/plugins"):
             self.path( "plugins.dat" )
             self.path( "*/*.so" )
 
-        with self.prefix(src=os.path.join(os.pardir, 'packages', 'lib' ), dst="lib"):
+        with self.prefix(src=os.path.join(pkgdir, 'lib' ), dst="lib"):
             self.path( "libvlc*.so*" )
 
         # llcommon
@@ -1319,43 +1513,42 @@ def package_finish(self):
         self.strip_binaries()
 
         # Fix access permissions
-        self.run_command("""
-                find %(dst)s -type d | xargs --no-run-if-empty chmod 755;
-                find %(dst)s -type f -perm 0700 | xargs --no-run-if-empty chmod 0755;
-                find %(dst)s -type f -perm 0500 | xargs --no-run-if-empty chmod 0555;
-                find %(dst)s -type f -perm 0600 | xargs --no-run-if-empty chmod 0644;
-                find %(dst)s -type f -perm 0400 | xargs --no-run-if-empty chmod 0444;
-                true""" %  {'dst':self.get_dst_prefix() })
+        self.run_command(['find', self.get_dst_prefix(),
+                          '-type', 'd', '-exec', 'chmod', '755', '{}', ';'])
+        for old, new in ('0700', '0755'), ('0500', '0555'), ('0600', '0644'), ('0400', '0444'):
+            self.run_command(['find', self.get_dst_prefix(),
+                              '-type', 'f', '-perm', old,
+                              '-exec', 'chmod', new, '{}', ';'])
         self.package_file = installer_name + '.tar.bz2'
 
         # temporarily move directory tree so that it has the right
         # name in the tarfile
-        self.run_command("mv %(dst)s %(inst)s" % {
-            'dst': self.get_dst_prefix(),
-            'inst': self.build_path_of(installer_name)})
+        realname = self.get_dst_prefix()
+        tempname = self.build_path_of(installer_name)
+        self.run_command(["mv", realname, tempname])
         try:
             # only create tarball if it's a release build.
             if self.args['buildtype'].lower() == 'release':
                 # --numeric-owner hides the username of the builder for
                 # security etc.
-                self.run_command('tar -C %(dir)s --numeric-owner -cjf '
-                                 '%(inst_path)s.tar.bz2 %(inst_name)s' % {
-                        'dir': self.get_build_prefix(),
-                        'inst_name': installer_name,
-                        'inst_path':self.build_path_of(installer_name)})
+                self.run_command(['tar', '-C', self.get_build_prefix(),
+                                  '--numeric-owner', '-cjf',
+                                 tempname + '.tar.bz2', installer_name])
             else:
                 print "Skipping %s.tar.bz2 for non-Release build (%s)" % \
                       (installer_name, self.args['buildtype'])
         finally:
-            self.run_command("mv %(inst)s %(dst)s" % {
-                'dst': self.get_dst_prefix(),
-                'inst': self.build_path_of(installer_name)})
+            self.run_command(["mv", tempname, realname])
 
     def strip_binaries(self):
         if self.args['buildtype'].lower() == 'release' and self.is_packaging_viewer():
             print "* Going strip-crazy on the packaged binaries, since this is a RELEASE build"
             # makes some small assumptions about our packaged dir structure
-            self.run_command(r"find %(d)r/bin %(d)r/lib -type f \! -name \*.py \! -name SL_Launcher \! -name update_install | xargs --no-run-if-empty strip -S" % {'d': self.get_dst_prefix()} ) 
+            self.run_command(
+                ["find"] +
+                [os.path.join(self.get_dst_prefix(), dir) for dir in ('bin', 'lib')] +
+                ['-type', 'f', '!', '-name', '*.py', '!', '-name', 'SL_Launcher',
+                 '!', '-name', 'update_install', '-exec', 'strip', '-S', '{}', ';'])
 
 class Linux_i686_Manifest(LinuxManifest):
     address_size = 32
@@ -1452,46 +1645,5 @@ def construct(self):
 
 ################################################################
 
-def symlinkf(src, dst):
-    """
-    Like ln -sf, but uses os.symlink() instead of running ln.
-    """
-    try:
-        os.symlink(src, dst)
-    except OSError as err:
-        if err.errno != errno.EEXIST:
-            raise
-        # We could just blithely attempt to remove and recreate the target
-        # file, but that strategy doesn't work so well if we don't have
-        # permissions to remove it. Check to see if it's already the
-        # symlink we want, which is the usual reason for EEXIST.
-        elif os.path.islink(dst):
-            if os.readlink(dst) == src:
-                # the requested link already exists
-                pass
-            else:
-                # dst is the wrong symlink; attempt to remove and recreate it
-                os.remove(dst)
-                os.symlink(src, dst)
-        elif os.path.isdir(dst):
-            print "Requested symlink (%s) exists but is a directory; replacing" % dst
-            shutil.rmtree(dst)
-            os.symlink(src, dst)
-        elif os.path.exists(dst):
-            print "Requested symlink (%s) exists but is a file; replacing" % dst
-            os.remove(dst)
-            os.symlink(src, dst)
-        else:
-            # see if the problem is that the parent directory does not exist
-            # and try to explain what is missing
-            (parent, tail) = os.path.split(dst)
-            while not os.path.exists(parent):
-                (parent, tail) = os.path.split(parent)
-            if tail:
-                raise Exception("Requested symlink (%s) cannot be created because %s does not exist"
-                                % os.path.join(parent, tail))
-            else:
-                raise
-
 if __name__ == "__main__":
     main()