Repository: beeware/briefcase

Issue:
Windows console output encoding corruption
### Describe the bug

Something is getting lost in translation when Briefcase streams `subprocess` calls.

For instance, if a user adds `print("Année")` to their app, then `briefcase dev` and `briefcase run` will ostensibly attempt to print `utf-8` characters to the console and you end up with something like `Ann├⌐e`. Since `é` is a two-byte char in UTF-8, it gets translated to `├` and `⌐` in cp437.

### Steps to reproduce

Add `print("Année")` to a Briefcase app and run `briefcase dev`.

### Expected behavior

Encodings should be managed properly.

This seems to be an outcome from Briefcase output streaming where Briefcase attaches a pipe to the subprocess' `stdout`, reads it in, and writes it to the console via Rich. One of these steps must be making an error about the encoding to use.

Interesting, if you just use Rich directly, it will say the console is UTF-8 while it is actually a msft code page...but still print the text correctly:
```console
❯ python -c 'from rich.console import Console; console=Console(); console.print("Année"); print(console.encoding)'
Année
utf-8

❯ python -c 'import sys, os; print(os.device_encoding(sys.__stdout__.fileno()))'
cp437
```

### Screenshots

_No response_

### Environment

- Operating System: Windows 11
- Python version: python 3.10.11
- Software versions:
  - Briefcase: `0.3.16.dev103+g48fc4baa.d20230820`

### Logs

<details>
<summary>Briefcase Log</summary>

```
Date/Time:       2023-08-19 18:48:32 
Command line:    C:\Users\user\github\beeware\briefcase\venv-3.10-briefcase\Scripts\briefcase dev --log

OS Release:      Windows 10
OS Version:      10.0.22621
Architecture:    AMD64
Platform:        Windows-10-10.0.22621-SP0

Python exe:      C:\Users\user\github\beeware\briefcase\venv-3.10-briefcase\Scripts\python.exe
Python version:  3.10.11 (tags/v3.10.11:7d4cc5a, Apr  5 2023, 00:38:17) [MSC v.1929 64 bit (AMD64)]
Virtual env:     True
Conda env:       False

Briefcase:       0.3.16.dev103+g48fc4baa
Target platform: windows
Target format:   None

Environment Variables:
    ALLUSERSPROFILE=C:\ProgramData
    APPDATA=C:\Users\user\AppData\Roaming
    COMMONPROGRAMFILES=C:\Program Files\Common Files
    COMMONPROGRAMFILES(X86)=C:\Program Files (x86)\Common Files
    COMMONPROGRAMW6432=C:\Program Files\Common Files
    COMPUTERNAME=VM-WIN11
    COMSPEC=C:\Windows\system32\cmd.exe
    DRIVERDATA=C:\Windows\System32\Drivers\DriverData
    FPS_BROWSER_APP_PROFILE_STRING=Internet Explorer
    FPS_BROWSER_USER_PROFILE_STRING=Default
    HOMEDRIVE=C:
    HOMEPATH=\Users\user
    LOCALAPPDATA=C:\Users\user\AppData\Local
    LOGONSERVER=\\VM-WIN11
    NUMBER_OF_PROCESSORS=4
    ONEDRIVE=C:\Users\user\OneDrive
    OS=Windows_NT
    PATH=C:\Users\user\github\beeware\briefcase\venv-3.10-briefcase\Scripts;C:\Program Files\PowerShell\7;C:\Users\user\.pyenv\pyenv-win\shims;C:\Users\user\.pyenv\pyenv-win\versions\3.9.13;C:\Users\user\.pyenv\pyenv-win\versions\3.8.10;C:\Users\user\.pyenv\pyenv-win\versions\3.10.11;C:\Users\user\.pyenv\pyenv-win\versions\3.11.3;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Program Files\Git\cmd;;C:\Program Files\dotnet\;C:\Program Files\starship\bin\;C:\Program Files\PowerShell\7\;C:\Program Files\Docker\Docker\resources\bin;C:\Users\user\.pyenv\pyenv-win\bin;C:\Users\user\AppData\Local\Microsoft\WindowsApps;C:\Users\user\.dotnet\tools;C:\Users\user\.dotnet\tools;C:\Users\user\AppData\Local\Programs\Microsoft VS Code\bin
    PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.CPL
    POWERSHELL_DISTRIBUTION_CHANNEL=MSI:Windows 10 Pro
    PROCESSOR_ARCHITECTURE=AMD64
    PROCESSOR_IDENTIFIER=AMD64 Family 25 Model 33 Stepping 0, AuthenticAMD
    PROCESSOR_LEVEL=25
    PROCESSOR_REVISION=2100
    PROGRAMDATA=C:\ProgramData
    PROGRAMFILES=C:\Program Files
    PROGRAMFILES(X86)=C:\Program Files (x86)
    PROGRAMW6432=C:\Program Files
    PSMODULEPATH=C:\Users\user\Documents\PowerShell\Modules;C:\Program Files\PowerShell\Modules;c:\program files\powershell\7\Modules;C:\Program Files\WindowsPowerShell\Modules;C:\Windows\system32\WindowsPowerShell\v1.0\Modules
    PUBLIC=C:\Users\Public
    PYENV=C:\Users\user\.pyenv\pyenv-win\
    PYENV_HOME=C:\Users\user\.pyenv\pyenv-win\
    PYENV_ROOT=C:\Users\user\.pyenv\pyenv-win\
    STARSHIP_SESSION_KEY=********************
    STARSHIP_SHELL=pwsh
    SYSTEMDRIVE=C:
    SYSTEMROOT=C:\Windows
    TEMP=C:\Users\user\AppData\Local\Temp
    TMP=C:\Users\user\AppData\Local\Temp
    USERDOMAIN=vm-win11
    USERDOMAIN_ROAMINGPROFILE=vm-win11
    USERNAME=user
    USERPROFILE=C:\Users\user
    VIRTUAL_ENV=C:\Users\user\github\beeware\briefcase\venv-3.10-briefcase
    VIRTUAL_ENV_DISABLE_PROMPT=1
    WINDIR=C:\Windows
    WSLENV=WT_SESSION::WT_PROFILE_ID
    WT_PROFILE_ID={574e775e-4f2a-5b96-ac1e-a2962a402336}
    WT_SESSION=f5d14929-bfdc-4438-b46d-323336b15bc0
    _OLD_VIRTUAL_PATH=C:\Program Files\PowerShell\7;C:\Users\user\.pyenv\pyenv-win\shims;C:\Users\user\.pyenv\pyenv-win\versions\3.9.13;C:\Users\user\.pyenv\pyenv-win\versions\3.8.10;C:\Users\user\.pyenv\pyenv-win\versions\3.10.11;C:\Users\user\.pyenv\pyenv-win\versions\3.11.3;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Program Files\Git\cmd;;C:\Program Files\dotnet\;C:\Program Files\starship\bin\;C:\Program Files\PowerShell\7\;C:\Program Files\Docker\Docker\resources\bin;C:\Users\user\.pyenv\pyenv-win\bin;C:\Users\user\AppData\Local\Microsoft\WindowsApps;C:\Users\user\.dotnet\tools;C:\Users\user\.dotnet\tools;C:\Users\user\AppData\Local\Programs\Microsoft VS Code\bin

Briefcase Log:
[18:48:29]                                                                                                                                                                dev.py:208
           [helloworld] Starting in dev mode...                                                                                                                           dev.py:208
                                                                                                                                                                   subprocess.py:705
           >>> Running Command:                                                                                                                                    subprocess.py:706
           >>>     'C:\Users\user\github\beeware\briefcase\venv-3.10-briefcase\Scripts\python.exe' -u -X dev -X utf8 -c 'import runpy,                             subprocess.py:707
           sys;sys.path.pop(0);sys.argv.extend([]);runpy.run_module("helloworld", run_name="__main__", alter_sys=True)'                                                             
           >>> Working Directory:                                                                                                                                  subprocess.py:714
           >>>     C:\Users\user                                                                                                                                   subprocess.py:715
           >>> Environment Overrides:                                                                                                                              subprocess.py:724
           >>>     PYTHONPATH=C:\Users\user\github\beeware\briefcase\tmp\helloworld\src                                                                            subprocess.py:726
           >>>     PYTHONMALLOC=default                                                                                                                            subprocess.py:726
           ===========================================================================                                                                                    run.py:160
           Ann├⌐e                                                                                                                                                  subprocess.py:675
```

</details>
### Additional context

_No response_

Discussion:
rmartin16: haha, this rabbit hole is fun....gradle's output is actually `cp1252` on Windows...
rmartin16: No matter what I do, Gradle continues to write to `stdout` in cp1252....although, there seems to be _a lot_ of arcane encoding settings for Gradle and jvm...so, maybe I've just missed it.

Setting `encoding=locale.getpreferredencoding(False)` seems appropriate for Gradle.
rmartin16: Running Briefcase on Linux without using a UTF-8 locale seems especially problematic. I tried with ISO-8859-1.

