Repository: beeware/briefcase

Issue:
AppImage loads Python code from project's sources, not from inside of AppImage
**Describe the bug**
Running built AppImage loads Python sources from source tree instead of loading them from inside of the AppImage. This appears to happen when project's Python package is at project's root, and not in `src` folder. 

You may get this abnormal folder structure if you create your project as Poetry project (with `poetry new`) and later on migrate to Briefcase.

**To Reproduce**
Steps to reproduce the behavior:
1. Create new Toga project using default answers with:
```bash
briefcase new
cd helloworld
```
2. Move `helloworld` package to root level:
```bash
mv src/helloworld .
rmdir src
sed -i 's|src/helloworld|helloworld|' pyproject.toml
```
3. Then build AppImage:
```bash
briefcase build
```
4. Run the built AppImage:
```bash
linux/Hello_World-0.0.1-x86_64.AppImage
```
5. You should see this:
![helloworld](https://user-images.githubusercontent.com/1950698/151598132-9881fc3b-10e9-4ce0-82bd-6a8bf843c341.png)
6. Next in `helloworld/app.py` change window title from:
```python
self.main_window = toga.MainWindow(title=self.formal_name)
```
To:
```python
self.main_window = toga.MainWindow(title='You cannot see me!')
```
7. Finally run the AppImage again. Do not build it, just run the previously built AppImage:
```bash
linux/Hello_World-0.0.1-x86_64.AppImage
```
8. The title has changed, even though it shouldn't have:
![changed-title](https://user-images.githubusercontent.com/1950698/151598763-88432791-d126-40a6-b1ea-5f73df2c5b66.png)

**Expected behavior**
The title should have remained as "Hello World" as I didn't rebuild the AppImage.

**Environment:**
 - Operating System: Pop!_OS 20.04
 - Python version: 3.8.10
 - Software versions:
   - Briefcase: 0.3.5
   - Toga: 0.3.0.dev30

**Additional context**
This bug appears to depend on current working directory. If you cd to parent directory then the AppImage will load its sources correctly from inside of it:
```bash
cd ..
helloworld/linux/Hello_World-0.0.1-x86_64.AppImage
```

Discussion:

Patch:
diff --git a/changes/662.bugfix.rst b/changes/662.bugfix.rst
new file mode 100644
index 00000000..279c61f6
--- /dev/null
+++ b/changes/662.bugfix.rst
@@ -0,0 +1,1 @@
+Apps now have better isolation against the current working directory. This ensures that code in the current working directory isn't inadvertently included when an app runs.
diff --git a/src/briefcase/commands/dev.py b/src/briefcase/commands/dev.py
index 688af8f7..0acf3062 100644
--- a/src/briefcase/commands/dev.py
+++ b/src/briefcase/commands/dev.py
@@ -1,6 +1,7 @@
 import os
 import subprocess
 import sys
+from pathlib import Path
 from typing import Optional
 
 from briefcase.config import BaseConfig
@@ -86,9 +87,18 @@ class DevCommand(BaseCommand):
         try:
             # Invoke the app.
             self.subprocess.run(
-                [sys.executable, "-m", app.module_name],
+                [
+                    sys.executable,
+                    "-c",
+                    (
+                        "import runpy, sys;"
+                        "sys.path.pop(0);"
+                        f'runpy.run_module("{app.module_name}", run_name="__main__", alter_sys=True)'
+                    ),
+                ],
                 env=env,
                 check=True,
+                cwd=self.home_path,
             )
         except subprocess.CalledProcessError as e:
             raise BriefcaseCommandError(
@@ -98,7 +108,11 @@ class DevCommand(BaseCommand):
     def get_environment(self, app):
         # Create a shell environment where PYTHONPATH points to the source
         # directories described by the app config.
-        return {"PYTHONPATH": os.pathsep.join(app.PYTHONPATH)}
+        return {
+            "PYTHONPATH": os.pathsep.join(
+                os.fsdecode(Path.cwd() / path) for path in app.PYTHONPATH
+            )
+        }
 
     def __call__(
         self,
diff --git a/src/briefcase/integrations/subprocess.py b/src/briefcase/integrations/subprocess.py
index 13a4bf1e..56b0891d 100644
--- a/src/briefcase/integrations/subprocess.py
+++ b/src/briefcase/integrations/subprocess.py
@@ -6,6 +6,7 @@ import subprocess
 import sys
 import threading
 import time
+from pathlib import Path
 
 import psutil
 
@@ -217,6 +218,7 @@ class Subprocess:
 
         # Otherwise, invoke run() normally.
         self._log_command(args)
+        self._log_cwd(kwargs.get("cwd"))
         self._log_environment(kwargs.get("env"))
 
         try:
@@ -291,6 +293,7 @@ class Subprocess:
            is returned as strings instead of bytes.
         """
         self._log_command(args)
+        self._log_cwd(kwargs.get("cwd"))
         self._log_environment(kwargs.get("env"))
 
         try:
@@ -355,6 +358,7 @@ class Subprocess:
            is returned as strings instead of bytes.
         """
         self._log_command(args)
+        self._log_cwd(kwargs.get("cwd"))
         self._log_environment(kwargs.get("env"))
 
         return self._subprocess.Popen(
@@ -436,6 +440,12 @@ class Subprocess:
             f"    {' '.join(shlex.quote(str(arg)) for arg in args)}"
         )
 
+    def _log_cwd(self, cwd):
+        """Log the working directory for the  command being executed."""
+        effective_cwd = Path.cwd() if cwd is None else cwd
+        self.command.logger.debug("Working Directory:")
+        self.command.logger.debug(f"    {effective_cwd}")
+
     def _log_environment(self, overrides):
         """Log the environment variables overrides prior to command execution.
 
diff --git a/src/briefcase/platforms/linux/appimage.py b/src/briefcase/platforms/linux/appimage.py
index 9eb48511..6abb0eb4 100644
--- a/src/briefcase/platforms/linux/appimage.py
+++ b/src/briefcase/platforms/linux/appimage.py
@@ -266,6 +266,7 @@ class LinuxAppImageRunCommand(LinuxAppImageMixin, RunCommand):
                     os.fsdecode(self.binary_path(app)),
                 ],
                 check=True,
+                cwd=self.home_path,
             )
         except subprocess.CalledProcessError as e:
             raise BriefcaseCommandError(f"Unable to start app {app.app_name}.") from e
diff --git a/src/briefcase/platforms/linux/flatpak.py b/src/briefcase/platforms/linux/flatpak.py
index 57812cae..4ff7ee01 100644
--- a/src/briefcase/platforms/linux/flatpak.py
+++ b/src/briefcase/platforms/linux/flatpak.py
@@ -172,7 +172,10 @@ class LinuxFlatpakRunCommand(LinuxFlatpakMixin, RunCommand):
         :param app: The config object for the app
         """
         self.logger.info("Starting app...", prefix=app.app_name)
-        self.flatpak.run(bundle=app.bundle, app_name=app.app_name)
+        self.flatpak.run(
+            bundle=app.bundle,
+            app_name=app.app_name,
+        )
 
 
 class LinuxFlatpakPackageCommand(LinuxFlatpakMixin, PackageCommand):
diff --git a/src/briefcase/platforms/macOS/__init__.py b/src/briefcase/platforms/macOS/__init__.py
index 63498288..6c9530d7 100644
--- a/src/briefcase/platforms/macOS/__init__.py
+++ b/src/briefcase/platforms/macOS/__init__.py
@@ -78,6 +78,7 @@ class macOSRunMixin:
                         "-n",  # Force a new app to be launched
                         os.fsdecode(self.binary_path(app)),
                     ],
+                    cwd=self.home_path,
                     check=True,
                 )
             except subprocess.CalledProcessError:
diff --git a/src/briefcase/platforms/windows/__init__.py b/src/briefcase/platforms/windows/__init__.py
index 5ace7e60..5a00317e 100644
--- a/src/briefcase/platforms/windows/__init__.py
+++ b/src/briefcase/platforms/windows/__init__.py
@@ -86,6 +86,7 @@ class WindowsRunCommand(RunCommand):
             self.logger.info("=" * 75)
             self.subprocess.run(
                 [os.fsdecode(self.binary_path(app))],
+                cwd=self.home_path,
                 check=True,
             )
         except subprocess.CalledProcessError as e:


Test patch:
diff --git a/tests/commands/dev/test_get_environment.py b/tests/commands/dev/test_get_environment.py
index 21c84569..17fd2952 100644
--- a/tests/commands/dev/test_get_environment.py
+++ b/tests/commands/dev/test_get_environment.py
@@ -1,4 +1,5 @@
 import sys
+from pathlib import Path
 
 import pytest
 
@@ -8,18 +9,18 @@ PYTHONPATH = "PYTHONPATH"
 def test_pythonpath_with_one_source(dev_command, first_app):
     """Test get environment with one source."""
     env = dev_command.get_environment(first_app)
-    assert env[PYTHONPATH] == "src"
+    assert env[PYTHONPATH] == f"{Path.cwd() / 'src'}"
 
 
 @pytest.mark.skipif(sys.platform != "win32", reason="Relevant only for windows")
 def test_pythonpath_with_two_sources_in_windows(dev_command, third_app):
     """Test get environment with two sources in windows."""
     env = dev_command.get_environment(third_app)
-    assert env[PYTHONPATH] == "src;other"
+    assert env[PYTHONPATH] == f"{Path.cwd() / 'src'};{Path.cwd() / 'other'}"
 
 
 @pytest.mark.skipif(sys.platform == "win32", reason="Relevant only for non-windows")
 def test_pythonpath_with_two_sources_in_linux(dev_command, third_app):
     """Test get environment with two sources in linux."""
     env = dev_command.get_environment(third_app)
-    assert env[PYTHONPATH] == "src:other"
+    assert env[PYTHONPATH] == f"{Path.cwd() / 'src'}:{Path.cwd() / 'other'}"
diff --git a/tests/commands/dev/test_run_dev_app.py b/tests/commands/dev/test_run_dev_app.py
index 69568ae6..7af79e52 100644
--- a/tests/commands/dev/test_run_dev_app.py
+++ b/tests/commands/dev/test_run_dev_app.py
@@ -6,15 +6,26 @@ import pytest
 from briefcase.exceptions import BriefcaseCommandError
 
 
-def test_subprocess_running_successfully(dev_command, first_app):
+def test_subprocess_running_successfully(dev_command, first_app, tmp_path):
     env = dict(a=1, b=2, c=3)
     dev_command.run_dev_app(first_app, env)
     dev_command.subprocess.run.assert_called_once_with(
-        [sys.executable, "-m", first_app.app_name], env=env, check=True
+        [
+            sys.executable,
+            "-c",
+            (
+                "import runpy, sys;"
+                "sys.path.pop(0);"
+                'runpy.run_module("first", run_name="__main__", alter_sys=True)'
+            ),
+        ],
+        env=env,
+        cwd=dev_command.home_path,
+        check=True,
     )
 
 
-def test_subprocess_throws_error(dev_command, first_app):
+def test_subprocess_throws_error(dev_command, first_app, tmp_path):
     env = dict(a=1, b=2, c=3)
     dev_command.subprocess.run.side_effect = CalledProcessError(returncode=2, cmd="cmd")
     with pytest.raises(
@@ -22,5 +33,16 @@ def test_subprocess_throws_error(dev_command, first_app):
     ):
         dev_command.run_dev_app(first_app, env)
     dev_command.subprocess.run.assert_called_once_with(
-        [sys.executable, "-m", first_app.app_name], env=env, check=True
+        [
+            sys.executable,
+            "-c",
+            (
+                "import runpy, sys;"
+                "sys.path.pop(0);"
+                'runpy.run_module("first", run_name="__main__", alter_sys=True)'
+            ),
+        ],
+        env=env,
+        cwd=dev_command.home_path,
+        check=True,
     )
diff --git a/tests/integrations/docker/test_Docker__check_output.py b/tests/integrations/docker/test_Docker__check_output.py
index a02c259e..bb9a042e 100644
--- a/tests/integrations/docker/test_Docker__check_output.py
+++ b/tests/integrations/docker/test_Docker__check_output.py
@@ -1,5 +1,6 @@
 import os
 import sys
+from pathlib import Path
 from unittest.mock import ANY
 
 import pytest
@@ -144,6 +145,8 @@ def test_simple_verbose_call(mock_docker, tmp_path, capsys):
         "--rm "
         "briefcase/com.example.myapp:py3.X "
         "hello world\n"
+        ">>> Working Directory:\n"
+        f">>>     {Path.cwd()}\n"
         ">>> Command Output:\n"
         ">>>     goodbye\n"
         ">>> Return code: 0\n"
diff --git a/tests/integrations/docker/test_Docker__run.py b/tests/integrations/docker/test_Docker__run.py
index 7091f893..cfc533dc 100644
--- a/tests/integrations/docker/test_Docker__run.py
+++ b/tests/integrations/docker/test_Docker__run.py
@@ -1,5 +1,6 @@
 import os
 import sys
+from pathlib import Path
 from unittest.mock import ANY
 
 import pytest
@@ -141,5 +142,7 @@ def test_simple_verbose_call(mock_docker, tmp_path, capsys):
         "--rm "
         "briefcase/com.example.myapp:py3.X "
         "hello world\n"
+        ">>> Working Directory:\n"
+        f">>>     {Path.cwd()}\n"
         ">>> Return code: 3\n"
     )
diff --git a/tests/integrations/subprocess/test_Subprocess__Popen.py b/tests/integrations/subprocess/test_Subprocess__Popen.py
index d32db10c..8fbf70f5 100644
--- a/tests/integrations/subprocess/test_Subprocess__Popen.py
+++ b/tests/integrations/subprocess/test_Subprocess__Popen.py
@@ -1,4 +1,5 @@
 import os
+from pathlib import Path
 from unittest.mock import ANY
 
 import pytest
@@ -133,16 +134,20 @@ def test_debug_call(mock_sub, capsys):
         encoding=ANY,
     )
     assert capsys.readouterr().out == (
-        "\n" ">>> Running Command:\n" ">>>     hello world\n"
+        "\n"
+        ">>> Running Command:\n"
+        ">>>     hello world\n"
+        ">>> Working Directory:\n"
+        f">>>     {Path.cwd()}\n"
     )
 
 
