Repository: beeware/briefcase

Issue:
XCode error when physical device is attached
If a locked physical iOS device is attached to your computer when building an iOS app, a stack trace is raised.

If the device is *unlocked*, the error doesn't occur.

Full stack trace:
```
[hello-world] Building XCode project...

2020-04-23 17:07:26.815 xcodebuild[53855:6550590]  DTDeviceKit: deviceType from 00008030-0006585E2EBA802E was NULL
2020-04-23 17:07:27.089 xcodebuild[53855:6550590]  DTDeviceKit: deviceType from 00008030-0006585E2EBA802E was NULL
2020-04-23 17:07:27.882 xcodebuild[53855:6550588]  DTDeviceKit: Failed to mount DDI from '<DVTFilePath:0x7f95b52e5c60:'/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/13.2/DeveloperDiskImage.dmg'>', error=-402652958: Error Domain=com.apple.dtdevicekit Code=-402652958 "Development cannot be enabled while your device is locked." UserInfo={NSLocalizedFailureReason=Please unlock your device and reattach. (0xE80000E2)., com.apple.dtdevicekit.stacktrace=(
	0   DTDeviceKitBase                     0x000000011b8ee6e7 DTDKCreateNSError + 109
	1   DTDeviceKitBase                     0x000000011b8eede9 DTDK_AMDErrorToNSError + 792
	2   DTDeviceKitBase                     0x000000011b92c780 -[DTDKMobileDeviceToken _mountDeveloperDiskImage:withError:] + 844
	3   DTDeviceKitBase                     0x000000011b92d29d __58-[DTDKMobileDeviceToken mountDeveloperDiskImageWithError:]_block_invoke + 699
	4   libdispatch.dylib                   0x00007fff7802463d _dispatch_client_callout + 8
	5   libdispatch.dylib                   0x00007fff78030129 _dispatch_lane_barrier_sync_invoke_and_complete + 60
	6   DTDeviceKitBase                     0x000000011b92cf80 -[DTDKMobileDeviceToken mountDeveloperDiskImageWithError:] + 199
	7   IDEiOSSupportCore                   0x000000011b4f72f1 __37-[DVTiOSDevice(Connect) hasConnected]_block_invoke_2 + 267
	8   DVTFoundation                       0x000000010bc7be7a __DVT_CALLING_CLIENT_BLOCK__ + 7
	9   DVTFoundation                       0x000000010bc7fb51 __DVTDispatchGroupAsync_block_invoke + 809
	10  libdispatch.dylib                   0x00007fff780235f8 _dispatch_call_block_and_release + 12
	11  libdispatch.dylib                   0x00007fff7802463d _dispatch_client_callout + 8
	12  libdispatch.dylib                   0x00007fff78026e5e _dispatch_continuation_pop + 534
	13  libdispatch.dylib                   0x00007fff780264a3 _dispatch_async_redirect_invoke + 703
	14  libdispatch.dylib                   0x00007fff780323bc _dispatch_root_queue_drain + 324
	15  libdispatch.dylib                   0x00007fff78032b46 _dispatch_worker_thread2 + 90
	16  libsystem_pthread.dylib             0x00007fff782646b3 _pthread_wqthread + 583
	17  libsystem_pthread.dylib             0x00007fff782643fd start_wqthread + 13
), DVTRadarComponentKey=487927, NSLocalizedDescription=Development cannot be enabled while your device is locked.}
2020-04-23 17:07:27.883 xcodebuild[53855:6550588]  iPhoneConnect: 📱<DVTiOSDevice (0x7f95b5257520), Russell’s iPhone, iPhone, 13.3.1 (17D50), 00008030-0006585E2EBA802E> unable to mount developer disk image, (Error Domain=com.apple.dtdevicekit Code=-402652958 "Development cannot be enabled while your device is locked." UserInfo={NSLocalizedFailureReason=Please unlock your device and reattach. (0xE80000E2)., com.apple.dtdevicekit.stacktrace=(
	0   DTDeviceKitBase                     0x000000011b8ee6e7 DTDKCreateNSError + 109
	1   DTDeviceKitBase                     0x000000011b8eede9 DTDK_AMDErrorToNSError + 792
	2   DTDeviceKitBase                     0x000000011b92c780 -[DTDKMobileDeviceToken _mountDeveloperDiskImage:withError:] + 844
	3   DTDeviceKitBase                     0x000000011b92d29d __58-[DTDKMobileDeviceToken mountDeveloperDiskImageWithError:]_block_invoke + 699
	4   libdispatch.dylib                   0x00007fff7802463d _dispatch_client_callout + 8
	5   libdispatch.dylib                   0x00007fff78030129 _dispatch_lane_barrier_sync_invoke_and_complete + 60
	6   DTDeviceKitBase                     0x000000011b92cf80 -[DTDKMobileDeviceToken mountDeveloperDiskImageWithError:] + 199
	7   IDEiOSSupportCore                   0x000000011b4f72f1 __37-[DVTiOSDevice(Connect) hasConnected]_block_invoke_2 + 267
	8   DVTFoundation                       0x000000010bc7be7a __DVT_CALLING_CLIENT_BLOCK__ + 7
	9   DVTFoundation                       0x000000010bc7fb51 __DVTDispatchGroupAsync_block_invoke + 809
	10  libdispatch.dylib                   0x00007fff780235f8 _dispatch_call_block_and_release + 12
	11  libdispatch.dylib                   0x00007fff7802463d _dispatch_client_callout + 8
	12  libdispatch.dylib                   0x00007fff78026e5e _dispatch_continuation_pop + 534
	13  libdispatch.dylib                   0x00007fff780264a3 _dispatch_async_redirect_invoke + 703
	14  libdispatch.dylib                   0x00007fff780323bc _dispatch_root_queue_drain + 324
	15  libdispatch.dylib                   0x00007fff78032b46 _dispatch_worker_thread2 + 90
	16  libsystem_pthread.dylib             0x00007fff782646b3 _pthread_wqthread + 583
	17  libsystem_pthread.dylib             0x00007fff782643fd start_wqthread + 13
), DVTRadarComponentKey=487927, NSLocalizedDescription=Development cannot be enabled while your device is locked.}) {
    DVTRadarComponentKey = 487927;
    NSLocalizedDescription = "Development cannot be enabled while your device is locked.";
    NSLocalizedFailureReason = "Please unlock your device and reattach. (0xE80000E2).";
    "com.apple.dtdevicekit.stacktrace" = (
	0   DTDeviceKitBase                     0x000000011b8ee6e7 DTDKCreateNSError + 109
	1   DTDeviceKitBase                     0x000000011b8eede9 DTDK_AMDErrorToNSError + 792
	2   DTDeviceKitBase                     0x000000011b92c780 -[DTDKMobileDeviceToken _mountDeveloperDiskImage:withError:] + 844
	3   DTDeviceKitBase                     0x000000011b92d29d __58-[DTDKMobileDeviceToken mountDeveloperDiskImageWithError:]_block_invoke + 699
	4   libdispatch.dylib                   0x00007fff7802463d _dispatch_client_callout + 8
	5   libdispatch.dylib                   0x00007fff78030129 _dispatch_lane_barrier_sync_invoke_and_complete + 60
	6   DTDeviceKitBase                     0x000000011b92cf80 -[DTDKMobileDeviceToken mountDeveloperDiskImageWithError:] + 199
	7   IDEiOSSupportCore                   0x000000011b4f72f1 __37-[DVTiOSDevice(Connect) hasConnected]_block_invoke_2 + 267
	8   DVTFoundation                       0x000000010bc7be7a __DVT_CALLING_CLIENT_BLOCK__ + 7
	9   DVTFoundation                       0x000000010bc7fb51 __DVTDispatchGroupAsync_block_invoke + 809
	10  libdispatch.dylib                   0x00007fff780235f8 _dispatch_call_block_and_release + 12
	11  libdispatch.dylib                   0x00007fff7802463d _dispatch_client_callout + 8
	12  libdispatch.dylib                   0x00007fff78026e5e _dispatch_continuation_pop + 534
	13  libdispatch.dylib                   0x00007fff780264a3 _dispatch_async_redirect_invoke + 703
	14  libdispatch.dylib                   0x00007fff780323bc _dispatch_root_queue_drain + 324
	15  libdispatch.dylib                   0x00007fff78032b46 _dispatch_worker_thread2 + 90
	16  libsystem_pthread.dylib             0x00007fff782646b3 _pthread_wqthread + 583
	17  libsystem_pthread.dylib             0x00007fff782643fd start_wqthread + 13
);
}
2020-04-23 17:07:27.957 xcodebuild[53855:6550568] [MT] iPhoneConnect: 📱<DVTiOSDevice (0x7f95b5257520), Russell’s iPhone, iPhone, 13.3.1 (17D50), 00008030-0006585E2EBA802E> -- An error occurred whilst preparing device for development -- Development cannot be enabled while your device is locked.
Domain: com.apple.dtdevicekit
Code: -402652958
Failure Reason: Please unlock your device and reattach. (0xE80000E2).
User Info: {
    DVTRadarComponentKey = 487927;
    "com.apple.dtdevicekit.stacktrace" = (
	0   DTDeviceKitBase                     0x000000011b8ee6e7 DTDKCreateNSError + 109
	1   DTDeviceKitBase                     0x000000011b8eede9 DTDK_AMDErrorToNSError + 792
	2   DTDeviceKitBase                     0x000000011b92c780 -[DTDKMobileDeviceToken _mountDeveloperDiskImage:withError:] + 844
	3   DTDeviceKitBase                     0x000000011b92d29d __58-[DTDKMobileDeviceToken mountDeveloperDiskImageWithError:]_block_invoke + 699
	4   libdispatch.dylib                   0x00007fff7802463d _dispatch_client_callout + 8
	5   libdispatch.dylib                   0x00007fff78030129 _dispatch_lane_barrier_sync_invoke_and_complete + 60
	6   DTDeviceKitBase                     0x000000011b92cf80 -[DTDKMobileDeviceToken mountDeveloperDiskImageWithError:] + 199
	7   IDEiOSSupportCore                   0x000000011b4f72f1 __37-[DVTiOSDevice(Connect) hasConnected]_block_invoke_2 + 267
	8   DVTFoundation                       0x000000010bc7be7a __DVT_CALLING_CLIENT_BLOCK__ + 7
	9   DVTFoundation                       0x000000010bc7fb51 __DVTDispatchGroupAsync_block_invoke + 809
	10  libdispatch.dylib                   0x00007fff780235f8 _dispatch_call_block_and_release + 12
	11  libdispatch.dylib                   0x00007fff7802463d _dispatch_client_callout + 8
	12  libdispatch.dylib                   0x00007fff78026e5e _dispatch_continuation_pop + 534
	13  libdispatch.dylib                   0x00007fff780264a3 _dispatch_async_redirect_invoke + 703
	14  libdispatch.dylib                   0x00007fff780323bc _dispatch_root_queue_drain + 324
	15  libdispatch.dylib                   0x00007fff78032b46 _dispatch_worker_thread2 + 90
	16  libsystem_pthread.dylib             0x00007fff782646b3 _pthread_wqthread + 583
	17  libsystem_pthread.dylib             0x00007fff782643fd start_wqthread + 13
);
}
--
2020-04-23 17:07:27.958 xcodebuild[53855:6550568] [MT] iPhoneConnect: 📱<DVTiOSDevice (0x7f95b5257520), Russell’s iPhone, iPhone, 13.3.1 (17D50), 00008030-0006585E2EBA802E> == Underlying device preparation errors ==
2020-04-23 17:07:27.958 xcodebuild[53855:6550568] [MT] iPhoneConnect: Development cannot be enabled while your device is locked.
Domain: com.apple.dtdevicekit
Code: -402652958
Failure Reason: Please unlock your device and reattach. (0xE80000E2).
User Info: {
    DVTRadarComponentKey = 487927;
    "com.apple.dtdevicekit.stacktrace" = (
	0   DTDeviceKitBase                     0x000000011b8ee6e7 DTDKCreateNSError + 109
	1   DTDeviceKitBase                     0x000000011b8eede9 DTDK_AMDErrorToNSError + 792
	2   DTDeviceKitBase                     0x000000011b92c780 -[DTDKMobileDeviceToken _mountDeveloperDiskImage:withError:] + 844
	3   DTDeviceKitBase                     0x000000011b92d29d __58-[DTDKMobileDeviceToken mountDeveloperDiskImageWithError:]_block_invoke + 699
	4   libdispatch.dylib                   0x00007fff7802463d _dispatch_client_callout + 8
	5   libdispatch.dylib                   0x00007fff78030129 _dispatch_lane_barrier_sync_invoke_and_complete + 60
	6   DTDeviceKitBase                     0x000000011b92cf80 -[DTDKMobileDeviceToken mountDeveloperDiskImageWithError:] + 199
	7   IDEiOSSupportCore                   0x000000011b4f72f1 __37-[DVTiOSDevice(Connect) hasConnected]_block_invoke_2 + 267
	8   DVTFoundation                       0x000000010bc7be7a __DVT_CALLING_CLIENT_BLOCK__ + 7
	9   DVTFoundation                       0x000000010bc7fb51 __DVTDispatchGroupAsync_block_invoke + 809
	10  libdispatch.dylib                   0x00007fff780235f8 _dispatch_call_block_and_release + 12
	11  libdispatch.dylib                   0x00007fff7802463d _dispatch_client_callout + 8
	12  libdispatch.dylib                   0x00007fff78026e5e _dispatch_continuation_pop + 534
	13  libdispatch.dylib                   0x00007fff780264a3 _dispatch_async_redirect_invoke + 703
	14  libdispatch.dylib                   0x00007fff780323bc _dispatch_root_queue_drain + 324
	15  libdispatch.dylib                   0x00007fff78032b46 _dispatch_worker_thread2 + 90
	16  libsystem_pthread.dylib             0x00007fff782646b3 _pthread_wqthread + 583
	17  libsystem_pthread.dylib             0x00007fff782643fd start_wqthread + 13
);
}
--
2020-04-23 17:07:27.958 xcodebuild[53855:6550568] [MT] iPhoneConnect: Development cannot be enabled while your device is locked.
Domain: com.apple.dtdevicekit
Code: -402652958
Failure Reason: Please unlock your device and reattach. (0xE80000E2).
User Info: {
    DVTRadarComponentKey = 487927;
    "com.apple.dtdevicekit.stacktrace" = (
	0   DTDeviceKitBase                     0x000000011b8ee6e7 DTDKCreateNSError + 109
	1   DTDeviceKitBase                     0x000000011b8eede9 DTDK_AMDErrorToNSError + 792
	2   DTDeviceKitBase                     0x000000011b92c780 -[DTDKMobileDeviceToken _mountDeveloperDiskImage:withError:] + 844
	3   DTDeviceKitBase                     0x000000011b92d29d __58-[DTDKMobileDeviceToken mountDeveloperDiskImageWithError:]_block_invoke + 699
	4   libdispatch.dylib                   0x00007fff7802463d _dispatch_client_callout + 8
	5   libdispatch.dylib                   0x00007fff78030129 _dispatch_lane_barrier_sync_invoke_and_complete + 60
	6   DTDeviceKitBase                     0x000000011b92cf80 -[DTDKMobileDeviceToken mountDeveloperDiskImageWithError:] + 199
	7   IDEiOSSupportCore                   0x000000011b4f72f1 __37-[DVTiOSDevice(Connect) hasConnected]_block_invoke_2 + 267
	8   DVTFoundation                       0x000000010bc7be7a __DVT_CALLING_CLIENT_BLOCK__ + 7
	9   DVTFoundation                       0x000000010bc7fb51 __DVTDispatchGroupAsync_block_invoke + 809
	10  libdispatch.dylib                   0x00007fff780235f8 _dispatch_call_block_and_release + 12
	11  libdispatch.dylib                   0x00007fff7802463d _dispatch_client_callout + 8
	12  libdispatch.dylib                   0x00007fff78026e5e _dispatch_continuation_pop + 534
	13  libdispatch.dylib                   0x00007fff780264a3 _dispatch_async_redirect_invoke + 703
	14  libdispatch.dylib                   0x00007fff780323bc _dispatch_root_queue_drain + 324
	15  libdispatch.dylib                   0x00007fff78032b46 _dispatch_worker_thread2 + 90
	16  libsystem_pthread.dylib             0x00007fff782646b3 _pthread_wqthread + 583
	17  libsystem_pthread.dylib             0x00007fff782643fd start_wqthread + 13
);
}
--
2020-04-23 17:07:27.959 xcodebuild[53855:6550568] [MT] iPhoneConnect: 📱<DVTiOSDevice (0x7f95b5257520), Russell’s iPhone, iPhone, 13.3.1 (17D50), 00008030-0006585E2EBA802E> == END: Underlying device preparation errors ==
2020-04-23 17:07:36.280 ibtoold[53884:6550857] DEBUG: Added to environment: {
    TMPDIR = "/var/folders/9p/nzv40l_974ddhp1hfrccbpw80000gn/T/F24B6543-8298-4EA3-AC94-3D962EB07E04";
}
/* com.apple.actool.document.warnings */
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/hello-world/Images.xcassets:./LaunchImage.launchimage: warning: Launch images are deprecated in iOS 13.0. Use a launch storyboard or XIB instead.
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/hello-world/Images.xcassets:./LaunchImage.launchimage: warning: An iPhone Retina (4-inch) launch image for iOS 7.0 and later is required.
/* com.apple.actool.compilation-results */
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Debug-iphonesimulator/Hello World.app/AppIcon20x20@2x.png
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Debug-iphonesimulator/Hello World.app/AppIcon20x20@3x.png
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Debug-iphonesimulator/Hello World.app/AppIcon29x29@2x.png
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Debug-iphonesimulator/Hello World.app/AppIcon29x29@3x.png
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Debug-iphonesimulator/Hello World.app/AppIcon40x40@2x.png
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Debug-iphonesimulator/Hello World.app/AppIcon40x40@3x.png
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Debug-iphonesimulator/Hello World.app/AppIcon60x60@2x.png
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Debug-iphonesimulator/Hello World.app/AppIcon60x60@3x.png
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Debug-iphonesimulator/Hello World.app/AppIcon20x20~ipad.png
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Debug-iphonesimulator/Hello World.app/AppIcon20x20@2x~ipad.png
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Debug-iphonesimulator/Hello World.app/AppIcon29x29~ipad.png
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Debug-iphonesimulator/Hello World.app/AppIcon29x29@2x~ipad.png
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Debug-iphonesimulator/Hello World.app/AppIcon40x40~ipad.png
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Debug-iphonesimulator/Hello World.app/AppIcon40x40@2x~ipad.png
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Debug-iphonesimulator/Hello World.app/AppIcon76x76~ipad.png
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Debug-iphonesimulator/Hello World.app/AppIcon76x76@2x~ipad.png
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Debug-iphonesimulator/Hello World.app/AppIcon83.5x83.5@2x~ipad.png
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Debug-iphonesimulator/Hello World.app/LaunchImage-1200-Portrait-2688h@3x.png
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Debug-iphonesimulator/Hello World.app/LaunchImage-1200-Landscape-2688h@3x.png
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Debug-iphonesimulator/Hello World.app/LaunchImage-1200-Portrait-1792h@2x.png
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Debug-iphonesimulator/Hello World.app/LaunchImage-1200-Landscape-1792h@2x.png
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Debug-iphonesimulator/Hello World.app/LaunchImage-1100-Portrait-2436h@3x.png
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Debug-iphonesimulator/Hello World.app/LaunchImage-1100-Landscape-2436h@3x.png
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Debug-iphonesimulator/Hello World.app/LaunchImage-800-Portrait-736h@3x.png
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Debug-iphonesimulator/Hello World.app/LaunchImage-800-Landscape-736h@3x.png
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Debug-iphonesimulator/Hello World.app/LaunchImage-800-667h@2x.png
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Debug-iphonesimulator/Hello World.app/Assets.car
/Users/rkm/projects/beeware/briefcase/local/hello-world/iOS/Hello World/build/Hello World.build/Debug-iphonesimulator/Hello World.build/assetcatalog_generated_info.plist

warning: 'UILaunchImages' has been deprecated, use launch storyboards instead. (in target 'Hello World' from project 'Hello World')
note: Using new build system
note: Planning build
note: Constructing build description
Build succeeded.

[hello-world] Built iOS/Hello World/build/Debug-iphonesimulator/Hello World.app
```



