diff --git a/.gitignore b/.gitignore
index 5a23b64d69db1779f9aaffde3e388bc1e748aaef..1ecd8c1009f808a0765481c86d66e2f4b393d14f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -47,6 +47,10 @@ pcviewer.cfg
 *_resource.rc
 .#*
 *.*#
+*_wrapper.sh
+*_wrapper.bat
+wrapper.sh
+wrapper.bat
 core
 .qmake.cache
 .qmake.vars
diff --git a/mkspecs/features/qt_functions.prf b/mkspecs/features/qt_functions.prf
index 00f4bdf93e8171df3279656d08efbd6758271b8c..abb7439f1818e252ad959fadc74f40c76111e0bf 100644
--- a/mkspecs/features/qt_functions.prf
+++ b/mkspecs/features/qt_functions.prf
@@ -70,6 +70,7 @@ defineTest(qtPrepareTool) {
         }
     }
     QT_TOOL_ENV += $$eval(QT_TOOL.$${2}.envvars)
+    QT_TOOL_NAME = $$2
     !isEmpty(3)|!isEmpty(4) {
         $$1$$3 =
         for (arg, cmd): \
@@ -90,6 +91,7 @@ defineTest(qtAddToolEnv) {
         ds = $$QMAKE_DIR_SEP
     else: \
         ds = $$DIR_SEPARATOR
+    batch_sets =
     for(env, 2) {
         value = $$eval($${env}.value)
         !isEmpty(value) {
@@ -97,22 +99,61 @@ defineTest(qtAddToolEnv) {
             equals(ds, /) {
                 contains($${env}.CONFIG, prepend): infix = \${$$name:+:\$$$name}
                 else: infix =
-                val = "$$name=$$shell_quote($$join(value, :))$$infix"
+                # Under msys, this path is taken only in the non-system()
+                # case, so using shell_quote() always works.
+                batch_sets += \
+                    "$$name=$$shell_quote($$join(value, :))$$infix" \
+                    "export $$name"
             } else {
-                # Escape closing parens when expanding the variable, otherwise cmd confuses itself.
-                contains($${env}.CONFIG, prepend): infix = ;%$$name:)=^)%
-                else: infix =
+                value ~= s,\\^,^^^^,g
+                value ~= s,!,^^!,g
                 value ~= s,\\),^),g
-                val = "(set $$name=$$join(value, ;)$$infix) &"
-            }
-            isEmpty(3): !contains(TEMPLATE, vc.*) {
-                contains(MAKEFILE_GENERATOR, MS.*): val ~= s,%,%%,g
-                val ~= s,\\\$,\$\$,g
+                contains($${env}.CONFIG, prepend) {
+                    batch_sets += \
+                        "if defined $$name (" \
+                        "    set $$name=$$join(value, ;);!$$name!" \
+                        ") else (" \
+                        "    set $$name=$$join(value, ;)" \
+                        ")"
+                } else {
+                    batch_sets += "(set $$name=$$join(value, ;))"
+                }
             }
-            $$1 = "$$val $$eval($$1)"
         }
     }
+    !isEmpty(batch_sets) {
+        batch_name = wrapper
+        !isEmpty(QT_TOOL_NAME): batch_name = $${QT_TOOL_NAME}_wrapper
+        cmd = $$eval($$1)
+        !isEmpty(cmd): cmd = "$$cmd "
+        equals(ds, /) {
+            batch_name = $${batch_name}.sh
+            batch_cont = \
+                "$$LITERAL_HASH!/bin/sh" \
+                $$batch_sets \
+                "exec $$cmd\"$@\""
+            # It would be nicer to use the '.' command (without 'exec' above),
+            # but that doesn't set the positional arguments under (d)ash.
+            $$1 =
+        } else {
+            batch_name = $${batch_name}.bat
+            batch_cont = \
+                "@echo off" \
+                "SetLocal EnableDelayedExpansion" \
+                $$batch_sets \
+                "$$cmd%*" \
+                "EndLocal"
+            $$1 = call
+        }
+        !build_pass:!write_file($$OUT_PWD/$$batch_name, batch_cont, exe): error("Aborting.")
+        isEmpty(3): \
+            $$1 += $$shell_quote($$shell_path($$OUT_PWD/$$batch_name))
+        else: \
+            $$1 += $$system_quote($$system_path($$OUT_PWD/$$batch_name))
+        QMAKE_DISTCLEAN += $$OUT_PWD/$$batch_name
+    }
     export($$1)
+    export(QMAKE_DISTCLEAN)
 }
 
 # target variable, dependency var name, [non-empty: prepare for system(), not make]
diff --git a/mkspecs/features/testcase.prf b/mkspecs/features/testcase.prf
index 6bac0546c32da8295f3c7e0dd310941a284ebe33..5ad372f976ce3d7597c8eddcefee76eef832e279 100644
--- a/mkspecs/features/testcase.prf
+++ b/mkspecs/features/testcase.prf
@@ -9,6 +9,11 @@ testcase_exceptions: CONFIG += exceptions
 check.files                =
 check.path                 = .
 
+# Add environment for non-installed builds. Do this first, so the
+# 'make' variable expansions don't end up in a batch file/script.
+QT_TOOL_NAME = target
+qtAddTargetEnv(check.commands, QT)
+
 # If the test ends up in a different directory, we should cd to that directory.
 TESTRUN_CWD = $$DESTDIR
 
@@ -40,10 +45,6 @@ unix {
 # Allow for custom arguments to tests
 check.commands += $(TESTARGS)
 
-# Add environment for non-installed builds
-qtAddTargetEnv(check.commands, QT)
-
-# This must happen after adding the environment.
 !isEmpty(TESTRUN_CWD):!contains(TESTRUN_CWD, ^\\./?): \
     check.commands = cd $$shell_path($$TESTRUN_CWD) && $$check.commands
 
diff --git a/qmake/generators/win32/msvc_objectmodel.cpp b/qmake/generators/win32/msvc_objectmodel.cpp
index 339dae953aea8641ea2c5df8030fe78a5fff913a..18457ac5ad3db2b5d9f5039a4cc046e86d6d511c 100644
--- a/qmake/generators/win32/msvc_objectmodel.cpp
+++ b/qmake/generators/win32/msvc_objectmodel.cpp
@@ -2399,11 +2399,7 @@ bool VCFilter::addExtraCompiler(const VCFilterFile &info)
         if (!CustomBuildTool.Description.isEmpty())
             CustomBuildTool.Description += ", ";
         CustomBuildTool.Description += cmd_name;
-        // Execute custom build steps in an environment variable scope to prevent unwanted
-        // side effects for downstream build steps
-        CustomBuildTool.CommandLine += QLatin1String("setlocal");
         CustomBuildTool.CommandLine += VCToolBase::fixCommandLine(cmd.trimmed());
-        CustomBuildTool.CommandLine += QLatin1String("endlocal");
         int space = cmd.indexOf(' ');
         QFileInfo finf(cmd.left(space));
         if (CustomBuildTool.ToolPath.isEmpty())