diff --git a/require-ess/tools/driver.makefile b/require-ess/tools/driver.makefile
index f38bce8e07d47ffe1285ea7c3f9b13cbd07d17f5..89cfa6acc6aee603f67d5ca2241b9e4b31e7f9ad 100644
--- a/require-ess/tools/driver.makefile
+++ b/require-ess/tools/driver.makefile
@@ -274,21 +274,26 @@ debug::
 	@echo "EXCLUDE_ARCHS = ${EXCLUDE_ARCHS}"
 	@echo "LIBVERSION = ${LIBVERSION}"
 
-# Loop over all architectures.
-install build debug:: $(COMMON_DIR)
-	@+failed_builds=0; \
-	for ARCH in ${BUILD_ARCHS}; do \
-	    umask 002; echo MAKING ARCH $$ARCH; ${MAKE} -f ${USERMAKEFILE} T_A=$$ARCH $@ || ((failed_builds++)); \
-	done; \
-	((failed_builds == 0))
+# Create e.g. build-$(T_A) rules for each architecture, so that we can just do
+#   build: build-arch1 build-arch2
+define target_rule
+$1-%: | $(COMMON_DIR)
+	$${MAKE} -f $${USERMAKEFILE} T_A=$$* $1
+endef
+$(foreach target,install build debug,$(eval $(call target_rule,$(target))))
+
+.SECONDEXPANSION:
 
 # This has to fit under .SECONDEXPANSION in order to catch TMPS and SUBS, which are typically defined
 # _after_ driver.makefile is included.
-.SECONDEXPANSION:
 db_internal: $$(addprefix $(COMMON_DIR)/,$$(notdir $$(patsubst %.template,%.db,$$(TMPS))))
 
 db_internal: $$(addprefix $(COMMON_DIR)/,$$(notdir $$(patsubst %.substitutions,%.db,$$(SUBS))))
 
+# This has to be after .SECONDEXPANSION since BUILD_ARCHS will be modified based on EXCLUDE_ARCHS
+# and ARCH_FILTER, which are defined _after_ driver.makefile.
+$(foreach target,install build debug,$(eval $(target):: $$$$(foreach arch,$$$${BUILD_ARCHS},$(target)-$$$${arch})))
+
 else # T_A
 
 ifeq ($(filter O.%,$(notdir ${CURDIR})),)
diff --git a/tests/conftest.py b/tests/conftest.py
index 1f9a531387023a28ee1af2eb6023151f14d56c44..7fdf70eba5fdda19367bfd27456624e5dca2b40c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -69,7 +69,7 @@ include $(E3_REQUIRE_TOOLS)/driver.makefile
 
     def add_var_to_config_module(self, makefile_var: str, value: str, modifier="+"):
         with open(self.config_module, "a") as f:
-            f.write(f"{makefile_var} {modifier}= {value}\n")
+            f.write(f"export {makefile_var} {modifier}= {value}\n")
 
     def add_var_to_makefile(self, makefile_var: str, value: str, modifier="+"):
         with open(self.makefile, "a") as f:
diff --git a/tests/test_build.py b/tests/test_build.py
index d599658cc22db464caf9ef33cbe039578089206b..580ceff8282b0a2e6f690d60fc4652654db79243 100644
--- a/tests/test_build.py
+++ b/tests/test_build.py
@@ -533,3 +533,35 @@ def test_arch_filter(wrapper, installed_archs, param, expected):
     build_archs = [arch for arch in arch_regex.findall(o) if arch != host_arch]
 
     assert build_archs == expected
+
+
+@pytest.mark.parametrize("archs,failing_arch", [("foo bar", "foo"), ("foo bar", "bar")])
+def test_build_fails_if_nth_architecture_fails(wrapper, archs, failing_arch):
+    # LIBOBJS is determined in part based on configuration data coming from
+    # $(CONFIG)/os/CONFIG.Common.$(T_A); since our architectures do not actually
+    # exist, we need to manually define these /before/ driver.makefile is included.
+    makefile_content = wrapper.makefile.read_text()
+    with open(wrapper.makefile, "w") as f:
+        f.write(
+            f"""ifeq ($(T_A),{failing_arch})
+                LIBOBJS = nonexistent_{failing_arch}.o
+                endif
+                """
+        )
+        f.write(makefile_content)
+
+    wrapper.add_var_to_config_module("OS_CLASS", "Linux")
+
+    # Skip the host architecture, we are not testing it.
+    host_arch = os.getenv("EPICS_HOST_ARCH")
+    wrapper.add_var_to_makefile("EXCLUDE_ARCHS", host_arch)
+
+    wrapper.add_var_to_makefile("CROSS_COMPILER_TARGET_ARCHS", archs, modifier="")
+    wrapper.add_var_to_makefile("SOURCES", "-none-")
+
+    rc, _, errs = wrapper.run_make("build")
+    assert rc == 2
+    assert re.search(
+        RE_MISSING_FILE.format(filename=re.escape(f"nonexistent_{failing_arch}.o")),
+        errs,
+    )