-def test_debug_call_with_env(mock_sub, capsys):
+def test_debug_call_with_env(mock_sub, capsys, tmp_path):
     """If verbosity is turned up, and injected env vars are included output."""
     mock_sub.command.logger = Log(verbosity=2)
 
     env = {"NewVar": "NewVarValue"}
-    mock_sub.Popen(["hello", "world"], env=env)
+    mock_sub.Popen(["hello", "world"], env=env, cwd=tmp_path / "cwd")
 
     merged_env = mock_sub.command.os.environ.copy()
     merged_env.update(env)
@@ -150,6 +155,7 @@ def test_debug_call_with_env(mock_sub, capsys):
     mock_sub._subprocess.Popen.assert_called_with(
         ["hello", "world"],
         env=merged_env,
+        cwd=os.fsdecode(tmp_path / "cwd"),
         text=True,
         encoding=ANY,
     )
@@ -158,6 +164,8 @@ def test_debug_call_with_env(mock_sub, capsys):
         "\n"
         ">>> Running Command:\n"
         ">>>     hello world\n"
+        ">>> Working Directory:\n"
+        f">>>     {tmp_path / 'cwd'}\n"
         ">>> Environment Overrides:\n"
         ">>>     NewVar=NewVarValue\n"
     )
diff --git a/tests/integrations/subprocess/test_Subprocess__check_output.py b/tests/integrations/subprocess/test_Subprocess__check_output.py
index 933730f9..0b965e00 100644
--- a/tests/integrations/subprocess/test_Subprocess__check_output.py
+++ b/tests/integrations/subprocess/test_Subprocess__check_output.py
@@ -1,4 +1,5 @@
 import os