Discussion:

Patch:
diff --git a/changes/377.bugfix.rst b/changes/377.bugfix.rst
new file mode 100644
index 00000000..99339b9b
--- /dev/null
+++ b/changes/377.bugfix.rst
@@ -0,0 +1,1 @@
+Ignorable build warnings caused by bugs in Xcode are now filtered out of visible output.
diff --git a/src/briefcase/integrations/subprocess.py b/src/briefcase/integrations/subprocess.py
index 0a54e0ad..3434eef6 100644
--- a/src/briefcase/integrations/subprocess.py
+++ b/src/briefcase/integrations/subprocess.py
@@ -293,23 +293,22 @@ class Subprocess(Tool):
         self,
         args: SubprocessArgsT,
         stream_output: bool = True,
+        filter_func: Callable[[str], Iterator[str]] | None = None,
         **kwargs,
     ) -> CompletedProcess:
         """A wrapper for subprocess.run().
 
-        This method implements the behavior of subprocess.run() with some
-        notable differences:
+        This method implements the behavior of subprocess.run() with some notable
+        differences:
 
-          - The command's output is streamed if ``stream_output=True`` or if
-            dynamic console content such as the Wait Bar is active and I/O is
-            not redirected.
-          - If dynamic content is not active, ``stream_output=False`` will
-            disable streaming.
-          - If the ``env`` argument is provided, the current system environment
-            will be copied, and the contents of env overwritten into that
-            environment.
-          - The ``text`` argument is defaulted to True so all output is returned
-            as strings instead of bytes.
+          - The command's output is streamed if ``stream_output=True`` or if dynamic
+            console content such as the Wait Bar is active and I/O is not redirected.
+          - If dynamic content is not active, ``stream_output=False`` will disable
+            streaming.
+          - If the ``env`` argument is provided, the current system environment will be
+            copied, and the contents of env overwritten into that environment.
+          - The ``text`` argument is defaulted to True so all output is returned as
+            strings instead of bytes.
 
         Output Streaming Primer
         ~~~~~~~~~~~~~~~~~~~~~~~
@@ -317,98 +316,102 @@ class Subprocess(Tool):
         What is output streaming?
         -------------------------
 
-        When a command is invoked via ``subprocess.run()``, by default, its
-        stdout and stderr are connected directly to the console and the output
-        is presented to the user as though the command was invoked natively.
-        When output streaming is active, Briefcase reads the command's output
-        and prints it in to the console on behalf of the command.
+        When a command is invoked via ``subprocess.run()``, by default, its stdout and
+        stderr are connected directly to the console and the output is presented to the
+        user as though the command was invoked natively. When output streaming is
+        active, Briefcase reads the command's output and prints it in to the console on
+        behalf of the command.
 
         Why is output streaming necessary?
         ----------------------------------
 
-        Allowing commands to print directly to the console can provide superior
-        UX since it allows tools to provide their native experience to users.
+        Allowing commands to print directly to the console can provide superior UX since
+        it allows tools to provide their native experience to users.
 
-        However, a primary mechanism for providing support for Briefcase is
-        asking users to provide the log file when errors occur. Often, these
-        errors are raised by invoked commands; streaming the output of commands
-        allows Briefcase to capture that output and include it in the log file
-        as well as the console.
+        However, a primary mechanism for providing support for Briefcase is asking users
+        to provide the log file when errors occur. Often, these errors are raised by
+        invoked commands; streaming the output of commands allows Briefcase to capture
+        that output and include it in the log file as well as the console.
 
-        Additionally, the content of the console can become corrupted if other
-        processes print to the console while Rich's dynamic console elements
-        such as a Progress Bar are active. Streaming such output allows Rich to
-        maintain integrity of all the console content.
+        Additionally, the content of the console can become corrupted if other processes
+        print to the console while Rich's dynamic console elements such as a Progress
+        Bar are active. Streaming such output allows Rich to maintain integrity of all
+        the console content.
 
         How does output streaming work?
         -------------------------------
 
         Streaming is activated when:
 
-          1. dynamic content such as the Wait Bar is active and I/O is not
-             redirected
+          1. dynamic content such as the Wait Bar is active and I/O is not redirected
 
           2. the ``stream_output`` argument is ``True`` (as it is by default)
 
-        I/O is considered redirected when both ``stdout`` and ``stderr`` are
-        specified (e.g., when ``stdout`` and ``stderr`` are set
-        to``subprocess.PIPE`` so the output is available to the caller).
+        I/O is considered redirected when both ``stdout`` and ``stderr`` are specified
+        (e.g., when ``stdout`` and ``stderr`` are set to``subprocess.PIPE`` so the
+        output is available to the caller).
 
-        To facilitate streaming the output, ``stdout`` is set to
-        ``subprocess.PIPE``. If ``stderr`` is not specified by calling code,
-        then it is set to ``subprocess.STDOUT`` so it is interlaced with stdout
-        output.
+        To facilitate streaming the output, ``stdout`` is set to ``subprocess.PIPE``. If
+        ``stderr`` is not specified by calling code, then it is set to
+        ``subprocess.STDOUT`` so it is interlaced with stdout output.
 
-        The process is created for the command with ``Popen``, and a separate
-        thread is started for continuously reading and printing the stdout to
-        the console from the command process. It is necessary to read from
-        stdout in a thread because the read is uninterruptible; so, a user may
-        not be able to abort with CTRL-C.
+        The process is created for the command with ``Popen``, and a separate thread is
+        started for continuously reading and printing the stdout to the console from the
+        command process. It is necessary to read from stdout in a thread because the
+        read is uninterruptible; so, a user may not be able to abort with CTRL-C.
 
-        When the process ends (or the user sends CTRL-C), a call is made to
-        ensure the command process terminates; this, in turn, ensures the thread
-        for streaming can properly exit its otherwise infinite loop. The calling
-        code is either returned a ``CompletedProcess`` object or
-        ``CalledProcessError`` could be raised.
+        When the process ends (or the user sends CTRL-C), a call is made to ensure the
+        command process terminates; this, in turn, ensures the thread for streaming can
+        properly exit its otherwise infinite loop. The calling code is either returned a
+        ``CompletedProcess`` object or ``CalledProcessError`` could be raised.
 
-        Output streaming ultimately strives to simulate calling
-        ``subprocess.run()`` while proxying the command's output to the console.
+        Output streaming ultimately strives to simulate calling ``subprocess.run()``
+        while proxying the command's output to the console.
 
         When *not* to use output streaming?
         -----------------------------------
 
-        When streaming is disabled, the command's output **WILL NOT** be
-        included the Briefcase log file. This makes troubleshooting difficult
-        and should be avoided. However, there are some situations where disabling
-        streaming is unavoidable.
-
-        Some commands use their own dynamic console content such as progress
-        bars, spinners, and other animations. When such output is streamed, its
-        quality can be significantly compromised and look terrible in the
-        console for users. If it is not possible to disable such output with
-        e.g. a flag for the command, then disabling streaming may be necessary.
-        As an example, the ``flatpak install`` command uses many animations.
-
-        If a command requires user input, do not use output streaming. The
-        output streaming implementation doesn't provide any facilities to send
-        input to the process and doesn't provide any special handling of stdin.
-        Additionally, when the command process prompts the user for input, that
-        doesn't always trigger ``readline()`` to return; so, the console may
-        actually end up waiting for user input before displaying a prompt to the
-        user. As an example, accepting Android SDK licenses requires user input.
-
-        :param args: args for subprocess.run()
-        :param stream_output: simulate run() while streaming the output to the
+        When streaming is disabled, the command's output **WILL NOT** be included the
+        Briefcase log file. This makes troubleshooting difficult and should be avoided.
+        However, there are some situations where disabling streaming is unavoidable.
+
+        Some commands use their own dynamic console content such as progress bars,
+        spinners, and other animations. When such output is streamed, its quality can be
+        significantly compromised and look terrible in the console for users. If it is
+        not possible to disable such output with e.g. a flag for the command, then
+        disabling streaming may be necessary. As an example, the ``flatpak install``
+        command uses many animations.
+
+        If a command requires user input, do not use output streaming. The output
+        streaming implementation doesn't provide any facilities to send input to the
+        process and doesn't provide any special handling of stdin. Additionally, when
+        the command process prompts the user for input, that doesn't always trigger
+        ``readline()`` to return; so, the console may actually end up waiting for user
+        input before displaying a prompt to the user. As an example, accepting Android
+        SDK licenses requires user input.
+
+        :param args: args for ``subprocess.run()``
+        :param stream_output: simulate ``run()`` while streaming the output to the
             console. Set to False if the command requires user interaction.
-        :param kwargs: keyword args for subprocess.run()
-        :return: `CompletedProcess` for invoked process
+        :param filter_func: If output streaming is enabled, a callable that will be
+            invoked on every line of output that is streamed. The function accepts the
+            "raw" line of input (stripped of any trailing newline); it returns a
+            generator that yields the filtered output that should be displayed to the
+            user. Can raise StopStreaming to terminate the output stream.
+        :param kwargs: keyword args for ``subprocess.run()``
+        :raises ValueError: if a filter function is provided when in non-streaming mode.
+        :return: ``CompletedProcess`` for invoked process
         """
 
         # Stream the output unless the caller explicitly disables it. When a
         # caller sets stream_output=False, then ensure_console_is_safe() will
         # disable any dynamic console elements while the command runs.
         if stream_output:
-            return self._run_and_stream_output(args, **kwargs)
+            return self._run_and_stream_output(args, filter_func=filter_func, **kwargs)
+
+        # Can't filter non-streamed output.
+        if filter_func:
+            raise ValueError("Cannot apply a filter to non-streamed output.")
 
         # Otherwise, invoke run() normally.
         self._log_command(args)
@@ -430,6 +433,7 @@ class Subprocess(Tool):
         self,
         args: SubprocessArgsT,
         check: bool = False,
+        filter_func: Callable[[str], Iterator[str]] | None = None,
         **kwargs,
     ) -> CompletedProcess:
         """Simulate subprocess.run() while streaming output to the console.
@@ -472,7 +476,7 @@ class Subprocess(Tool):
 
         stderr = None
         with self.Popen(args, **kwargs) as process:
-            self.stream_output(args[0], process)
+            self.stream_output(args[0], process, filter_func=filter_func)
             if process.stderr and kwargs["stderr"] != subprocess.STDOUT:
                 stderr = process.stderr.read()
         return_code = process.poll()
diff --git a/src/briefcase/platforms/iOS/xcode.py b/src/briefcase/platforms/iOS/xcode.py
index 3011b492..38aea0da 100644
--- a/src/briefcase/platforms/iOS/xcode.py
+++ b/src/briefcase/platforms/iOS/xcode.py
@@ -23,7 +23,7 @@ from briefcase.exceptions import (
 from briefcase.integrations.subprocess import is_process_dead
 from briefcase.integrations.xcode import DeviceState, get_device_state, get_simulators
 from briefcase.platforms.iOS import iOSMixin
-from briefcase.platforms.macOS import macOS_log_clean_filter
+from briefcase.platforms.macOS.filters import XcodeBuildFilter, macOS_log_clean_filter
 
 
 class iOSXcodePassiveMixin(iOSMixin):
@@ -327,6 +327,9 @@ class iOSXcodeBuildCommand(iOSXcodePassiveMixin, BuildCommand):
                         "-verbose" if self.tools.logger.is_deep_debug else "-quiet",
                     ],
                     check=True,
+                    filter_func=(
+                        None if self.tools.logger.is_deep_debug else XcodeBuildFilter()
+                    ),
                 )
             except subprocess.CalledProcessError as e:
                 raise BriefcaseCommandError(
diff --git a/src/briefcase/platforms/macOS/__init__.py b/src/briefcase/platforms/macOS/__init__.py
index fd547ce9..6cfe599b 100644
--- a/src/briefcase/platforms/macOS/__init__.py
+++ b/src/briefcase/platforms/macOS/__init__.py
@@ -14,6 +14,7 @@ from briefcase.console import select_option
 from briefcase.exceptions import BriefcaseCommandError
 from briefcase.integrations.subprocess import get_process_id_by_command, is_process_dead
 from briefcase.integrations.xcode import XcodeCliTools, get_identities
+from briefcase.platforms.macOS.filters import macOS_log_clean_filter
 
 try:
     import dmgbuild
@@ -25,48 +26,11 @@ except ImportError:  # pragma: no-cover-if-is-macos
 
 DEFAULT_OUTPUT_FORMAT = "app"
 
-
-MACOS_LOG_PREFIX_REGEX = re.compile(
-    r"\d{4}-\d{2}-\d{2} (?P<timestamp>\d{2}:\d{2}:\d{2}.\d{3}) Df (.*?)\[.*?:.*?\]"
-    r"(?P<subsystem>( \(libffi\.dylib\))|( \(_ctypes\.cpython-3\d{1,2}-.*?\.so\)))? (?P<content>.*)"
-)
-
 ADHOC_IDENTITY_NAME = (
     "Ad-hoc identity. The resulting package will run but cannot be re-distributed."
 )
 
 
-def macOS_log_clean_filter(line):
-    """Filter a macOS system log to extract the Python-generated message content.
-
-    Any system or stub messages are ignored; all logging prefixes are stripped.
-
-    :param line: The raw line from the system log
-    :returns: A tuple, containing (a) the log line, stripped of any system
-        logging context, and (b) a boolean indicating if the message should be
-        included for analysis purposes (i.e., it's Python content, not a system
-        message). Returns a single ``None`` if the line should be dumped.
-    """
-    if any(
-        [
-            # Log stream outputs the filter when it starts
-            line.startswith("Filtering the log data using "),
-            # Log stream outputs barely useful column headers on startup
-            line.startswith("Timestamp          "),
-            # iOS reports an ignorable error on startup
-            line.startswith("Error from getpwuid_r:"),
-        ]
-    ):
-        return None
-
-    match = MACOS_LOG_PREFIX_REGEX.match(line)
-    if match:
-        groups = match.groupdict()
-        return groups["content"], bool(groups["subsystem"])
-
-    return line, False
-
-
 class macOSMixin:
     platform = "macOS"
     supported_host_os = {"Darwin"}
diff --git a/src/briefcase/platforms/macOS/filters.py b/src/briefcase/platforms/macOS/filters.py
new file mode 100644
index 00000000..e1f59df1
--- /dev/null
+++ b/src/briefcase/platforms/macOS/filters.py
@@ -0,0 +1,133 @@
+import re
+
+MACOS_LOG_PREFIX_REGEX = re.compile(
+    r"\d{4}-\d{2}-\d{2} (?P<timestamp>\d{2}:\d{2}:\d{2}.\d{3}) Df (.*?)\[.*?:.*?\]"
+    r"(?P<subsystem>( \(libffi\.dylib\))|( \(_ctypes\.cpython-3\d{1,2}-.*?\.so\)))? (?P<content>.*)"
+)
+
+
+def macOS_log_clean_filter(line):
+    """Filter a macOS system log to extract the Python-generated message content.
+
+    Any system or stub messages are ignored; all logging prefixes are stripped.
+
+    :param line: The raw line from the system log
+    :returns: A tuple, containing (a) the log line, stripped of any system
+        logging context, and (b) a boolean indicating if the message should be
+        included for analysis purposes (i.e., it's Python content, not a system
+        message). Returns a single ``None`` if the line should be dumped.
+    """
+    if any(
+        [
+            # Log stream outputs the filter when it starts
+            line.startswith("Filtering the log data using "),
+            # Log stream outputs barely useful column headers on startup
+            line.startswith("Timestamp          "),
+            # iOS reports an ignorable error on startup
+            line.startswith("Error from getpwuid_r:"),
+        ]
+    ):
+        return None
+
+    match = MACOS_LOG_PREFIX_REGEX.match(line)
+    if match:
+        groups = match.groupdict()
+        return groups["content"], bool(groups["subsystem"])
+
+    return line, False
+
+
+class XcodeBuildFilter:
+    # The prefix for all xcodebuild messages.
+    XCODEBUILD_PREFIX = (
+        r"\d{4}-\d{2}-\d{2} +\d+:\d{2}:\d{2}\.\d{3} xcodebuild\[\d+:\d+\] +"
+    )
+
+    # Xcode 14 generates the following warning when there is a passcode protected device
+    # attached to your computer, even if you're not doing anything to target that device:
+    # ---------------------------------------------------------------------
+    # 2023-09-27 08:38:11.865 xcodebuild[41087:25901835]  DTDKRemoteDeviceConnection:
+    #   Failed to start remote service "com.apple.mobile.notification_proxy" on device.
+    #   Error: Error Domain=com.apple.dtdevicekit Code=811 "Failed to start remote
+    #   service "com.apple.mobile.notification_proxy" on device."
+    #   UserInfo={NSUnderlyingError=0x10b8ec780 {Error Domain=
+    #   com.apple.dt.MobileDeviceErrorDomain Code=-402653158 "The device is passcode
+    #   protected." UserInfo={MobileDeviceErrorCode=(0xE800001A),
+    #   com.apple.dtdevicekit.stacktrace=(
+    #         0   DTDeviceKitBase ...
+    #         ...
+    #         21  libsystem_pthread.dylib ...
+    # ), DVTRadarComponentKey=261622, NSLocalizedDescription=The device is
+    #   passcode protected.}}, NSLocalizedRecoverySuggestion=Please check your
+    #   connection to your device., DVTRadarComponentKey=261622,
+    #   NSLocalizedDescription=Failed to start remote service
+    #   "com.apple.mobile.notification_proxy" on device.}
+    # 2023-09-27 08:38:11.903 xcodebuild[41087:25901835] Failed to start service
+    #   (com.apple.amfi.lockdown): 0xe800001a
+    # ---------------------------------------------------------------------
+    # These warnings can be ignored.
+    LOCKED_DEVICE_START_RE = re.compile(
+        XCODEBUILD_PREFIX
+        + r"DTDKRemoteDeviceConnection: Failed to start remote service "
+        r"\"com\.apple\.mobile\.notification_proxy\" on device. Error: "
+        r"Error Domain=com\.apple\.dtdevicekit Code=811 \"Failed to start "
+        r"remote service \"com.apple.mobile.notification_proxy\" on device.\""
+    )
+    LOCKED_DEVICE_END_RE = re.compile(
+        r"\), DVTRadarComponentKey=\d+, NSLocalizedDescription=The device is "
+        # r"passcode protected\.}}, NSLocalizedRecoverySuggestion=Please check "
+        # r"your connection to your device\."
+    )
+    LOCKED_DEVICE_ADDITIONAL_RE = re.compile(
+        XCODEBUILD_PREFIX + r"Failed to start service \(com\.apple\.amfi\.lockdown\): "
+    )
+
+    # XCode 15 generates dozens of copies of the following message:
+    # ---------------------------------------------------------------------
+    # 2023-09-26 14:35:45.775 xcodebuild[75877:23947967] [MT] DVTAssertions:
+    #   Warning in /System/Volumes/Data/SWE/Apps/DT/BuildRoots/BuildRoot11
+    #   /ActiveBuildRoot/Library/Caches/com.apple.xbs/Sources/IDEFrameworks
+    #   /IDEFrameworks-22267/IDEFoundation/Provisioning/Capabilities Infrastructure
+    #   /IDECapabilityQuerySelection.swift:103
+    # Details:  createItemModels creation requirements should not create capability
+    #   item model for a capability item model that already exists.
+    # Function: createItemModels(for:itemModelSource:)
+    # Thread:   <_NSMainThread: 0x11d60beb0>{number = 1, name = main}
+    # Please file a bug at https://feedbackassistant.apple.com with this warning
+    #   message and any useful information you can provide.
+    # ---------------------------------------------------------------------
+    # As best as I can make out, this is a bug in Xcode; but it's overwhelming and
+    # confusing, so filter it out.
+    DVT_ASSERTIONS_RE = re.compile(
+        XCODEBUILD_PREFIX + r"\[MT\] DVTAssertions: "
+        r"Warning in /System/Volumes/Data/SWE/Apps/DT/BuildRoots/BuildRoot11/"
+        r"ActiveBuildRoot/Library/Caches/com.apple.xbs/Sources/IDEFrameworks/"
+        r"IDEFrameworks-22267/IDEFoundation/Provisioning"
+        r"/Capabilities Infrastructure/IDECapabilityQuerySelection.swift:\d+"
+    )
+
+    def __init__(self):
+        self.dvt_ignore_count = 0
+        self.locked_device = False
+
+    def __call__(self, line):
+        """Filter a single line of a log.
+
+        :param line: A single line of raw system log content, including the newline.
+        """
+        if self.dvt_ignore_count:
+            self.dvt_ignore_count -= 1
+        elif self.locked_device:
+            if self.LOCKED_DEVICE_END_RE.match(line):
+                self.locked_device = False
+        elif self.LOCKED_DEVICE_START_RE.match(line):
+            # Start ignoring content until the end line is found.
+            self.locked_device = True
+        elif self.LOCKED_DEVICE_ADDITIONAL_RE.match(line):
+            # Ignore the additional 1-line warning about failing to start the service.
+            pass
+        elif self.DVT_ASSERTIONS_RE.match(line):
+            # Ignore the 4 lines after the DVTAssertions message
+            self.dvt_ignore_count = 4
+        else:
+            yield line
diff --git a/src/briefcase/platforms/macOS/xcode.py b/src/briefcase/platforms/macOS/xcode.py
index 9b416a19..d0806171 100644
--- a/src/briefcase/platforms/macOS/xcode.py
+++ b/src/briefcase/platforms/macOS/xcode.py
@@ -13,6 +13,7 @@ from briefcase.config import BaseConfig
 from briefcase.exceptions import BriefcaseCommandError
 from briefcase.integrations.xcode import Xcode
 from briefcase.platforms.macOS import macOSMixin, macOSPackageMixin, macOSRunMixin
+from briefcase.platforms.macOS.filters import XcodeBuildFilter
 
 
 class macOSXcodeMixin(macOSMixin):
@@ -70,6 +71,9 @@ class macOSXcodeBuildCommand(macOSXcodeMixin, BuildCommand):
                         "build",
                     ],
                     check=True,
+                    filter_func=(
+                        None if self.tools.logger.is_deep_debug else XcodeBuildFilter()
+                    ),
                 )
                 self.logger.info("Build succeeded.")
             except subprocess.CalledProcessError as e:


Test patch:
diff --git a/tests/integrations/subprocess/test_Subprocess__run__stream_output__False.py b/tests/integrations/subprocess/test_Subprocess__run__stream_output__False.py
index 694c3b6a..baf44985 100644
--- a/tests/integrations/subprocess/test_Subprocess__run__stream_output__False.py
+++ b/tests/integrations/subprocess/test_Subprocess__run__stream_output__False.py
@@ -224,3 +224,17 @@ def test_text_eq_true_default_overriding(mock_sub, in_kwargs, kwargs):
     mock_sub.run(["hello", "world"], stream_output=False, **in_kwargs)
 
     mock_sub._subprocess.run.assert_called_with(["hello", "world"], **kwargs)
+
+
+def test_call_with_filter_func(mock_sub, capsys, sub_kw):
+    """Providing a filter function when not streaming raises an exception."""
+
+    with pytest.raises(
+        ValueError,
+        match=r"Cannot apply a filter to non-streamed output",
+    ):
+        mock_sub.run(
+            ["hello", "world"],
+            filter_func=lambda line: line,
+            stream_output=False,
+        )
diff --git a/tests/integrations/subprocess/test_Subprocess__run__stream_output__True.py b/tests/integrations/subprocess/test_Subprocess__run__stream_output__True.py
index 491857df..5f04da55 100644
--- a/tests/integrations/subprocess/test_Subprocess__run__stream_output__True.py
+++ b/tests/integrations/subprocess/test_Subprocess__run__stream_output__True.py
@@ -257,3 +257,35 @@ def test_text_eq_true_default_overriding(mock_sub, in_kwargs, kwargs, sleep_zero
         bufsize=1,
         **kwargs,
     )
+
+
+@pytest.mark.parametrize("platform", ["Linux", "Darwin", "Windows"])
+def test_filtered_output(mock_sub, capsys, platform, sub_stream_kw, sleep_zero):
+    """Streamed output can be filtered."""
+
+    # Add an output filter that replaces all spaces with newlines.
+    def space_filter(line):
+        yield from line.split(" ")
+
+    mock_sub.tools.sys.platform = platform
+    mock_sub.run(
+        ["hello", "world"],
+        filter_func=space_filter,
+    )
+
+    mock_sub._subprocess.Popen.assert_called_with(
+        ["hello", "world"],
+        **sub_stream_kw,
+    )
+    # fmt: off
+    expected_output = (
+        "output\n"
+        "line\n"
+        "1\n"
+        "\n"
+        "output\n"
+        "line\n"
+        "3\n"
+    )
+    # fmt: on
+    assert capsys.readouterr().out == expected_output
diff --git a/tests/integrations/subprocess/test_ensure_console_is_safe.py b/tests/integrations/subprocess/test_ensure_console_is_safe.py
index e70d97d5..93e6ef7f 100644
--- a/tests/integrations/subprocess/test_ensure_console_is_safe.py
+++ b/tests/integrations/subprocess/test_ensure_console_is_safe.py
@@ -26,7 +26,10 @@ def test_run_windows_batch_script(mock_sub, batch_script):
     with mock_sub.tools.input.wait_bar("Testing..."):
         mock_sub.run([batch_script, "World"])
 
-    mock_sub._run_and_stream_output.assert_called_with([batch_script, "World"])
+    mock_sub._run_and_stream_output.assert_called_with(
+        [batch_script, "World"],
+        filter_func=None,
+    )
     mock_sub.tools.input.release_console_control.assert_called_once()
 
 
@@ -52,7 +55,10 @@ def test_run_stream_output_true(mock_sub, sub_kwargs):
     with mock_sub.tools.input.wait_bar("Testing..."):
         mock_sub.run(["Hello", "World"], **sub_kwargs)
 
-    mock_sub._run_and_stream_output.assert_called_with(["Hello", "World"])
+    mock_sub._run_and_stream_output.assert_called_with(
+        ["Hello", "World"],
+        filter_func=None,
+    )
     mock_sub.tools.input.release_console_control.assert_not_called()
 
 
@@ -82,7 +88,11 @@ def test_negative_condition_not_controlled(
     """Passthrough to Subprocess if conditions to release console control are not met
     while the console is not controlled."""
     mock_sub.run(cmdline, **kwargs)
-    mock_sub._run_and_stream_output.assert_called_with(cmdline, **kwargs)
+    mock_sub._run_and_stream_output.assert_called_with(
+        cmdline,
+        filter_func=None,
+        **kwargs,
+    )
 
     mock_sub.check_output(cmdline, **kwargs)
 
@@ -106,7 +116,11 @@ def test_negative_condition_controlled(mock_sub, cmdline, kwargs, sub_check_outp
         mock_sub.run(cmdline, **kwargs)
         mock_sub.check_output(cmdline, **kwargs)
 
-    mock_sub._run_and_stream_output.assert_called_with(cmdline, **kwargs)
+    mock_sub._run_and_stream_output.assert_called_with(
+        cmdline,
+        filter_func=None,
+        **kwargs,
+    )
     final_kwargs = {**sub_check_output_kw, **kwargs}
     mock_sub._subprocess.check_output.assert_called_with(cmdline, **final_kwargs)
     mock_sub.tools.input.release_console_control.assert_not_called()
diff --git a/tests/platforms/iOS/xcode/test_build.py b/tests/platforms/iOS/xcode/test_build.py
index ec10eff3..3b9c53b0 100644
--- a/tests/platforms/iOS/xcode/test_build.py
+++ b/tests/platforms/iOS/xcode/test_build.py
@@ -57,6 +57,7 @@ def test_build_app(build_command, first_app_generated, tool_debug_mode, tmp_path
             "-verbose" if tool_debug_mode else "-quiet",
         ],
         check=True,
+        filter_func=None if tool_debug_mode else mock.ANY,
     )
 
     # The app metadata references the app module
@@ -99,6 +100,7 @@ def test_build_app_test_mode(build_command, first_app_generated, tmp_path):
             "-quiet",
         ],
         check=True,
+        filter_func=mock.ANY,
     )
 
     # The app metadata has been rewritten to reference the test module
@@ -146,4 +148,5 @@ def test_build_app_failed(build_command, first_app_generated, tmp_path):
             "-quiet",
         ],
         check=True,
+        filter_func=mock.ANY,
     )
diff --git a/tests/platforms/macOS/test_XcodeBuildFilter.py b/tests/platforms/macOS/test_XcodeBuildFilter.py
new file mode 100644
index 00000000..b9425e95
--- /dev/null
+++ b/tests/platforms/macOS/test_XcodeBuildFilter.py
@@ -0,0 +1,101 @@
+# This file contains raw log lines, which are extremely long. Turn of the flake8 rule
+# for line length.
+# flake8: noqa: E501
+import pytest
+
+from briefcase.platforms.macOS.filters import XcodeBuildFilter
+
+
+@pytest.mark.parametrize(
+    "original, filtered",
+    [
+        # Nothing to filter
+        (
+            [
+                "'Twas brillig, and the slithy toves",
+                "Did gyre and gimble in the wabe;",
+                "All mimsy were the borogoves,",
+                "And the mome raths outgrabe.",
+            ],
+            [
+                "'Twas brillig, and the slithy toves",
+                "Did gyre and gimble in the wabe;",
+                "All mimsy were the borogoves,",
+                "And the mome raths outgrabe.",
+            ],
+        ),
+        # Xcode 14: DTDKRemoteDeviceConnection warning about locked attached devices
+        (
+            [
+                "'Twas brillig, and the slithy toves",
+                '2023-09-27 08:38:11.865 xcodebuild[41087:25901835]  DTDKRemoteDeviceConnection: Failed to start remote service "com.apple.mobile.notification_proxy" on device. Error: Error Domain=com.apple.dtdevicekit Code=811 "Failed to start remote service "com.apple.mobile.notification_proxy" on device." UserInfo={NSUnderlyingError=0x10b8ec780 {Error Domain=com.apple.dt.MobileDeviceErrorDomain Code=-402653158 "The device is passcode protected." UserInfo={MobileDeviceErrorCode=(0xE800001A), com.apple.dtdevicekit.stacktrace=(',
+                "        0   DTDeviceKitBase                     0x00000001288ff298 DTDKCreateNSErrorFromAMDErrorCode + 300",
+                "        1   DTDeviceKitBase                     0x000000012890ba38 __63-[DTDKRemoteDeviceConnection startFirstServiceOf:unlockKeybag:]_block_invoke + 380",
+                "        2   DTDeviceKitBase                     0x000000012890b248 __48-[DTDKRemoteDeviceConnection futureWithSession:]_block_invoke_4 + 28",
+                "        3   DTDeviceKitBase                     0x0000000128901460 __DTDKExecuteInSession_block_invoke_2 + 68",
+                "        4   DTDeviceKitBase                     0x0000000128900af0 __DTDKExecuteWithConnection_block_invoke_2 + 216",
+                "        5   DTDeviceKitBase                     0x00000001289009e8 __DTDKExecuteWithConnection_block_invoke + 112",
+                "        6   libdispatch.dylib                   0x00000001a81b4400 _dispatch_client_callout + 20",
+                "        7   libdispatch.dylib                   0x00000001a81c397c _dispatch_lane_barrier_sync_invoke_and_complete + 56",
+                "        8   DVTFoundation                       0x0000000100fa8014 DVTDispatchBarrierSync + 148",
+                "        9   DVTFoundation                       0x0000000100f842b4 -[DVTDispatchLock performLockedBlock:] + 60",
+                "        10  DTDeviceKitBase                     0x00000001289008e4 DTDKExecuteWithConnection + 200",
+                "        11  DTDeviceKitBase                     0x00000001289012e4 DTDKExecuteInSession + 260",
+                "        12  DTDeviceKitBase                     0x000000012890b028 __48-[DTDKRemoteDeviceConnection futureWithSession:]_block_invoke_2 + 204",
+                "        13  DVTFoundation                       0x0000000100fa7330 __DVT_CALLING_CLIENT_BLOCK__ + 16",
+                "        14  DVTFoundation                       0x0000000100fa7d58 __DVTDispatchAsync_block_invoke + 152",
+                "        15  libdispatch.dylib                   0x00000001a81b2874 _dispatch_call_block_and_release + 32",
+                "        16  libdispatch.dylib                   0x00000001a81b4400 _dispatch_client_callout + 20",
+                "        17  libdispatch.dylib                   0x00000001a81bba88 _dispatch_lane_serial_drain + 668",
+                "        18  libdispatch.dylib                   0x00000001a81bc62c _dispatch_lane_invoke + 436",
+                "        19  libdispatch.dylib                   0x00000001a81c7244 _dispatch_workloop_worker_thread + 648",
+                "        20  libsystem_pthread.dylib             0x00000001a8360074 _pthread_wqthread + 288",
+                "        21  libsystem_pthread.dylib             0x00000001a835ed94 start_wqthread + 8",
+                '), DVTRadarComponentKey=261622, NSLocalizedDescription=The device is passcode protected.}}, NSLocalizedRecoverySuggestion=Please check your connection to your "device., DVTRadarComponentKey=261622, NSLocalizedDescription=Failed to start remote service "com.apple.mobile.notification_proxy" on device.}',
+                "Did gyre and gimble in the wabe;",
+            ],
+            [
+                "'Twas brillig, and the slithy toves",
+                "Did gyre and gimble in the wabe;",
+            ],
+        ),
+        # Xcode 14: Additional locking-related message.
+        (
+            [
+                "'Twas brillig, and the slithy toves",
+                "2023-09-27 09:09:55.400 xcodebuild[44887:25948169] Failed to start service (com.apple.amfi.lockdown): 0xe800001a",
+                "Did gyre and gimble in the wabe;",
+            ],
+            [
+                "'Twas brillig, and the slithy toves",
+                "Did gyre and gimble in the wabe;",
+            ],
+        ),
+        # Xcode 15: DVTAssertions warning about createItemModels
+        (
+            [
+                "'Twas brillig, and the slithy toves",
+                "2023-09-26 14:35:45.775 xcodebuild[75877:23947967] [MT] DVTAssertions: Warning in /System/Volumes/Data/SWE/Apps/DT/BuildRoots/BuildRoot11/ActiveBuildRoot/Library/Caches/com.apple.xbs/Sources/IDEFrameworks/IDEFrameworks-22267/IDEFoundation/Provisioning/Capabilities Infrastructure/IDECapabilityQuerySelection.swift:103",
+                "Details: createItemModels creation requirements should not create capability item model for a capability item model that already exists.",
+                "Function: createItemModels(for:itemModelSource:)",
+                "Thread:   <_NSMainThread: 0x11d60beb0>{number = 1, name = main}",
+                "Please file a bug at https://feedbackassistant.apple.com with this warning message and any useful information you can provide.",
+                "Did gyre and gimble in the wabe;",
+            ],
+            [
+                "'Twas brillig, and the slithy toves",
+                "Did gyre and gimble in the wabe;",
+            ],
+        ),
+    ],
+)
+def test_filter(original, filtered):
+    """The Xcode build output filters out ignorable warnings."""
+    xcode_filter = XcodeBuildFilter()
+
+    output = []
+    for raw_line in original:
+        for line in xcode_filter(raw_line):
+            output.append(line)
+
+    assert output == filtered
diff --git a/tests/platforms/macOS/test_macOS_log_clean_filter.py b/tests/platforms/macOS/test_macOS_log_clean_filter.py
index afc556f1..771a1ca8 100644
--- a/tests/platforms/macOS/test_macOS_log_clean_filter.py
+++ b/tests/platforms/macOS/test_macOS_log_clean_filter.py
@@ -1,6 +1,6 @@
 import pytest
 
-from briefcase.platforms.macOS import macOS_log_clean_filter
+from briefcase.platforms.macOS.filters import macOS_log_clean_filter
 
 
 @pytest.mark.parametrize(
diff --git a/tests/platforms/macOS/xcode/test_build.py b/tests/platforms/macOS/xcode/test_build.py
index 40db9a27..57744e60 100644
--- a/tests/platforms/macOS/xcode/test_build.py
+++ b/tests/platforms/macOS/xcode/test_build.py
@@ -1,5 +1,5 @@
 import subprocess
-from unittest.mock import MagicMock
+from unittest.mock import ANY, MagicMock
 
 import pytest
 
@@ -46,6 +46,7 @@ def test_build_app(build_command, first_app_generated, tool_debug_mode, tmp_path
             "build",
         ],
         check=True,
+        filter_func=None if tool_debug_mode else ANY,
     )
 
 
@@ -78,4 +79,5 @@ def test_build_app_failed(build_command, first_app_generated, tmp_path):
             "build",
         ],
         check=True,
+        filter_func=ANY,
     )