1. The • character (i.e. [\u2022](https://www.compart.com/en/unicode/U+2022)) we're putting in Rich's Progress Bar isn't supported...so, Rich crashes out and recommends setting `PYTHONIOENCODING=utf-8`. (This recommendation seems to arise from environments without `$LANG` set and Python defaults to ASCII). This more or less fixes Rich....but it also means everything written to `stdout` over 7-bit ASCII is a bit of a disaster....but Python doesn't crash at least....

2. Briefcase runs Docker assuming the system encoding will come back....but the Docker containers will almost certainly be using UTF-8. This can be overcome by forcing `encoding="UTF-8"` for all Docker calls. Although, something weird happens when you do this and certain actions, like running `pip` in the container, can cause the container to crash `with <Signals.SIGKILL: 9>`....

3. linuxdeploy (or the way Briefcase is invoking) doesn't play well with non-ASCII filenames...they turn in to mojibake....or it may actually because of the next problem...

4. When `briefcase.toml` is loaded via `tomllib.load()`, it makes a naked `decode()` call on the bytes of the file....which means it uses ISO-8859-1 instead of UTF-8 when the file is encoded with UTF-8. This is obviously fine if the file only contains ASCII...but if you name your project "Année", that won't be the case and things go downhill from here. This could be avoided if the sanitized App name was used....like with system packages.

Given how severe the situation is with using Linux without UTF-8 and the fact no one has brought this up.....I'm inclined to do nothing. This reinforces that UTF-8 has significantly pervaded Linux distros and going out of our way to support non-UTF-8 locales is unnecessary.
freakboy3742: > Given how severe the situation is with using Linux without UTF-8 and the fact no one has brought this up.....I'm inclined to do nothing. This reinforces that UTF-8 has significantly pervaded Linux distros and going out of our way to support non-UTF-8 locales is unnecessary.

Broadly agreed. We would have heard someone make noise about this if it were more than a severe edge case.

If we're able to detect this situation and drop a warning in the console when it occurs (but not make any attempt to fix problems beyond that), I'd be fine with that. But if not... I'm also fine with that - we can handle this with a documentation note saying "srsly... use UTF-8 already".


Patch:
diff --git a/changes/1407.bugfix.rst b/changes/1407.bugfix.rst
new file mode 100644
index 00000000..484a0794
--- /dev/null
+++ b/changes/1407.bugfix.rst
@@ -0,0 +1,1 @@
+The console output from invoking Python via a subprocess call is now properly decoded as UTF-8.
diff --git a/pyproject.toml b/pyproject.toml
index f8b256e2..21e0749b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -36,6 +36,8 @@ no-cover-if-is-windows = "'win32' == os_environ.get('COVERAGE_PLATFORM', sys_pla
 no-cover-if-not-windows = "'win32' != os_environ.get('COVERAGE_PLATFORM', sys_platform) and os_environ.get('COVERAGE_EXCLUDE_PLATFORM') != 'disable'"
 no-cover-if-is-py312 = "python_version == '3.12' and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'"
 no-cover-if-lt-py312 = "sys_version_info < (3, 12) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'"
+no-cover-if-lt-py311 = "sys_version_info < (3, 11) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'"
+no-cover-if-gte-py311 = "sys_version_info > (3, 11) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'"
 no-cover-if-is-py310 = "python_version == '3.10' and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'"
 no-cover-if-lt-py310 = "sys_version_info < (3, 10) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'"
 no-cover-if-gte-py310 = "sys_version_info > (3, 10) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'"
diff --git a/src/briefcase/commands/base.py b/src/briefcase/commands/base.py
index 1c17acd5..7125f1d2 100644
--- a/src/briefcase/commands/base.py
+++ b/src/briefcase/commands/base.py
@@ -159,6 +159,9 @@ class BaseCommand(ABC):
         Subprocess.verify(tools=self.tools)
         Download.verify(tools=self.tools)
 
+        if not is_clone:
+            self.validate_locale()
+
         self.global_config = None
         self._briefcase_toml: dict[AppConfig, dict[str, ...]] = {}
 
@@ -250,6 +253,28 @@ a custom location for Briefcase's tools.
 
         return Path(data_path)
 
+    def validate_locale(self):
+        """Validate the system's locale is compatible."""
+        if self.tools.host_os == "Linux" and self.tools.system_encoding != "UTF-8":
+            self.logger.warning(
+                f"""
+*************************************************************************
+** WARNING: Default system encoding is not supported                   **
+*************************************************************************
+
+    Briefcase and the third-party tools it uses only support UTF-8.
+
+    The detected default system encoding is {self.tools.system_encoding}.
+
+    Briefcase will proceed but some console output could be corrupted and
+    created files or artefacts may contain corrupted text.
+
+    Update your system's encoding to UTF-8 to avoid issues.
+
+*************************************************************************
+"""
+            )
+
     def _command_factory(self, command_name: str):
         """Command factory for the current platform and format.
 
@@ -833,9 +858,9 @@ Did you run Briefcase in a project directory that contains {filename.name!r}?"""
 ** WARNING: Unable to update template                                  **
 *************************************************************************
 
-   Briefcase is unable the update the application template. This
-   may be because your computer is currently offline. Briefcase will
-   use existing template without updating.
+    Briefcase is unable the update the application template. This
+    may be because your computer is currently offline. Briefcase will
+    use existing template without updating.
 
 *************************************************************************
 """
diff --git a/src/briefcase/commands/create.py b/src/briefcase/commands/create.py
index 7d7585be..4f29c373 100644
--- a/src/briefcase/commands/create.py
+++ b/src/briefcase/commands/create.py
@@ -488,6 +488,8 @@ class CreateCommand(BaseCommand):
                         [
                             sys.executable,
                             "-u",
+                            "-X",
+                            "utf8",
                             "-m",
                             "pip",
                             "install",
@@ -500,6 +502,7 @@ class CreateCommand(BaseCommand):
                         + self._extra_pip_args(app)
                         + self._pip_requires(app, requires),
                         check=True,
+                        encoding="UTF-8",
                         **self._pip_kwargs(app),
                     )
                 except subprocess.CalledProcessError as e:
diff --git a/src/briefcase/commands/dev.py b/src/briefcase/commands/dev.py
index 075ae524..5380c2b1 100644
--- a/src/briefcase/commands/dev.py
+++ b/src/briefcase/commands/dev.py
@@ -91,6 +91,8 @@ class DevCommand(RunAppMixin, BaseCommand):
                         [
                             sys.executable,
                             "-u",
+                            "-X",
+                            "utf8",
                             "-m",
                             "pip",
                             "install",
@@ -98,6 +100,7 @@ class DevCommand(RunAppMixin, BaseCommand):
                         ]
                         + requires,
                         check=True,
+                        encoding="UTF-8",
                     )
                 except subprocess.CalledProcessError as e:
                     raise RequirementsInstallError() from e
@@ -126,6 +129,7 @@ class DevCommand(RunAppMixin, BaseCommand):
 
         app_popen = self.tools.subprocess.Popen(
             [
+                # Do not add additional switches for sys.executable; see DEV_ENVIRONMENT
                 sys.executable,
                 "-c",
                 (
@@ -136,6 +140,7 @@ class DevCommand(RunAppMixin, BaseCommand):
                 ),
             ],
             env=env,
+            encoding="UTF-8",
             cwd=self.tools.home_path,
             stdout=subprocess.PIPE,
             stderr=subprocess.STDOUT,
diff --git a/src/briefcase/integrations/android_sdk.py b/src/briefcase/integrations/android_sdk.py
index 144ad802..ad3f776a 100644
--- a/src/briefcase/integrations/android_sdk.py
+++ b/src/briefcase/integrations/android_sdk.py
@@ -1120,7 +1120,6 @@ An emulator named '{avd}' already exists.
 
 """
                 )
-                self.tools.logger.info()
             else:
                 avd_is_invalid = False
 
@@ -1558,6 +1557,8 @@ Activity class not found while starting app.
         :param pid: The PID whose logs you want to display.
         :returns: A Popen object for the logcat call
         """
+        # As best as we can make out, adb logcat returns UTF-8 output.
+        # See #1425 for details.
         return self.tools.subprocess.Popen(
             [
                 os.fsdecode(self.tools.android_sdk.adb_path),
@@ -1571,6 +1572,7 @@ Activity class not found while starting app.
             # Filter out some noisy and useless tags.
             + [f"{tag}:S" for tag in ["EGL_emulation"]],
             env=self.tools.android_sdk.env,
+            encoding="UTF-8",
             stdout=subprocess.PIPE,
             stderr=subprocess.STDOUT,
             bufsize=1,
@@ -1583,6 +1585,8 @@ Activity class not found while starting app.
         :param since: The start time from which logs should be displayed
         """
         try:
+            # As best as we can make out, adb logcat returns UTF-8 output.
+            # See #1425 for details.
             self.tools.subprocess.run(
                 [
                     os.fsdecode(self.tools.android_sdk.adb_path),
@@ -1602,6 +1606,7 @@ Activity class not found while starting app.
                 ],
                 env=self.tools.android_sdk.env,
                 check=True,
+                encoding="UTF-8",
             )
         except subprocess.CalledProcessError as e:
             raise BriefcaseCommandError("Error starting ADB logcat.") from e
diff --git a/src/briefcase/integrations/base.py b/src/briefcase/integrations/base.py
index 472c4374..a359b230 100644
--- a/src/briefcase/integrations/base.py
+++ b/src/briefcase/integrations/base.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import locale
 import os
 import platform
 import shutil
@@ -7,6 +8,7 @@ import sys
 from abc import ABC, abstractmethod
 from collections import defaultdict
 from collections.abc import Mapping
+from functools import cached_property
 from pathlib import Path
 from typing import TYPE_CHECKING, DefaultDict, TypeVar
 
@@ -45,6 +47,8 @@ ManagedToolT = TypeVar("ManagedToolT", bound="ManagedTool")
 # Registry of all defined Tools
 tool_registry: dict[str, type[Tool | ManagedTool]] = dict()
 
+DEFAULT_SYSTEM_ENCODING = "UTF-8"
+
 
 class Tool(ABC):
     """Tool Base."""
@@ -207,6 +211,29 @@ class ToolCache(Mapping):
             )
         )
 