+from pathlib import Path
 from subprocess import CalledProcessError
 from unittest.mock import ANY
 
@@ -138,6 +139,8 @@ def test_debug_call(mock_sub, capsys):
         "\n"
         ">>> Running Command:\n"
         ">>>     hello world\n"
+        ">>> Working Directory:\n"
+        f">>>     {Path.cwd()}\n"
         ">>> Command Output:\n"
         ">>>     some output line 1\n"
         ">>>     more output line 2\n"
@@ -147,12 +150,12 @@ def test_debug_call(mock_sub, capsys):
     assert capsys.readouterr().out == expected_output
 
 
-def test_debug_call_with_env(mock_sub, capsys):
+def test_debug_call_with_env(mock_sub, capsys, tmp_path):
     """If verbosity is turned up, injected env vars are included in output."""
     mock_sub.command.logger = Log(verbosity=2)
 
     env = {"NewVar": "NewVarValue"}
-    mock_sub.check_output(["hello", "world"], env=env)
+    mock_sub.check_output(["hello", "world"], env=env, cwd=tmp_path / "cwd")
 
     merged_env = mock_sub.command.os.environ.copy()
     merged_env.update(env)
@@ -160,6 +163,7 @@ def test_debug_call_with_env(mock_sub, capsys):
     mock_sub._subprocess.check_output.assert_called_with(
         ["hello", "world"],
         env=merged_env,
+        cwd=os.fsdecode(tmp_path / "cwd"),
         text=True,
         encoding=ANY,
     )
