Skip to content

Commit

Permalink
Merge pull request #1468 from freakboy3742/xcode-build-filter
Browse files Browse the repository at this point in the history
Filter Xcode build output to remove noisy warnings
  • Loading branch information
mhsmith authored Sep 28, 2023
2 parents 685080d + 0cac95f commit f86386f
Show file tree
Hide file tree
Showing 13 changed files with 394 additions and 119 deletions.
1 change: 1 addition & 0 deletions changes/377.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Ignorable build warnings caused by bugs in Xcode are now filtered out of visible output.
154 changes: 79 additions & 75 deletions src/briefcase/integrations/subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,122 +293,125 @@ def run(
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
~~~~~~~~~~~~~~~~~~~~~~~
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)
Expand All @@ -430,6 +433,7 @@ def _run_and_stream_output(
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.
Expand Down Expand Up @@ -472,7 +476,7 @@ def _run_and_stream_output(

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()
Expand Down
5 changes: 4 additions & 1 deletion src/briefcase/platforms/iOS/xcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
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):
Expand Down Expand Up @@ -327,6 +327,9 @@ def build_app(self, app: BaseConfig, test_mode: bool = False, **kwargs):
"-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(
Expand Down
38 changes: 1 addition & 37 deletions src/briefcase/platforms/macOS/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
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
Expand All @@ -25,48 +26,11 @@

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"}
Expand Down
Loading

0 comments on commit f86386f

Please # to comment.