+    @cached_property
+    def system_encoding(self) -> str:
+        """The character encoding for the system's locale.
+
+        This locale API tries to determine the system's default encoding and generally
+        works on typically configured systems; although, there are potential pitfalls
+        in certain situations...so, this is best-effort.
+
+        This API is used over getpreferredencoding() to avoid respecting Python's UTF-8
+        mode; the system may not be using UTF-8 even if Python is configured to use it.
+
+        :returns: a character encoding (upper-cased), e.g. UTF-8. Defaults to UTF-8.
+        """
+        if sys.version_info < (3, 11):  # pragma: no-cover-if-gte-py311
+            encoding = locale.getdefaultlocale()[1]  # deprecated in Python 3.11
+        else:  # pragma: no-cover-if-lt-py311
+            encoding = locale.getencoding()
+
+        if not encoding:
+            encoding = DEFAULT_SYSTEM_ENCODING
+
+        return encoding.upper()
+
     def __getitem__(self, app: AppConfig) -> ToolCache:
         return self.app_tools[app]
 
diff --git a/src/briefcase/integrations/subprocess.py b/src/briefcase/integrations/subprocess.py
index 0fc27b6e..0a54e0ad 100644
--- a/src/briefcase/integrations/subprocess.py
+++ b/src/briefcase/integrations/subprocess.py
@@ -214,31 +214,33 @@ class Subprocess(Tool):
         except KeyError:
             pass
 
-        # if `text` or backwards-compatible `universal_newlines` are
-        # not provided, then default `text` to True so all output is
-        # returned as strings instead of bytes.
-        if "text" not in kwargs and "universal_newlines" not in kwargs:
-            kwargs["text"] = True
-
-        # If we're in text/universal_newlines mode, ensure that there is
-        # an encoding specified. If encoding isn't specified, default to
-        # the system's stdout encoding.
-        # This is for the benefit of Windows, which uses cp437 for console
-        # output when the system encoding is cp1252.
-        # `__stdout__` is used because rich captures and redirects `sys.stdout`.
-        # The fallback to "UTF-8" is needed to catch the case where stdout
-        # is redirected to a non-tty (e.g. during pytest conditions)
-        if kwargs.get("text") or kwargs.get("universal_newlines", False):
+        # Default to running subprocess in "text" mode so all output is returned as
+        # strings instead of bytes. The legacy setting of `universal_newlines` is
+        # converted to `text` for calling subprocess.
+        kwargs["text"] = kwargs.pop("universal_newlines", kwargs.get("text", True))
+
+        # Enable text mode if an `encoding` or `errors` is specified; this aligns with
+        # the default behavior of subprocess.
+        kwargs["text"] |= any(kwargs.get(kw) for kw in ["encoding", "errors"])
+
+        # Configure subprocess for text mode
+        if kwargs["text"]:
+            # Ensure an appropriate encoding is specified.
+            # If an encoding isn't provided, default to the encoding that Python is
+            # using for stdout. This is for the benefit of Windows which uses cp437 for
+            # console output when the system encoding is cp1252.
+            # `sys.__stdout__` is used because Rich captures and redirects `sys.stdout`.
+            # The fallback to "UTF-8" is needed to catch the case where stdout is
+            # redirected to a non-tty (e.g. during pytest conditions).
             kwargs.setdefault(
                 "encoding", os.device_encoding(sys.__stdout__.fileno()) or "UTF-8"
             )
 
-        # When text mode is enabled, subprocess defaults to "strict"
-        # handling for errors arising from decoding output to Unicode.
-        # To avoid Unicode exceptions from misbehaving commands, set
-        # `errors` so output that cannot be decoded for the specified
-        # encoding are replaced with hex of the raw bytes.
-        if any(kwargs.get(kw) for kw in ["text", "encoding", "universal_encoding"]):
+            # Use relaxed output decoding by default.
+            # subprocess defaults to "strict" handling for errors arising from decoding
+            # output to Unicode. To avoid Unicode exceptions from misbehaving commands,
+            # set `errors` so output that cannot be decoded for the specified encoding
+            # are replaced with hex of the raw bytes.
             kwargs.setdefault("errors", "backslashreplace")
 
         # For Windows, convert start_new_session=True to creation flags
diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py
index 950d0197..89f73026 100644
--- a/src/briefcase/platforms/android/gradle.py
+++ b/src/briefcase/platforms/android/gradle.py
@@ -126,6 +126,12 @@ class GradleMixin:
             # project root, i.e., to avoid 'Task assembleDebug not found'.
             cwd=self.bundle_path(app),
             check=True,
+            # Gradle writes to stdout using the system encoding. So, explicitly use it
+            # here to avoid defaulting to the console encoding for the subprocess call.
+            # This is mostly for the benefit of Windows where the system encoding may
+            # not be the same as the console encoding and typically neither are UTF-8.
+            # See #1425 for details.
+            encoding=self.tools.system_encoding,
         )
 
     def verify_tools(self):