@@ -168,6 +172,8 @@ def test_debug_call_with_env(mock_sub, capsys):
         "\n"
         ">>> Running Command:\n"
         ">>>     hello world\n"
+        ">>> Working Directory:\n"
+        f">>>     {tmp_path / 'cwd'}\n"
         ">>> Environment Overrides:\n"
         ">>>     NewVar=NewVarValue\n"
         ">>> Command Output:\n"
@@ -198,6 +204,8 @@ def test_calledprocesserror_exception_logging(mock_sub, capsys):
         "\n"
         ">>> Running Command:\n"
         ">>>     hello world\n"
+        ">>> Working Directory:\n"
+        f">>>     {Path.cwd()}\n"
         ">>> Command Output:\n"
         ">>>     output line 1\n"
         ">>>     output line 2\n"
diff --git a/tests/integrations/subprocess/test_Subprocess__run.py b/tests/integrations/subprocess/test_Subprocess__run.py
index 6540f8ec..8b7219f1 100644
--- a/tests/integrations/subprocess/test_Subprocess__run.py
+++ b/tests/integrations/subprocess/test_Subprocess__run.py
@@ -1,4 +1,5 @@
 import os
+from pathlib import Path
 from subprocess import CalledProcessError
 from unittest.mock import ANY
 
@@ -138,6 +139,8 @@ def test_debug_call(mock_sub, capsys):
         "\n"
         ">>> Running Command:\n"
         ">>>     hello world\n"
+        ">>> Working Directory:\n"
+        f">>>     {Path.cwd()}\n"
         ">>> Return code: 0\n"
     )
     # fmt: on
@@ -145,12 +148,12 @@ def test_debug_call(mock_sub, capsys):
     assert capsys.readouterr().out == expected_output
 
 
-def test_debug_call_with_env(mock_sub, capsys):
+def test_debug_call_with_env(mock_sub, capsys, tmp_path):
     """If verbosity is turned up, injected env vars are included output."""
     mock_sub.command.logger = Log(verbosity=2)
 
     env = {"NewVar": "NewVarValue"}
-    mock_sub.run(["hello", "world"], env=env)
+    mock_sub.run(["hello", "world"], env=env, cwd=tmp_path / "cwd")
 
     merged_env = mock_sub.command.os.environ.copy()
     merged_env.update(env)
@@ -158,6 +161,7 @@ def test_debug_call_with_env(mock_sub, capsys):
     mock_sub._subprocess.run.assert_called_with(
         ["hello", "world"],
         env=merged_env,
+        cwd=os.fsdecode(tmp_path / "cwd"),
         text=True,
         encoding=ANY,
     )
@@ -165,6 +169,8 @@ def test_debug_call_with_env(mock_sub, capsys):
         "\n"
         ">>> Running Command:\n"
         ">>>     hello world\n"
+        ">>> Working Directory:\n"
+        f">>>     {tmp_path / 'cwd'}\n"
         ">>> Environment Overrides:\n"
         ">>>     NewVar=NewVarValue\n"
         ">>> Return code: 0\n"
@@ -191,6 +197,8 @@ def test_calledprocesserror_exception_logging(mock_sub, capsys):
         "\n"
         ">>> Running Command:\n"
         ">>>     hello world\n"
+        ">>> Working Directory:\n"
+        f">>>     {Path.cwd()}\n"
         ">>> Return code: -1\n"
     )
     # fmt: on
diff --git a/tests/integrations/subprocess/test_Subprocess__run_in_wait_bar.py b/tests/integrations/subprocess/test_Subprocess__run_in_wait_bar.py
index 4f5655e4..3d905fa7 100644
--- a/tests/integrations/subprocess/test_Subprocess__run_in_wait_bar.py
+++ b/tests/integrations/subprocess/test_Subprocess__run_in_wait_bar.py
@@ -1,4 +1,6 @@
+import os
 import subprocess