diff --git a/src/briefcase/platforms/linux/__init__.py b/src/briefcase/platforms/linux/__init__.py
index 339b9fe9..63ec625d 100644
--- a/src/briefcase/platforms/linux/__init__.py
+++ b/src/briefcase/platforms/linux/__init__.py
@@ -165,6 +165,8 @@ class LocalRequirementsMixin:  # pragma: no-cover-if-is-windows
                             self.tools.subprocess.check_output(
                                 [
                                     sys.executable,
+                                    "-X",
+                                    "utf8",
                                     "-m",
                                     "build",
                                     "--sdist",
@@ -172,6 +174,7 @@ class LocalRequirementsMixin:  # pragma: no-cover-if-is-windows
                                     local_requirements_path,
                                     requirement,
                                 ],
+                                encoding="UTF-8",
                             )
                         except subprocess.CalledProcessError as e:
                             raise BriefcaseCommandError(
diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py
index 50dff44e..b4e18b92 100644
--- a/src/briefcase/platforms/web/static.py
+++ b/src/briefcase/platforms/web/static.py
@@ -137,6 +137,8 @@ class StaticWebBuildCommand(StaticWebMixin, BuildCommand):
                     [
                         sys.executable,
                         "-u",
+                        "-X",
+                        "utf8",
                         "-m",
                         "wheel",
                         "pack",
@@ -145,6 +147,7 @@ class StaticWebBuildCommand(StaticWebMixin, BuildCommand):
                         self.wheel_path(app),
                     ],
                     check=True,
+                    encoding="UTF-8",
                 )
             except subprocess.CalledProcessError as e:
                 raise BriefcaseCommandError(
@@ -157,6 +160,8 @@ class StaticWebBuildCommand(StaticWebMixin, BuildCommand):
                     [
                         sys.executable,
                         "-u",
+                        "-X",
+                        "utf8",
                         "-m",
                         "pip",
                         "wheel",
@@ -166,6 +171,7 @@ class StaticWebBuildCommand(StaticWebMixin, BuildCommand):
                         self.bundle_path(app) / "requirements.txt",
                     ],
                     check=True,
+                    encoding="UTF-8",
                 )
             except subprocess.CalledProcessError as e:
                 raise BriefcaseCommandError(
diff --git a/src/briefcase/platforms/windows/__init__.py b/src/briefcase/platforms/windows/__init__.py
index c205efd5..dc282170 100644
--- a/src/briefcase/platforms/windows/__init__.py
+++ b/src/briefcase/platforms/windows/__init__.py
@@ -143,6 +143,7 @@ class WindowsRunCommand(RunCommand):
         app_popen = self.tools.subprocess.Popen(
             [os.fsdecode(self.binary_path(app))] + passthrough,
             cwd=self.tools.home_path,
+            encoding="UTF-8",
             stdout=subprocess.PIPE,
             stderr=subprocess.STDOUT,
             bufsize=1,


Test patch:
diff --git a/tests/commands/base/test_validate_locale.py b/tests/commands/base/test_validate_locale.py
new file mode 100644
index 00000000..50a174a3
--- /dev/null
+++ b/tests/commands/base/test_validate_locale.py
@@ -0,0 +1,22 @@
+from unittest.mock import PropertyMock
+
+
+def test_supported_encoding(base_command, capsys, monkeypatch):
+    """User is not warned for a supported encoding."""
+    monkeypatch.setattr(
+        type(base_command.tools), "system_encoding", PropertyMock(return_value="UTF-8")
+    )
+    base_command.validate_locale()
+    assert "Default system encoding is not supported" not in capsys.readouterr().out
+
+
+def test_unsupported_encoding(base_command, capsys, monkeypatch):
+    """User is warned for an unsupported encoding on Linux."""
+    base_command.tools.host_os = "Linux"
+    monkeypatch.setattr(
+        type(base_command.tools),
+        "system_encoding",
+        PropertyMock(return_value="ISO-NOPE"),
+    )
+    base_command.validate_locale()
+    assert "Default system encoding is not supported" in capsys.readouterr().out
diff --git a/tests/commands/create/test_install_app_requirements.py b/tests/commands/create/test_install_app_requirements.py
index 20fc232e..206cc550 100644
--- a/tests/commands/create/test_install_app_requirements.py
+++ b/tests/commands/create/test_install_app_requirements.py
@@ -120,6 +120,8 @@ def test_app_packages_valid_requires(
         [
             sys.executable,
             "-u",
+            "-X",
+            "utf8",
             "-m",
             "pip",
             "install",
@@ -133,6 +135,7 @@ def test_app_packages_valid_requires(
             "third>=3.2.1",
         ],
         check=True,
+        encoding="UTF-8",
         env={
             "PYTHONPATH": str(
                 create_command.bundle_path(myapp)
@@ -171,6 +174,8 @@ def test_app_packages_valid_requires_no_support_package(
         [
             sys.executable,
             "-u",
+            "-X",
+            "utf8",
             "-m",
             "pip",
             "install",
@@ -184,6 +189,7 @@ def test_app_packages_valid_requires_no_support_package(
             "third>=3.2.1",
         ],
         check=True,
+        encoding="UTF-8",
     )
 
     # Original app definitions haven't changed
@@ -217,6 +223,8 @@ def test_app_packages_invalid_requires(
         [
             sys.executable,
             "-u",
+            "-X",
+            "utf8",
             "-m",
             "pip",
             "install",
@@ -228,6 +236,7 @@ def test_app_packages_invalid_requires(
             "does-not-exist",
         ],
         check=True,
+        encoding="UTF-8",
         env={
             "PYTHONPATH": str(
                 create_command.bundle_path(myapp)
@@ -270,6 +279,8 @@ def test_app_packages_offline(
         [
             sys.executable,
             "-u",
+            "-X",
+            "utf8",
             "-m",
             "pip",
             "install",
@@ -283,6 +294,7 @@ def test_app_packages_offline(
             "third",
         ],
         check=True,
+        encoding="UTF-8",
         env={
             "PYTHONPATH": str(
                 create_command.bundle_path(myapp)
@@ -325,6 +337,8 @@ def test_app_packages_install_requirements(
         [
             sys.executable,
             "-u",
+            "-X",
+            "utf8",
             "-m",
             "pip",
             "install",
@@ -338,6 +352,7 @@ def test_app_packages_install_requirements(
             "third",
         ],
         check=True,
+        encoding="UTF-8",
         env={
             "PYTHONPATH": str(
                 create_command.bundle_path(myapp)
@@ -390,6 +405,8 @@ def test_app_packages_replace_existing_requirements(
         [
             sys.executable,
             "-u",
+            "-X",
+            "utf8",
             "-m",
             "pip",
             "install",
@@ -403,6 +420,7 @@ def test_app_packages_replace_existing_requirements(
             "third",
         ],
         check=True,
+        encoding="UTF-8",
         env={
             "PYTHONPATH": str(
                 create_command.bundle_path(myapp)
@@ -680,6 +698,8 @@ def test_app_packages_test_requires(
         [
             sys.executable,
             "-u",
+            "-X",
+            "utf8",
             "-m",
             "pip",
             "install",
@@ -693,6 +713,7 @@ def test_app_packages_test_requires(
             "third>=3.2.1",
         ],
         check=True,
+        encoding="UTF-8",
         env={
             "PYTHONPATH": str(
                 create_command.bundle_path(myapp)
@@ -726,6 +747,8 @@ def test_app_packages_test_requires_test_mode(
         [
             sys.executable,
             "-u",
+            "-X",
+            "utf8",
             "-m",
             "pip",
             "install",
@@ -741,6 +764,7 @@ def test_app_packages_test_requires_test_mode(
             "pytest-tldr",
         ],
         check=True,
+        encoding="UTF-8",
         env={
             "PYTHONPATH": str(
                 create_command.bundle_path(myapp)
@@ -775,6 +799,8 @@ def test_app_packages_only_test_requires_test_mode(
         [
             sys.executable,
             "-u",
+            "-X",
+            "utf8",
             "-m",
             "pip",
             "install",
@@ -787,6 +813,7 @@ def test_app_packages_only_test_requires_test_mode(
             "pytest-tldr",
         ],
         check=True,
+        encoding="UTF-8",
         env={
             "PYTHONPATH": str(
                 create_command.bundle_path(myapp)
diff --git a/tests/commands/dev/test_install_dev_requirements.py b/tests/commands/dev/test_install_dev_requirements.py
index cc0614ba..b8a02705 100644
--- a/tests/commands/dev/test_install_dev_requirements.py
+++ b/tests/commands/dev/test_install_dev_requirements.py
@@ -16,6 +16,8 @@ def test_install_requirements_no_error(dev_command, first_app):
         [
             sys.executable,
             "-u",
+            "-X",
+            "utf8",
             "-m",
             "pip",
             "install",
@@ -25,6 +27,7 @@ def test_install_requirements_no_error(dev_command, first_app):
             "packagethree",
         ],
         check=True,
+        encoding="UTF-8",
     )
 
 
@@ -46,6 +49,8 @@ def test_install_requirements_error(dev_command, first_app):
         [
             sys.executable,
             "-u",
+            "-X",
+            "utf8",
             "-m",
             "pip",
             "install",
@@ -55,6 +60,7 @@ def test_install_requirements_error(dev_command, first_app):
             "packagethree",
         ],
         check=True,
+        encoding="UTF-8",
     )
 
 
@@ -78,6 +84,8 @@ def test_install_requirements_test_mode(dev_command, first_app):
         [
             sys.executable,
             "-u",
+            "-X",
+            "utf8",
             "-m",
             "pip",
             "install",
@@ -89,6 +97,7 @@ def test_install_requirements_test_mode(dev_command, first_app):
             "test_two",
         ],
         check=True,
+        encoding="UTF-8",
     )
 
 
@@ -103,6 +112,8 @@ def test_only_test_requirements(dev_command, first_app):
         [
             sys.executable,
             "-u",
+            "-X",
+            "utf8",
             "-m",
             "pip",
             "install",
@@ -111,4 +122,5 @@ def test_only_test_requirements(dev_command, first_app):
             "test_two",
         ],
         check=True,
+        encoding="UTF-8",
     )
diff --git a/tests/commands/dev/test_run_dev_app.py b/tests/commands/dev/test_run_dev_app.py
index 2fe86fec..9cec5017 100644
--- a/tests/commands/dev/test_run_dev_app.py
+++ b/tests/commands/dev/test_run_dev_app.py
@@ -39,6 +39,7 @@ def test_dev_run(dev_command, first_app, tmp_path):
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
         bufsize=1,
+        encoding="UTF-8",
     )
     dev_command._stream_app_logs.assert_called_once_with(
         first_app,
@@ -84,6 +85,7 @@ def test_dev_run_with_args(dev_command, first_app, tmp_path):
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
         bufsize=1,
+        encoding="UTF-8",
     )
     dev_command._stream_app_logs.assert_called_once_with(
         first_app,
@@ -129,6 +131,7 @@ def test_dev_test_mode(dev_command, first_app, tmp_path):
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
         bufsize=1,
+        encoding="UTF-8",
     )
     dev_command._stream_app_logs.assert_called_once_with(
         first_app,
@@ -139,7 +142,7 @@ def test_dev_test_mode(dev_command, first_app, tmp_path):
 
 
 def test_dev_test_mode_with_args(dev_command, first_app, tmp_path):
-    "The test suite can be run in development mode with args"
+    """The test suite can be run in development mode with args."""
     dev_command._stream_app_logs = mock.MagicMock()
     app_popen = mock.MagicMock()
     dev_command.tools.subprocess.Popen.return_value = app_popen
@@ -174,6 +177,7 @@ def test_dev_test_mode_with_args(dev_command, first_app, tmp_path):
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
         bufsize=1,
+        encoding="UTF-8",
     )
     dev_command._stream_app_logs.assert_called_once_with(
         first_app,
diff --git a/tests/integrations/android_sdk/ADB/test_logcat.py b/tests/integrations/android_sdk/ADB/test_logcat.py
index 0ec09a81..c9ee173e 100644
--- a/tests/integrations/android_sdk/ADB/test_logcat.py
+++ b/tests/integrations/android_sdk/ADB/test_logcat.py
@@ -25,6 +25,7 @@ def test_logcat(mock_tools, adb):
             "EGL_emulation:S",
         ],
         env=mock_tools.android_sdk.env,
+        encoding="UTF-8",
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
         bufsize=1,
diff --git a/tests/integrations/android_sdk/ADB/test_logcat_tail.py b/tests/integrations/android_sdk/ADB/test_logcat_tail.py
index 61854241..f80c449b 100644
--- a/tests/integrations/android_sdk/ADB/test_logcat_tail.py
+++ b/tests/integrations/android_sdk/ADB/test_logcat_tail.py
@@ -31,6 +31,7 @@ def test_logcat_tail(mock_tools, adb):
         ],
         env=mock_tools.android_sdk.env,
         check=True,
+        encoding="UTF-8",
     )
 
 
diff --git a/tests/integrations/base/test_ToolCache.py b/tests/integrations/base/test_ToolCache.py
index ac6dddb6..c437f599 100644
--- a/tests/integrations/base/test_ToolCache.py
+++ b/tests/integrations/base/test_ToolCache.py
@@ -1,9 +1,11 @@
 import importlib
+import locale
 import os
 import platform
 import shutil
 import sys
 from pathlib import Path
+from unittest.mock import MagicMock
 
 import pytest
 import requests
@@ -179,3 +181,27 @@ def test_is_32bit_python(maxsize, is_32bit, monkeypatch, tmp_path):
     )
 
     assert tools.is_32bit_python is is_32bit
+
+
+@pytest.mark.parametrize(
+    "mock_encoding, expected_encoding",
+    [
+        ("iso-123", "ISO-123"),
+        ("", "ISO-4242"),
+        (None, "ISO-4242"),
+    ],
+)
+def test_system_encoding(simple_tools, mock_encoding, expected_encoding, monkeypatch):
+    """The expected system encoding is returned."""
+    if sys.version_info < (3, 11):
+        monkeypatch.setattr(
+            locale, "getdefaultlocale", MagicMock(return_value=("aa_BB", mock_encoding))
+        )
+    else:
+        monkeypatch.setattr(
+            locale, "getencoding", MagicMock(return_value=mock_encoding)
+        )
+    monkeypatch.setattr(
+        briefcase.integrations.base, "DEFAULT_SYSTEM_ENCODING", "ISO-4242"
+    )
+    assert simple_tools.system_encoding == expected_encoding
diff --git a/tests/integrations/docker/test_DockerAppContext__check_output.py b/tests/integrations/docker/test_DockerAppContext__check_output.py
index f036a413..c2f641be 100644
--- a/tests/integrations/docker/test_DockerAppContext__check_output.py
+++ b/tests/integrations/docker/test_DockerAppContext__check_output.py
@@ -109,11 +109,10 @@ def test_call_with_arg_and_env(
             "MAGIC": "True",
             "IMPORTANCE": "super high",
         },
-        universal_newlines=True,
+        extra_kw="extra",
     )
     assert output == "goodbye\n"
 
-    sub_check_output_kw.pop("text")
     mock_docker_app_context.tools.subprocess._subprocess.check_output.assert_called_once_with(
         [
             "docker",
@@ -131,7 +130,7 @@ def test_call_with_arg_and_env(
             "hello",
             "world",
         ],
-        universal_newlines=True,
+        extra_kw="extra",
         **sub_check_output_kw,
     )
     assert capsys.readouterr().out == ""
diff --git a/tests/integrations/docker/test_DockerAppContext__run.py b/tests/integrations/docker/test_DockerAppContext__run.py
index 429181b5..80d4b7f2 100644
--- a/tests/integrations/docker/test_DockerAppContext__run.py
+++ b/tests/integrations/docker/test_DockerAppContext__run.py
@@ -159,10 +159,9 @@ def test_call_with_arg_and_env(
             "MAGIC": "True",
             "IMPORTANCE": "super high",
         },
-        universal_newlines=True,
+        extra_kw="extra",
     )
 
-    sub_stream_kw.pop("text")
     mock_docker_app_context.tools.subprocess._subprocess.Popen.assert_called_once_with(
         [
             "docker",
@@ -180,7 +179,7 @@ def test_call_with_arg_and_env(
             "hello",
             "world",
         ],
-        universal_newlines=True,
+        extra_kw="extra",
         **sub_stream_kw,
     )
     assert capsys.readouterr().out == (
diff --git a/tests/integrations/subprocess/test_Subprocess__Popen.py b/tests/integrations/subprocess/test_Subprocess__Popen.py
index 1b6188fd..bdc9e57c 100644
--- a/tests/integrations/subprocess/test_Subprocess__Popen.py
+++ b/tests/integrations/subprocess/test_Subprocess__Popen.py
@@ -21,12 +21,11 @@ def test_call(mock_sub, capsys, platform, sub_kw):
 def test_call_with_arg(mock_sub, capsys, sub_kw):
     """Any extra keyword arguments are passed through as-is."""
 
-    mock_sub.Popen(["hello", "world"], universal_newlines=True)
+    mock_sub.Popen(["hello", "world"], extra_kw="extra")
 
-    sub_kw.pop("text")
     mock_sub._subprocess.Popen.assert_called_with(
         ["hello", "world"],
-        universal_newlines=True,
+        extra_kw="extra",
         **sub_kw,
     )
     assert capsys.readouterr().out == ""
@@ -172,16 +171,16 @@ def test_debug_call_with_env(mock_sub, capsys, tmp_path, sub_kw):
         ({}, {"text": True, "encoding": ANY, "errors": "backslashreplace"}),
         ({"text": True}, {"text": True, "encoding": ANY, "errors": "backslashreplace"}),
         ({"text": False}, {"text": False}),
-        ({"universal_newlines": False}, {"universal_newlines": False}),
+        ({"universal_newlines": False}, {"text": False}),
         (
             {"universal_newlines": True},
-            {"universal_newlines": True, "encoding": ANY, "errors": "backslashreplace"},
+            {"text": True, "encoding": ANY, "errors": "backslashreplace"},
         ),
     ],
 )
 def test_text_eq_true_default_overriding(mock_sub, in_kwargs, kwargs):
     """If text or universal_newlines is explicitly provided, those should override
-    text=true default."""
+    text=true default and universal_newlines should be converted to text."""
 
     mock_sub.Popen(["hello", "world"], **in_kwargs)
     mock_sub._subprocess.Popen.assert_called_with(["hello", "world"], **kwargs)
diff --git a/tests/integrations/subprocess/test_Subprocess__check_output.py b/tests/integrations/subprocess/test_Subprocess__check_output.py
index ac8c5ed2..512f37f8 100644
--- a/tests/integrations/subprocess/test_Subprocess__check_output.py
+++ b/tests/integrations/subprocess/test_Subprocess__check_output.py
@@ -26,12 +26,11 @@ def test_call(mock_sub, capsys, platform, sub_check_output_kw):
 def test_call_with_arg(mock_sub, capsys, sub_check_output_kw):
     """Any extra keyword arguments are passed through as-is."""
 
-    mock_sub.check_output(["hello", "world"], universal_newlines=True)
+    mock_sub.check_output(["hello", "world"], extra_kw="extra")
 
-    sub_check_output_kw.pop("text")
     mock_sub._subprocess.check_output.assert_called_with(
         ["hello", "world"],
-        universal_newlines=True,
+        extra_kw="extra",
         **sub_check_output_kw,
     )
     assert capsys.readouterr().out == ""
@@ -353,12 +352,12 @@ def test_calledprocesserror_exception_logging_no_cmd_output(mock_sub, capsys):
         ({"text": False}, {"text": False, "stderr": subprocess.STDOUT}),
         (
             {"universal_newlines": False},
-            {"universal_newlines": False, "stderr": subprocess.STDOUT},
+            {"text": False, "stderr": subprocess.STDOUT},
         ),
         (
             {"universal_newlines": True},
             {
-                "universal_newlines": True,
+                "text": True,
                 "encoding": ANY,
                 "stderr": subprocess.STDOUT,
                 "errors": "backslashreplace",
@@ -368,7 +367,7 @@ def test_calledprocesserror_exception_logging_no_cmd_output(mock_sub, capsys):
 )
 def test_text_eq_true_default_overriding(mock_sub, in_kwargs, kwargs):
     """If text or universal_newlines is explicitly provided, those should override
-    text=true default."""
+    text=true default and universal_newlines should be converted to text."""
     mock_sub.check_output(["hello", "world"], stderr=subprocess.STDOUT, **in_kwargs)
 
     mock_sub._subprocess.check_output.assert_called_with(["hello", "world"], **kwargs)
diff --git a/tests/integrations/subprocess/test_Subprocess__final_kwargs.py b/tests/integrations/subprocess/test_Subprocess__final_kwargs.py
index 164e628d..6ac5bf8b 100644
--- a/tests/integrations/subprocess/test_Subprocess__final_kwargs.py
+++ b/tests/integrations/subprocess/test_Subprocess__final_kwargs.py
@@ -85,14 +85,10 @@ def test_non_str_cwd_provided(mock_sub):
             {"text": True, "encoding": CONSOLE_ENCODING, "errors": "backslashreplace"},
         ),
         ({"text": False}, {"text": False}),
-        ({"universal_newlines": False}, {"universal_newlines": False}),
+        ({"universal_newlines": False}, {"text": False}),
         (
             {"universal_newlines": True},
-            {
-                "universal_newlines": True,
-                "encoding": CONSOLE_ENCODING,
-                "errors": "backslashreplace",
-            },
+            {"text": True, "encoding": CONSOLE_ENCODING, "errors": "backslashreplace"},
         ),
         # Explicit encoding provided
         (
@@ -105,23 +101,15 @@ def test_non_str_cwd_provided(mock_sub):
         ),
         (
             {"text": False, "encoding": "ibm850"},
-            {"text": False, "encoding": "ibm850", "errors": "backslashreplace"},
+            {"text": True, "encoding": "ibm850", "errors": "backslashreplace"},
         ),
         (
             {"universal_newlines": False, "encoding": "ibm850"},
-            {
-                "universal_newlines": False,
-                "encoding": "ibm850",
-                "errors": "backslashreplace",
-            },
+            {"text": True, "encoding": "ibm850", "errors": "backslashreplace"},
         ),
         (
             {"universal_newlines": True, "encoding": "ibm850"},
-            {
-                "universal_newlines": True,
-                "encoding": "ibm850",
-                "errors": "backslashreplace",
-            },
+            {"text": True, "encoding": "ibm850", "errors": "backslashreplace"},
         ),
         # Explicit errors provided
         (
@@ -138,19 +126,15 @@ def test_non_str_cwd_provided(mock_sub):
         ),
         (
             {"text": False, "errors": "emojireplace"},
-            {"text": False, "errors": "emojireplace"},
+            {"text": True, "encoding": CONSOLE_ENCODING, "errors": "emojireplace"},
         ),
         (
             {"universal_newlines": True, "errors": "emojireplace"},
-            {
-                "universal_newlines": True,
-                "encoding": CONSOLE_ENCODING,
-                "errors": "emojireplace",
-            },
+            {"text": True, "encoding": CONSOLE_ENCODING, "errors": "emojireplace"},
         ),
         (
             {"universal_newlines": False, "errors": "emojireplace"},
-            {"universal_newlines": False, "errors": "emojireplace"},
+            {"text": True, "encoding": CONSOLE_ENCODING, "errors": "emojireplace"},
         ),
     ],
 )
diff --git a/tests/integrations/subprocess/test_Subprocess__parse_output.py b/tests/integrations/subprocess/test_Subprocess__parse_output.py
index 67d59d97..01cf9c56 100644
--- a/tests/integrations/subprocess/test_Subprocess__parse_output.py
+++ b/tests/integrations/subprocess/test_Subprocess__parse_output.py
@@ -44,12 +44,14 @@ def test_call_with_arg(mock_sub, capsys, sub_check_output_kw):
     """Any extra keyword arguments are passed through as-is to check_output."""
 
     output = mock_sub.parse_output(
-        splitlines_parser, ["hello", "world"], extra_arg="asdf"
+        splitlines_parser,
+        ["hello", "world"],
+        extra_kw="extra",
     )
 
     mock_sub._subprocess.check_output.assert_called_with(
         ["hello", "world"],
-        extra_arg="asdf",
+        extra_kw="extra",
         **sub_check_output_kw,
     )
     assert capsys.readouterr().out == ""
@@ -102,16 +104,16 @@ def test_call_with_parser_error(mock_sub, capsys, sub_check_output_kw):
         ({}, {"text": True, "encoding": ANY, "errors": "backslashreplace"}),
         ({"text": True}, {"text": True, "encoding": ANY, "errors": "backslashreplace"}),
         ({"text": False}, {"text": False}),
-        ({"universal_newlines": False}, {"universal_newlines": False}),
+        ({"universal_newlines": False}, {"text": False}),
         (
             {"universal_newlines": True},
-            {"universal_newlines": True, "encoding": ANY, "errors": "backslashreplace"},
+            {"text": True, "encoding": ANY, "errors": "backslashreplace"},
         ),
     ],
 )
 def test_text_eq_true_default_overriding(mock_sub, in_kwargs, kwargs):
     """If text or universal_newlines is explicitly provided, those should override
-    text=true default."""
+    text=true default and universal_newlines should be converted to text."""
 
     mock_sub.parse_output(splitlines_parser, ["hello", "world"], **in_kwargs)
 
diff --git a/tests/integrations/subprocess/test_Subprocess__run__controlled_console.py b/tests/integrations/subprocess/test_Subprocess__run__controlled_console.py
index 35e5c1dc..bf409dc6 100644
--- a/tests/integrations/subprocess/test_Subprocess__run__controlled_console.py
+++ b/tests/integrations/subprocess/test_Subprocess__run__controlled_console.py
@@ -28,12 +28,11 @@ def test_call_with_arg(mock_sub, sub_stream_kw, sleep_zero, capsys):
     """Any extra keyword arguments are passed through as-is."""
 
     with mock_sub.tools.input.wait_bar():
-        mock_sub.run(["hello", "world"], universal_newlines=True)
+        mock_sub.run(["hello", "world"], extra_kw="extra")
 
-    sub_stream_kw.pop("text")
     mock_sub._subprocess.Popen.assert_called_with(
         ["hello", "world"],
-        universal_newlines=True,
+        extra_kw="extra",
         **sub_stream_kw,
     )
     # fmt: off
@@ -121,21 +120,16 @@ def test_debug_call_with_env(mock_sub, sub_stream_kw, sleep_zero, capsys, tmp_pa
         ),
         ({"text": False}, {"text": False, "bufsize": 1}),
         ({"text": False, "bufsize": 42}, {"text": False, "bufsize": 42}),
-        ({"universal_newlines": False}, {"universal_newlines": False, "bufsize": 1}),
+        ({"universal_newlines": False}, {"text": False, "bufsize": 1}),
         (
             {"universal_newlines": True},
-            {
-                "universal_newlines": True,
-                "encoding": ANY,
-                "bufsize": 1,
-                "errors": "backslashreplace",
-            },
+            {"text": True, "encoding": ANY, "bufsize": 1, "errors": "backslashreplace"},
         ),
     ],
 )
 def test_text_eq_true_default_overriding(mock_sub, in_kwargs, kwargs, sleep_zero):
     """If text or universal_newlines is explicitly provided, those should override
-    text=true default."""
+    text=true default and universal_newlines should be converted to text."""
     with mock_sub.tools.input.wait_bar():
         mock_sub.run(["hello", "world"], **in_kwargs)
 
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 6e2751f3..694c3b6a 100644
--- a/tests/integrations/subprocess/test_Subprocess__run__stream_output__False.py
+++ b/tests/integrations/subprocess/test_Subprocess__run__stream_output__False.py
@@ -22,12 +22,11 @@ def test_call(mock_sub, capsys, platform, sub_kw):
 def test_call_with_arg(mock_sub, capsys, sub_kw):
     """Any extra keyword arguments are passed through as-is."""
 
-    mock_sub.run(["hello", "world"], universal_newlines=True, stream_output=False)
+    mock_sub.run(["hello", "world"], extra_kw="extra", stream_output=False)
 
-    sub_kw.pop("text")
     mock_sub._subprocess.run.assert_called_with(
         ["hello", "world"],
-        universal_newlines=True,
+        extra_kw="extra",
         **sub_kw,
     )
     assert capsys.readouterr().out == ""
@@ -212,16 +211,16 @@ def test_calledprocesserror_exception_logging(mock_sub, capsys):
         ({}, {"text": True, "encoding": ANY, "errors": "backslashreplace"}),
         ({"text": True}, {"text": True, "encoding": ANY, "errors": "backslashreplace"}),
         ({"text": False}, {"text": False}),
-        ({"universal_newlines": False}, {"universal_newlines": False}),
+        ({"universal_newlines": False}, {"text": False}),
         (
             {"universal_newlines": True},
-            {"universal_newlines": True, "encoding": ANY, "errors": "backslashreplace"},
+            {"text": True, "encoding": ANY, "errors": "backslashreplace"},
         ),
     ],
 )
 def test_text_eq_true_default_overriding(mock_sub, in_kwargs, kwargs):
     """If text or universal_newlines is explicitly provided, those should override
-    text=true default."""
+    text=true default and universal_newlines should be converted to text."""
     mock_sub.run(["hello", "world"], stream_output=False, **in_kwargs)
 
     mock_sub._subprocess.run.assert_called_with(["hello", "world"], **kwargs)
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 0a6655ec..491857df 100644
--- a/tests/integrations/subprocess/test_Subprocess__run__stream_output__True.py
+++ b/tests/integrations/subprocess/test_Subprocess__run__stream_output__True.py
@@ -30,12 +30,11 @@ def test_call(mock_sub, capsys, platform, sub_stream_kw, sleep_zero):
 def test_call_with_arg(mock_sub, capsys, sub_stream_kw, sleep_zero):
     """Any extra keyword arguments are passed through as-is."""
 
-    mock_sub.run(["hello", "world"], universal_newlines=True)
+    mock_sub.run(["hello", "world"], extra_kw="extra")
 
-    sub_stream_kw.pop("text")
     mock_sub._subprocess.Popen.assert_called_with(
         ["hello", "world"],
-        universal_newlines=True,
+        extra_kw="extra",
         **sub_stream_kw,
     )
     # fmt: off
@@ -239,16 +238,16 @@ def test_calledprocesserror_exception_logging(mock_sub, sleep_zero, capsys):
         ({}, {"text": True, "encoding": ANY, "errors": "backslashreplace"}),
         ({"text": True}, {"text": True, "encoding": ANY, "errors": "backslashreplace"}),
         ({"text": False}, {"text": False}),
-        ({"universal_newlines": False}, {"universal_newlines": False}),
+        ({"universal_newlines": False}, {"text": False}),
         (
             {"universal_newlines": True},
-            {"universal_newlines": True, "encoding": ANY, "errors": "backslashreplace"},
+            {"text": True, "encoding": ANY, "errors": "backslashreplace"},
         ),
     ],
 )
 def test_text_eq_true_default_overriding(mock_sub, in_kwargs, kwargs, sleep_zero):
     """If text or universal_newlines is explicitly provided, those should override
-    text=true default."""
+    text=true default and universal_newlines should be converted to text."""
     mock_sub.run(["hello", "world"], **in_kwargs)
 
     mock_sub._subprocess.Popen.assert_called_with(
diff --git a/tests/platforms/android/gradle/conftest.py b/tests/platforms/android/gradle/conftest.py
index b9de86b0..4d2046b3 100644
--- a/tests/platforms/android/gradle/conftest.py
+++ b/tests/platforms/android/gradle/conftest.py
@@ -1,6 +1,6 @@
 import os
 import sys
-from unittest.mock import MagicMock
+from unittest.mock import MagicMock, PropertyMock
 
 import pytest
 import requests
@@ -14,7 +14,7 @@ from ....utils import create_file
 
 
 @pytest.fixture
-def package_command(tmp_path, first_app_config):
+def package_command(tmp_path, first_app_config, monkeypatch):
     command = GradlePackageCommand(
         logger=Log(),
         console=Console(),
@@ -27,6 +27,9 @@ def package_command(tmp_path, first_app_config):
     command.tools.sys = MagicMock(spec_set=sys)
     command.tools.requests = MagicMock(spec_set=requests)
     command.tools.subprocess = MagicMock(spec_set=Subprocess)
+    monkeypatch.setattr(
+        type(command.tools), "system_encoding", PropertyMock(return_value="ISO-42")
+    )
 
     # Make sure the dist folder exists
     (tmp_path / "base_path" / "dist").mkdir(parents=True)
diff --git a/tests/platforms/android/gradle/test_build.py b/tests/platforms/android/gradle/test_build.py
index 10a6b696..5fd0b1eb 100644
--- a/tests/platforms/android/gradle/test_build.py
+++ b/tests/platforms/android/gradle/test_build.py
@@ -1,7 +1,7 @@
 import os
 import sys
 from subprocess import CalledProcessError
-from unittest.mock import MagicMock
+from unittest.mock import MagicMock, PropertyMock
 
 import pytest
 import requests
@@ -14,7 +14,7 @@ from briefcase.platforms.android.gradle import GradleBuildCommand
 
 
 @pytest.fixture
-def build_command(tmp_path, first_app_generated):
+def build_command(tmp_path, first_app_generated, monkeypatch):
     command = GradleBuildCommand(
         logger=Log(),
         console=Console(),
@@ -27,6 +27,9 @@ def build_command(tmp_path, first_app_generated):
     command.tools.sys = MagicMock(spec_set=sys)
     command.tools.requests = MagicMock(spec_set=requests)
     command.tools.subprocess = MagicMock(spec_set=Subprocess)
+    monkeypatch.setattr(
+        type(command.tools), "system_encoding", PropertyMock(return_value="ISO-42")
+    )
     return command
 
 
@@ -86,6 +89,7 @@ def test_build_app(
         cwd=build_command.bundle_path(first_app_generated),
         env=build_command.tools.android_sdk.env,
         check=True,
+        encoding="ISO-42",
     )
 
     # The app metadata contains the app module
@@ -152,6 +156,7 @@ def test_build_app_test_mode(
         cwd=build_command.bundle_path(first_app_generated),
         env=build_command.tools.android_sdk.env,
         check=True,
+        encoding="ISO-42",
     )
 
     # The app metadata contains the app module
diff --git a/tests/platforms/android/gradle/test_package__aab.py b/tests/platforms/android/gradle/test_package__aab.py
index dafe0dfb..82a7ab07 100644
--- a/tests/platforms/android/gradle/test_package__aab.py
+++ b/tests/platforms/android/gradle/test_package__aab.py
@@ -106,6 +106,7 @@ def test_execute_gradle(
         cwd=package_command.bundle_path(first_app_aab),
         env=package_command.tools.android_sdk.env,
         check=True,
+        encoding="ISO-42",
     )
 
     # The release asset has been moved into the dist folder
diff --git a/tests/platforms/android/gradle/test_package__apk.py b/tests/platforms/android/gradle/test_package__apk.py
index c9b8e720..0eacf780 100644
--- a/tests/platforms/android/gradle/test_package__apk.py
+++ b/tests/platforms/android/gradle/test_package__apk.py
@@ -107,6 +107,7 @@ def test_execute_gradle(
         cwd=package_command.bundle_path(first_app_apk),
         env=package_command.tools.android_sdk.env,
         check=True,
+        encoding="ISO-42",
     )
 
     # The release asset has been moved into the dist folder
diff --git a/tests/platforms/android/gradle/test_package__debug_apk.py b/tests/platforms/android/gradle/test_package__debug_apk.py
index 0d1497ad..b75b595f 100644
--- a/tests/platforms/android/gradle/test_package__debug_apk.py
+++ b/tests/platforms/android/gradle/test_package__debug_apk.py
@@ -107,6 +107,7 @@ def test_execute_gradle(
         cwd=package_command.bundle_path(first_app_apk),
         env=package_command.tools.android_sdk.env,
         check=True,
+        encoding="ISO-42",
     )
 
     # The release asset has been moved into the dist folder
diff --git a/tests/platforms/iOS/xcode/test_create.py b/tests/platforms/iOS/xcode/test_create.py
index 036614a2..0bb35205 100644
--- a/tests/platforms/iOS/xcode/test_create.py
+++ b/tests/platforms/iOS/xcode/test_create.py
@@ -45,6 +45,8 @@ def test_extra_pip_args(create_command, first_app_generated, tmp_path):
         [
             sys.executable,
             "-u",
+            "-X",
+            "utf8",
             "-m",
             "pip",
             "install",
@@ -60,6 +62,7 @@ def test_extra_pip_args(create_command, first_app_generated, tmp_path):
             "other>=2.3.4",
         ],
         check=True,
+        encoding="UTF-8",
         env={
             "PYTHONPATH": str(
                 tmp_path
diff --git a/tests/platforms/iOS/xcode/test_update.py b/tests/platforms/iOS/xcode/test_update.py
index 33c26051..fd6d2bdd 100644
--- a/tests/platforms/iOS/xcode/test_update.py
+++ b/tests/platforms/iOS/xcode/test_update.py
@@ -32,6 +32,8 @@ def test_extra_pip_args(update_command, first_app_generated, tmp_path):
         [
             sys.executable,
             "-u",
+            "-X",
+            "utf8",
             "-m",
             "pip",
             "install",
@@ -47,6 +49,7 @@ def test_extra_pip_args(update_command, first_app_generated, tmp_path):
             "other>=2.3.4",
         ],
         check=True,
+        encoding="UTF-8",
         env={
             "PYTHONPATH": str(
                 tmp_path
diff --git a/tests/platforms/linux/test_LocalRequirementsMixin.py b/tests/platforms/linux/test_LocalRequirementsMixin.py
index 34eb2806..d5bdf943 100644
--- a/tests/platforms/linux/test_LocalRequirementsMixin.py
+++ b/tests/platforms/linux/test_LocalRequirementsMixin.py
@@ -174,6 +174,8 @@ def test_install_app_requirements_in_docker(create_command, first_app_config, tm
             "briefcase/com.example.first-app:py3.X",
             "python3.X",
             "-u",
+            "-X",
+            "utf8",
             "-m",
             "pip",
             "install",
@@ -186,6 +188,7 @@ def test_install_app_requirements_in_docker(create_command, first_app_config, tm
             "bar>=4.5",
         ],
         check=True,
+        encoding="UTF-8",
     )
 
     # The local requirements path exists, but is empty
@@ -229,6 +232,8 @@ def test_install_app_requirements_no_docker(
         [
             sys.executable,
             "-u",
+            "-X",
+            "utf8",
             "-m",
             "pip",
             "install",
@@ -241,6 +246,7 @@ def test_install_app_requirements_no_docker(
             "bar>=4.5",
         ],
         check=True,
+        encoding="UTF-8",
     )
 
     # The local requirements path exists, but is empty
@@ -290,6 +296,8 @@ def test_install_app_requirements_with_locals(
     create_command.tools.subprocess.check_output.assert_called_once_with(
         [
             sys.executable,
+            "-X",
+            "utf8",
             "-m",
             "build",
             "--sdist",
@@ -302,7 +310,8 @@ def test_install_app_requirements_with_locals(
             / "dummy"
             / "_requirements",
             str(tmp_path / "local" / "first"),
-        ]
+        ],
+        encoding="UTF-8",
     )
 
     # An attempt was made to copy the prebuilt packages
@@ -342,6 +351,8 @@ def test_install_app_requirements_with_locals(
             "briefcase/com.example.first-app:py3.X",
             "python3.X",
             "-u",
+            "-X",
+            "utf8",
             "-m",
             "pip",
             "install",
@@ -357,6 +368,7 @@ def test_install_app_requirements_with_locals(
             "/app/_requirements/third-3.4.5-py3-none-any.whl",
         ],
         check=True,
+        encoding="UTF-8",
     )
 
     # The local requirements path exists, and contains the compiled sdist, the
@@ -403,6 +415,8 @@ def test_install_app_requirements_with_bad_local(
     create_command.tools.subprocess.check_output.assert_called_once_with(
         [
             sys.executable,
+            "-X",
+            "utf8",
             "-m",
             "build",
             "--sdist",
@@ -415,7 +429,8 @@ def test_install_app_requirements_with_bad_local(
             / "dummy"
             / "_requirements",
             str(tmp_path / "local" / "first"),
-        ]
+        ],
+        encoding="UTF-8",
     )
 
     # pip was *not* invoked inside docker.
diff --git a/tests/platforms/web/static/test_build.py b/tests/platforms/web/static/test_build.py
index 3b92919a..34ab256e 100644
--- a/tests/platforms/web/static/test_build.py
+++ b/tests/platforms/web/static/test_build.py
@@ -38,7 +38,7 @@ def test_build_app(build_command, first_app_generated, tmp_path):
 
     # Invoking build will create wheels as a side effect.
     def mock_run(*args, **kwargs):
-        if args[0][3] == "wheel":
+        if args[0][5] == "wheel":
             create_wheel(
                 bundle_path / "www" / "static" / "wheels",
                 "first_app",
@@ -46,7 +46,7 @@ def test_build_app(build_command, first_app_generated, tmp_path):
                     ("dependency/static/style.css", "span { margin: 10px; }\n"),
                 ],
             ),
-        elif args[0][3] == "pip":
+        elif args[0][5] == "pip":
             create_wheel(
                 bundle_path / "www" / "static" / "wheels",
                 "dependency",
@@ -85,6 +85,8 @@ def test_build_app(build_command, first_app_generated, tmp_path):
             [
                 sys.executable,
                 "-u",
+                "-X",
+                "utf8",
                 "-m",
                 "wheel",
                 "pack",
@@ -93,11 +95,14 @@ def test_build_app(build_command, first_app_generated, tmp_path):
                 bundle_path / "www" / "static" / "wheels",
             ],
             check=True,
+            encoding="UTF-8",
         ),
         mock.call(
             [
                 sys.executable,
                 "-u",
+                "-X",
+                "utf8",
                 "-m",
                 "pip",
                 "wheel",
@@ -107,6 +112,7 @@ def test_build_app(build_command, first_app_generated, tmp_path):
                 bundle_path / "requirements.txt",
             ],
             check=True,
+            encoding="UTF-8",
         ),
     ]
 
@@ -249,6 +255,8 @@ def test_build_app_missing_wheel_dir(build_command, first_app_generated, tmp_pat
             [
                 sys.executable,
                 "-u",
+                "-X",
+                "utf8",
                 "-m",
                 "wheel",
                 "pack",
@@ -257,11 +265,14 @@ def test_build_app_missing_wheel_dir(build_command, first_app_generated, tmp_pat
                 bundle_path / "www" / "static" / "wheels",
             ],
             check=True,
+            encoding="UTF-8",
         ),
         mock.call(
             [
                 sys.executable,
                 "-u",
+                "-X",
+                "utf8",
                 "-m",
                 "pip",
                 "wheel",
@@ -271,6 +282,7 @@ def test_build_app_missing_wheel_dir(build_command, first_app_generated, tmp_pat
                 bundle_path / "requirements.txt",
             ],
             check=True,
+            encoding="UTF-8",
         ),
     ]
 
@@ -291,7 +303,7 @@ def test_build_app_no_requirements(build_command, first_app_generated, tmp_path)
 
     # Invoking build will create wheels as a side effect.
     def mock_run(*args, **kwargs):
-        if args[0][3] == "wheel":
+        if args[0][5] == "wheel":
             create_wheel(
                 bundle_path / "www" / "static" / "wheels",
                 "first_app",
@@ -299,7 +311,7 @@ def test_build_app_no_requirements(build_command, first_app_generated, tmp_path)
                     ("dependency/static/style.css", "span { margin: 10px; }\n"),
                 ],
             ),
-        elif args[0][3] == "pip":
+        elif args[0][5] == "pip":
             pass
         else:
             raise ValueError("Unknown command")
@@ -325,6 +337,8 @@ def test_build_app_no_requirements(build_command, first_app_generated, tmp_path)
             [
                 sys.executable,
                 "-u",
+                "-X",
+                "utf8",
                 "-m",
                 "wheel",
                 "pack",
@@ -333,11 +347,14 @@ def test_build_app_no_requirements(build_command, first_app_generated, tmp_path)
                 bundle_path / "www" / "static" / "wheels",
             ],
             check=True,
+            encoding="UTF-8",
         ),
         mock.call(
             [
                 sys.executable,
                 "-u",
+                "-X",
+                "utf8",
                 "-m",
                 "pip",
                 "wheel",
@@ -347,6 +364,7 @@ def test_build_app_no_requirements(build_command, first_app_generated, tmp_path)
                 bundle_path / "requirements.txt",
             ],
             check=True,
+            encoding="UTF-8",
         ),
     ]
 
@@ -422,6 +440,8 @@ def test_app_package_fail(build_command, first_app_generated, tmp_path):
             [
                 sys.executable,
                 "-u",
+                "-X",
+                "utf8",
                 "-m",
                 "wheel",
                 "pack",
@@ -430,6 +450,7 @@ def test_app_package_fail(build_command, first_app_generated, tmp_path):
                 bundle_path / "www" / "static" / "wheels",
             ],
             check=True,
+            encoding="UTF-8",
         ),
     ]
 
@@ -474,6 +495,8 @@ def test_dependency_fail(build_command, first_app_generated, tmp_path):
             [
                 sys.executable,
                 "-u",
+                "-X",
+                "utf8",
                 "-m",
                 "wheel",
                 "pack",
@@ -482,11 +505,14 @@ def test_dependency_fail(build_command, first_app_generated, tmp_path):
                 bundle_path / "www" / "static" / "wheels",
             ],
             check=True,
+            encoding="UTF-8",
         ),
         mock.call(
             [
                 sys.executable,
                 "-u",
+                "-X",
+                "utf8",
                 "-m",
                 "pip",
                 "wheel",
@@ -496,6 +522,7 @@ def test_dependency_fail(build_command, first_app_generated, tmp_path):
                 bundle_path / "requirements.txt",
             ],
             check=True,
+            encoding="UTF-8",
         ),
     ]
 
diff --git a/tests/platforms/windows/app/test_run.py b/tests/platforms/windows/app/test_run.py
index 689a4e53..b098861c 100644
--- a/tests/platforms/windows/app/test_run.py
+++ b/tests/platforms/windows/app/test_run.py
@@ -49,6 +49,7 @@ def test_run_app(run_command, first_app_config, tmp_path):
             ),
         ],
         cwd=tmp_path / "home",
+        encoding="UTF-8",
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
         bufsize=1,
@@ -93,6 +94,7 @@ def test_run_app_with_passthrough(run_command, first_app_config, tmp_path):
             "--bar",
         ],
         cwd=tmp_path / "home",
+        encoding="UTF-8",
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
         bufsize=1,
@@ -130,6 +132,7 @@ def test_run_app_failed(run_command, first_app_config, tmp_path):
             ),
         ],
         cwd=tmp_path / "home",
+        encoding="UTF-8",
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
         bufsize=1,
@@ -163,6 +166,7 @@ def test_run_app_test_mode(run_command, first_app_config, tmp_path):
             ),
         ],
         cwd=tmp_path / "home",
+        encoding="UTF-8",
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
         bufsize=1,
@@ -208,6 +212,7 @@ def test_run_app_test_mode_with_passthrough(run_command, first_app_config, tmp_p
             "--bar",
         ],
         cwd=tmp_path / "home",
+        encoding="UTF-8",
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
         bufsize=1,
diff --git a/tests/platforms/windows/visualstudio/test_run.py b/tests/platforms/windows/visualstudio/test_run.py
index c9898573..b9f69c16 100644
--- a/tests/platforms/windows/visualstudio/test_run.py
+++ b/tests/platforms/windows/visualstudio/test_run.py
@@ -54,6 +54,7 @@ def test_run_app(run_command, first_app_config, tmp_path):
             ),
         ],
         cwd=tmp_path / "home",
+        encoding="UTF-8",
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
         bufsize=1,
@@ -100,6 +101,7 @@ def test_run_app_with_args(run_command, first_app_config, tmp_path):
             "--bar",
         ],
         cwd=tmp_path / "home",
+        encoding="UTF-8",
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
         bufsize=1,
@@ -140,6 +142,7 @@ def test_run_app_test_mode(run_command, first_app_config, tmp_path):
             ),
         ],
         cwd=tmp_path / "home",
+        encoding="UTF-8",
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
         bufsize=1,
@@ -187,6 +190,7 @@ def test_run_app_test_mode_with_args(run_command, first_app_config, tmp_path):
             "--bar",
         ],
         cwd=tmp_path / "home",
+        encoding="UTF-8",
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
         bufsize=1,