+from pathlib import Path
 from unittest.mock import ANY
 
 import pytest
@@ -65,6 +67,8 @@ def test_debug_call(mock_sub, capsys):
         "\n"
         ">>> Running Command:\n"
         ">>>     hello world\n"
+        ">>> Working Directory:\n"
+        f">>>     {Path.cwd()}\n"
         "output line 1\n"
         "\n"
         "output line 3\n"
@@ -74,14 +78,14 @@ def test_debug_call(mock_sub, capsys):
     assert capsys.readouterr().out == expected_output
 
 
-def test_debug_call_with_env(mock_sub, capsys):
+def test_debug_call_with_env(mock_sub, capsys, tmp_path):
     """If verbosity is turned up, injected env vars are included in debug
     output."""
     mock_sub.command.logger = Log(verbosity=2)
 
     env = {"NewVar": "NewVarValue"}
     with mock_sub.command.input.wait_bar():
-        mock_sub.run(["hello", "world"], env=env)
+        mock_sub.run(["hello", "world"], env=env, cwd=tmp_path / "cwd")
 
     merged_env = mock_sub.command.os.environ.copy()
     merged_env.update(env)
@@ -89,6 +93,7 @@ def test_debug_call_with_env(mock_sub, capsys):
     mock_sub._subprocess.Popen.assert_called_with(
         ["hello", "world"],
         env=merged_env,
+        cwd=os.fsdecode(tmp_path / "cwd"),
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
         text=True,
@@ -98,6 +103,8 @@ def test_debug_call_with_env(mock_sub, capsys):
         "\n"
         ">>> Running Command:\n"
         ">>>     hello world\n"
+        ">>> Working Directory:\n"
+        f">>>     {tmp_path / 'cwd'}\n"
         ">>> Environment Overrides:\n"
         ">>>     NewVar=NewVarValue\n"
         "output line 1\n"
diff --git a/tests/platforms/linux/appimage/test_run.py b/tests/platforms/linux/appimage/test_run.py
index afc834aa..2b1b3c91 100644
--- a/tests/platforms/linux/appimage/test_run.py
+++ b/tests/platforms/linux/appimage/test_run.py
@@ -9,7 +9,10 @@ from briefcase.platforms.linux.appimage import LinuxAppImageRunCommand
 
 def test_verify_linux(tmp_path):
     """A linux App can be started on linux."""
-    command = LinuxAppImageRunCommand(base_path=tmp_path)
+    command = LinuxAppImageRunCommand(
+        base_path=tmp_path / "base",
+        home_path=tmp_path / "home",
+    )
     command.use_docker = True
     command.host_os = "Linux"
 
@@ -24,7 +27,10 @@ def test_verify_linux(tmp_path):
 
 def test_verify_non_linux(tmp_path):
     """A linux App can be started on linux, even if Docker is enabled."""
-    command = LinuxAppImageRunCommand(base_path=tmp_path)
+    command = LinuxAppImageRunCommand(
+        base_path=tmp_path / "base",
+        home_path=tmp_path / "home",
+    )
     command.use_docker = True
     command.host_os = "WierdOS"
 
@@ -40,7 +46,10 @@ def test_verify_non_linux(tmp_path):
 
 def test_run_app(first_app_config, tmp_path):
     """A linux App can be started."""
-    command = LinuxAppImageRunCommand(base_path=tmp_path)
+    command = LinuxAppImageRunCommand(
+        base_path=tmp_path / "base",
+        home_path=tmp_path / "home",
+    )
 
     # Set the host architecture for test purposes.
     command.host_arch = "wonky"
@@ -50,13 +59,18 @@ def test_run_app(first_app_config, tmp_path):
     command.run_app(first_app_config)
 
     command.subprocess.run.assert_called_with(
-        [os.fsdecode(tmp_path / "linux" / "First_App-0.0.1-wonky.AppImage")], check=True
+        [os.fsdecode(tmp_path / "base" / "linux" / "First_App-0.0.1-wonky.AppImage")],
+        cwd=tmp_path / "home",
+        check=True,
     )
 
 
 def test_run_app_failed(first_app_config, tmp_path):
     """If there's a problem started the app, an exception is raised."""
-    command = LinuxAppImageRunCommand(base_path=tmp_path)
+    command = LinuxAppImageRunCommand(
+        base_path=tmp_path / "base",
+        home_path=tmp_path / "home",
+    )
 
     # Set the host architecture for test purposes.
     command.host_arch = "wonky"
@@ -69,5 +83,7 @@ def test_run_app_failed(first_app_config, tmp_path):
 
     # The run command was still invoked, though
     command.subprocess.run.assert_called_with(
-        [os.fsdecode(tmp_path / "linux" / "First_App-0.0.1-wonky.AppImage")], check=True
+        [os.fsdecode(tmp_path / "base" / "linux" / "First_App-0.0.1-wonky.AppImage")],
+        cwd=tmp_path / "home",
+        check=True,
     )
diff --git a/tests/platforms/macOS/app/test_run.py b/tests/platforms/macOS/app/test_run.py
index 2cbdb6fa..c3c7dff2 100644
--- a/tests/platforms/macOS/app/test_run.py
+++ b/tests/platforms/macOS/app/test_run.py
@@ -10,7 +10,10 @@ from briefcase.platforms.macOS.app import macOSAppRunCommand
 
 def test_run_app(first_app_config, tmp_path, monkeypatch):
     """A macOS app can be started."""
-    command = macOSAppRunCommand(base_path=tmp_path)
+    command = macOSAppRunCommand(
+        base_path=tmp_path / "base",
+        home_path=tmp_path / "home",
+    )
     command.subprocess = mock.MagicMock()
     log_stream_process = mock.MagicMock()
     command.subprocess.Popen.return_value = log_stream_process
@@ -42,7 +45,9 @@ def test_run_app(first_app_config, tmp_path, monkeypatch):
         bufsize=1,
     )
     command.subprocess.run.assert_called_with(
-        ["open", "-n", os.fsdecode(bin_path)], check=True
+        ["open", "-n", os.fsdecode(bin_path)],
+        cwd=tmp_path / "home",
+        check=True,
     )
     command.subprocess.stream_output.assert_called_with(
         "log stream", log_stream_process, stop_func=mock.ANY
@@ -52,7 +57,10 @@ def test_run_app(first_app_config, tmp_path, monkeypatch):
 
 def test_run_app_failed(first_app_config, tmp_path):
     """If there's a problem started the app, an exception is raised."""
-    command = macOSAppRunCommand(base_path=tmp_path)
+    command = macOSAppRunCommand(
+        base_path=tmp_path / "base",
+        home_path=tmp_path / "home",
+    )
     command.subprocess = mock.MagicMock()
     log_stream_process = mock.MagicMock()
     command.subprocess.Popen.return_value = log_stream_process
@@ -83,7 +91,9 @@ def test_run_app_failed(first_app_config, tmp_path):
         bufsize=1,
     )
     command.subprocess.run.assert_called_with(
-        ["open", "-n", os.fsdecode(bin_path)], check=True
+        ["open", "-n", os.fsdecode(bin_path)],
+        cwd=tmp_path / "home",
+        check=True,
     )
 
     # No attempt was made to stream the log; but there was a cleanup
@@ -93,7 +103,10 @@ def test_run_app_failed(first_app_config, tmp_path):
 
 def test_run_app_find_pid_failed(first_app_config, tmp_path, monkeypatch, capsys):
     """If after app is started, its pid is not found, do not stream output."""
-    command = macOSAppRunCommand(base_path=tmp_path)
+    command = macOSAppRunCommand(
+        base_path=tmp_path / "base",
+        home_path=tmp_path / "home",
+    )
     command.subprocess = mock.MagicMock()
     log_stream_process = mock.MagicMock()
     command.subprocess.Popen.return_value = log_stream_process
@@ -122,7 +135,9 @@ def test_run_app_find_pid_failed(first_app_config, tmp_path, monkeypatch, capsys
         bufsize=1,
     )
     command.subprocess.run.assert_called_with(
-        ["open", "-n", os.fsdecode(bin_path)], check=True
+        ["open", "-n", os.fsdecode(bin_path)],
+        cwd=tmp_path / "home",
+        check=True,
     )
     assert capsys.readouterr().out == (
         "\n"
diff --git a/tests/platforms/macOS/xcode/test_run.py b/tests/platforms/macOS/xcode/test_run.py
index 41260191..c7b0c89a 100644
--- a/tests/platforms/macOS/xcode/test_run.py
+++ b/tests/platforms/macOS/xcode/test_run.py
@@ -10,9 +10,11 @@ from briefcase.platforms.macOS.xcode import macOSXcodeRunCommand
 
 def test_run_app(first_app_config, tmp_path):
     """A macOS Xcode app can be started."""
-    command = macOSXcodeRunCommand(base_path=tmp_path)
+    command = macOSXcodeRunCommand(
+        base_path=tmp_path / "base",
+        home_path=tmp_path / "home",
+    )
     command.subprocess = mock.MagicMock()
-
     command.run_app(first_app_config)
 
     # Calls were made to start the app and to start a log stream.
@@ -34,5 +36,7 @@ def test_run_app(first_app_config, tmp_path):
         bufsize=1,
     )
     command.subprocess.run.assert_called_with(
-        ["open", "-n", os.fsdecode(bin_path)], check=True
+        ["open", "-n", os.fsdecode(bin_path)],
+        cwd=tmp_path / "home",
+        check=True,
     )
diff --git a/tests/platforms/windows/app/test_run.py b/tests/platforms/windows/app/test_run.py
index c51c9bca..36d6afd8 100644
--- a/tests/platforms/windows/app/test_run.py
+++ b/tests/platforms/windows/app/test_run.py
@@ -9,7 +9,10 @@ from briefcase.platforms.windows.app import WindowsAppRunCommand
 
 def test_run_app(first_app_config, tmp_path):
     """A windows app can be started."""
-    command = WindowsAppRunCommand(base_path=tmp_path)
+    command = WindowsAppRunCommand(
+        base_path=tmp_path / "base",
+        home_path=tmp_path / "home",
+    )
     command.subprocess = mock.MagicMock()
 
     command.run_app(first_app_config)
@@ -17,16 +20,26 @@ def test_run_app(first_app_config, tmp_path):
     command.subprocess.run.assert_called_with(
         [
             os.fsdecode(
-                tmp_path / "windows" / "app" / "First App" / "src" / "First App.exe"
+                tmp_path
+                / "base"
+                / "windows"
+                / "app"
+                / "First App"
+                / "src"
+                / "First App.exe"
             ),
         ],
+        cwd=tmp_path / "home",
         check=True,
     )
 
 
 def test_run_app_failed(first_app_config, tmp_path):
     """If there's a problem started the app, an exception is raised."""
-    command = WindowsAppRunCommand(base_path=tmp_path)
+    command = WindowsAppRunCommand(
+        base_path=tmp_path / "base",
+        home_path=tmp_path / "home",
+    )
     command.subprocess = mock.MagicMock()
     command.subprocess.run.side_effect = BriefcaseCommandError("problem")
 
@@ -37,8 +50,15 @@ def test_run_app_failed(first_app_config, tmp_path):
     command.subprocess.run.assert_called_with(
         [
             os.fsdecode(
-                tmp_path / "windows" / "app" / "First App" / "src" / "First App.exe"
+                tmp_path
+                / "base"
+                / "windows"
+                / "app"
+                / "First App"
+                / "src"
+                / "First App.exe"
             ),
         ],
+        cwd=tmp_path / "home",
         check=True,
     )
diff --git a/tests/platforms/windows/visualstudio/test_run.py b/tests/platforms/windows/visualstudio/test_run.py
index 58972996..ce516b9e 100644
--- a/tests/platforms/windows/visualstudio/test_run.py
+++ b/tests/platforms/windows/visualstudio/test_run.py
@@ -9,7 +9,10 @@ from briefcase.platforms.windows.visualstudio import WindowsVisualStudioRunComma
 
 def test_run_app(first_app_config, tmp_path):
     """A windows Visual Studio project app can be started."""
-    command = WindowsVisualStudioRunCommand(base_path=tmp_path)
+    command = WindowsVisualStudioRunCommand(
+        base_path=tmp_path / "base",
+        home_path=tmp_path / "home",
+    )
     command.subprocess = mock.MagicMock()
 
     command.run_app(first_app_config)
@@ -18,6 +21,7 @@ def test_run_app(first_app_config, tmp_path):
         [
             os.fsdecode(
                 tmp_path
+                / "base"
                 / "windows"
                 / "VisualStudio"
                 / "First App"
@@ -26,5 +30,6 @@ def test_run_app(first_app_config, tmp_path):
                 / "First App.exe"
             ),
         ],
+        cwd=tmp_path / "home",
         check=True,
     )