Repository: beeware/briefcase
Pull request #1661 (2024-02-21 17:38:10)

Add support to pass arguments through to Docker image build
## Changes
- Allow passing arguments to `docker build`
- RE: https://github.com/beeware/.github/pull/99

## PR Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
- [x] All new features have been tested
- [x] All new features have been documented
- [X] I have read the **CONTRIBUTING.md** file
- [X] I will abide by the code of conduct


Patch:
diff --git a/changes/1661.feature.rst b/changes/1661.feature.rst
new file mode 100644
index 00000000..9175339f
--- /dev/null
+++ b/changes/1661.feature.rst
@@ -0,0 +1,1 @@
+Additional options can now be passed to the ``docker build`` command for building native Linux packages and AppImages via the ``--Xdocker-build`` argument.
diff --git a/docs/reference/platforms/linux/appimage.rst b/docs/reference/platforms/linux/appimage.rst
index 22130bb2..0914b461 100644
--- a/docs/reference/platforms/linux/appimage.rst
+++ b/docs/reference/platforms/linux/appimage.rst
@@ -97,6 +97,8 @@ AppImages.
 
 Use native execution, rather than using Docker to start a container.
 
+.. include:: docker_build_options.rst
+
 Application configuration
 =========================
 
diff --git a/docs/reference/platforms/linux/docker_build_options.rst b/docs/reference/platforms/linux/docker_build_options.rst
new file mode 100644
index 00000000..c2035876
--- /dev/null
+++ b/docs/reference/platforms/linux/docker_build_options.rst
@@ -0,0 +1,12 @@
+``--Xdocker-build=<value>``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A configuration argument to be passed to the Docker build command for the app
+image. For example, to provide an additional build argument to the Dockerfile,
+specify ``--Xdocker-build=--build-arg=ARG=value``. See `the Docker build
+documentation
+<https://docs.docker.com/engine/reference/commandline/image_build/#options>`__
+for details on the full list of options that can be provided.
+
+You may specify multiple ``--Xdocker-build`` arguments; each one specifies a
+single argument to pass to Docker, in the order they are specified.
diff --git a/docs/reference/platforms/linux/system.rst b/docs/reference/platforms/linux/system.rst
index 12570f88..25e724d6 100644
--- a/docs/reference/platforms/linux/system.rst
+++ b/docs/reference/platforms/linux/system.rst
@@ -111,15 +111,17 @@ supported by the vendor, and system Python is Python 3.8 or later.
 
 The following Linux vendors are known to work as Docker targets:
 
-* Debian (e.g., ``debian:bullseye`` or ``debian:11``)
+* Debian (e.g., ``debian:bookworm`` or ``debian:12``)
 * Ubuntu (e.g., ``ubuntu:jammy`` or ``ubuntu:22.04``)
-* Fedora (e.g, ``fedora:37``)
+* Fedora (e.g, ``fedora:39``)
 * AlmaLinux (e.g., ``almalinux:9``)
 * Red Hat Enterprise Linux (e.g., ``redhat/ubi9:9``)
 * openSUSE Tumbleweed (e.g., ``"opensuse/tumbleweed:latest"``)
 * Arch Linux (e.g., ``archlinux:latest``)
 * Manjaro Linux (e.g., ``manjarolinux/base:latest``)
 
+.. include:: docker_build_options.rst
+
 Application configuration
 =========================
 
diff --git a/pyproject.toml b/pyproject.toml
index 0d87bcc0..19155686 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -62,9 +62,9 @@ dependencies = [
     # Dependencies required at runtime are set as ranges to ensure maximum
     # compatibility with the end-user's development environment.
     #
-    # Any dependency that is part of the "core python toolchain" (e.g. pip,
-    # wheel) specify a minimum version, but no maximum, because we always want
-    # the most recent version.
+    # Any dependency that is part of the "core python toolchain" (e.g. pip, wheel)
+    # specify a minimum version, but no maximum, because we always want the most
+    # recent version.
     #
     # limited to <=3.9 for the `group` argument for `entry_points()`
     "importlib_metadata >= 4.4; python_version <= '3.9'",
@@ -73,13 +73,15 @@ dependencies = [
     "setuptools >= 60",
     "wheel >= 0.37",
     "build >= 0.10",
-    # For the remaining packages: We set the lower version limit to the lowest
-    # possible version that satisfies our API needs. If the package uses semver,
-    # we set a limit to prevent the next major version (which will potentially
-    # cause API breakages). If the package uses calver, we don't pin the upper
-    # version, as the upper version provides no basis for determining API
-    # stability.
-    "cookiecutter >= 2.3.1, < 3.0",
+    #
+    # For the remaining packages: We set the lower version limit to the lowest possible
+    # version that satisfies our API needs. If the package uses semver, we set a limit
+    # to prevent the next major version (which will potentially cause API breakages).
+    # If the package uses calver, we don't pin the upper version, as the upper version
+    # provides no basis for determining API stability.
+    #
+    # See beeware/briefcase#1663 to bump <2.6.0 pin
+    "cookiecutter >= 2.3.1, < 2.6.0",
     "dmgbuild >= 1.6, < 2.0; sys_platform == 'darwin'",
     "GitPython >= 3.1, < 4.0",
     "platformdirs >= 2.6, < 5.0",
diff --git a/src/briefcase/integrations/docker.py b/src/briefcase/integrations/docker.py
index d8f1f4f4..e7660aaf 100644
--- a/src/briefcase/integrations/docker.py
+++ b/src/briefcase/integrations/docker.py
@@ -839,6 +839,7 @@ class DockerAppContext(Tool):
         host_bundle_path: Path,
         host_data_path: Path,
         python_version: str,
+        extra_build_args: list[str] | None = None,
         **kwargs,
     ) -> DockerAppContext:  # pragma: no-cover-if-is-windows
         """Verify that docker is available as an app-bound tool.
@@ -870,6 +871,7 @@ class DockerAppContext(Tool):
             host_bundle_path=host_bundle_path,
             host_data_path=host_data_path,
             python_version=python_version,
+            extra_build_args=extra_build_args,
         )
         return tools[app].app_context
 
@@ -881,6 +883,7 @@ class DockerAppContext(Tool):
         host_bundle_path: Path,
         host_data_path: Path,
         python_version: str,
+        extra_build_args: list[str] | None = None,
     ):
         """Create/update the Docker image from the app's Dockerfile."""
         self.app_base_path = app_base_path
@@ -904,7 +907,9 @@ class DockerAppContext(Tool):
                     self.tools.subprocess.run(
                         [
                             "docker",
+                            "buildx",
                             "build",
+                            "--load",
                             "--progress",
                             "plain",
                             "--tag",
@@ -921,7 +926,8 @@ class DockerAppContext(Tool):
                                 self.app_base_path,
                                 *self.app.sources[0].split("/")[:-1],
                             ),
-                        ],
+                        ]
+                        + (extra_build_args if extra_build_args is not None else []),
                         check=True,
                     )
                 except subprocess.CalledProcessError as e:
diff --git a/src/briefcase/platforms/linux/appimage.py b/src/briefcase/platforms/linux/appimage.py
index 463b01a4..b8118149 100644
--- a/src/briefcase/platforms/linux/appimage.py
+++ b/src/briefcase/platforms/linux/appimage.py
@@ -67,17 +67,26 @@ class LinuxAppImagePassiveMixin(LinuxMixin):
             help="Don't use Docker for building the AppImage",
             required=False,
         )
+        parser.add_argument(
+            "--Xdocker-build",
+            action="append",
+            dest="extra_docker_build_args",
+            help="Additional arguments to use when building the Docker image",
+            required=False,
+        )
 
     def parse_options(self, extra):
         """Extract the use_docker option."""
         options, overrides = super().parse_options(extra)
         self.use_docker = options.pop("use_docker")
+        self.extra_docker_build_args = options.pop("extra_docker_build_args")
         return options, overrides
 
     def clone_options(self, command):
         """Clone the use_docker option."""
         super().clone_options(command)
         self.use_docker = command.use_docker
+        self.extra_docker_build_args = command.extra_docker_build_args
 
     def finalize_app_config(self, app: AppConfig):
         """If we're *not* using Docker, warn the user about portability."""
@@ -149,6 +158,7 @@ class LinuxAppImageMostlyPassiveMixin(LinuxAppImagePassiveMixin):
                 host_bundle_path=self.bundle_path(app),
                 host_data_path=self.data_path,
                 python_version=self.python_version_tag,
+                extra_build_args=self.extra_docker_build_args,
             )
         else:
             NativeAppContext.verify(tools=self.tools, app=app)
diff --git a/src/briefcase/platforms/linux/system.py b/src/briefcase/platforms/linux/system.py
index 97ed057e..b529a3bc 100644
--- a/src/briefcase/platforms/linux/system.py
+++ b/src/briefcase/platforms/linux/system.py
@@ -56,11 +56,19 @@ class LinuxSystemPassiveMixin(LinuxMixin):
             help="Docker base image tag for the distribution to target for the build (e.g., `ubuntu:jammy`)",
             required=False,
         )
+        parser.add_argument(
+            "--Xdocker-build",
+            action="append",
+            dest="extra_docker_build_args",
+            help="Additional arguments to use when building the Docker image",
+            required=False,
+        )
 
     def parse_options(self, extra):
         """Extract the target_image option."""
         options, overrides = super().parse_options(extra)
         self.target_image = options.pop("target")
+        self.extra_docker_build_args = options.pop("extra_docker_build_args")
 
         return options, overrides
 
@@ -377,6 +385,7 @@ class LinuxSystemMostlyPassiveMixin(LinuxSystemPassiveMixin):
         """Clone the target_image option."""
         super().clone_options(command)
         self.target_image = command.target_image
+        self.extra_docker_build_args = command.extra_docker_build_args
 
     def verify_python(self, app: AppConfig):
         """Verify that the version of Python being used to build the app in Docker is
@@ -577,6 +586,7 @@ to install the missing dependencies, and re-run Briefcase.
                 host_bundle_path=self.bundle_path(app),
                 host_data_path=self.data_path,
                 python_version=app.python_version_tag,
+                extra_build_args=self.extra_docker_build_args,
             )
 
             # Check the system Python on the target system to see if it is


Test patch:
diff --git a/tests/integrations/docker/test_DockerAppContext__prepare.py b/tests/integrations/docker/test_DockerAppContext__prepare.py
index baa3d0f8..3509f4f6 100644
--- a/tests/integrations/docker/test_DockerAppContext__prepare.py
+++ b/tests/integrations/docker/test_DockerAppContext__prepare.py
@@ -7,7 +7,8 @@ from briefcase.exceptions import BriefcaseCommandError
 from briefcase.integrations.docker import DockerAppContext
 
 
-def test_prepare(mock_tools, my_app, tmp_path, sub_stream_kw):
+@pytest.mark.parametrize("extra_args", [[], ["--option-one", "--option-two"]])
+def test_prepare(mock_tools, my_app, extra_args, tmp_path, sub_stream_kw):
     """The Docker environment can be prepared."""
     mock_docker_app_context = DockerAppContext(mock_tools, my_app)
     mock_docker_app_context.prepare(
@@ -17,12 +18,15 @@ def test_prepare(mock_tools, my_app, tmp_path, sub_stream_kw):
         host_bundle_path=tmp_path / "bundle",
         host_data_path=tmp_path / "briefcase",
         python_version="3.X",
+        extra_build_args=extra_args,
     )
 
     mock_docker_app_context.tools.subprocess._subprocess.Popen.assert_called_with(
         [
             "docker",
+            "buildx",
             "build",
+            "--load",
             "--progress",
             "plain",
             "--tag",
@@ -36,7 +40,8 @@ def test_prepare(mock_tools, my_app, tmp_path, sub_stream_kw):
             "--build-arg",
             "HOST_GID=42",
             os.fsdecode(tmp_path / "base/path/to/src"),
-        ],
+        ]
+        + extra_args,
         **sub_stream_kw,
     )
 
@@ -62,12 +67,15 @@ def test_prepare_failure(mock_docker_app_context, tmp_path, sub_stream_kw):
             host_bundle_path=tmp_path / "bundle",
             host_data_path=tmp_path / "briefcase",
             python_version="3.X",
+            extra_build_args=["--option-one", "--option-two"],
         )
 
     mock_docker_app_context.tools.subprocess._subprocess.Popen.assert_called_with(
         [
             "docker",
+            "buildx",
             "build",
+            "--load",
             "--progress",
             "plain",
             "--tag",
@@ -81,6 +89,8 @@ def test_prepare_failure(mock_docker_app_context, tmp_path, sub_stream_kw):
             "--build-arg",
             "HOST_GID=42",
             os.fsdecode(tmp_path / "base/path/to/src"),
+            "--option-one",
+            "--option-two",
         ],
         **sub_stream_kw,
     )
diff --git a/tests/integrations/docker/test_DockerAppContext__verify.py b/tests/integrations/docker/test_DockerAppContext__verify.py
index 5ad96741..a7cdd2c0 100644
--- a/tests/integrations/docker/test_DockerAppContext__verify.py
+++ b/tests/integrations/docker/test_DockerAppContext__verify.py
@@ -18,6 +18,7 @@ def verify_kwargs():
         host_bundle_path=Path("/host/bundle"),
         host_data_path=Path("/host/data"),
         python_version="py3.X",
+        extra_build_args=["--option-one", "--option-two"],
     )
 
 
@@ -57,7 +58,9 @@ def test_success(mock_tools, first_app_config, verify_kwargs, sub_stream_kw):
     mock_tools.subprocess._subprocess.Popen.assert_called_with(
         [
             "docker",
+            "buildx",
             "build",
+            "--load",
             "--progress",
             "plain",
             "--tag",
@@ -71,6 +74,8 @@ def test_success(mock_tools, first_app_config, verify_kwargs, sub_stream_kw):
             "--build-arg",
             "HOST_GID=42",
             "/app/base/src",
+            "--option-one",
+            "--option-two",
         ],
         **sub_stream_kw,
     )
diff --git a/tests/platforms/linux/appimage/test_build.py b/tests/platforms/linux/appimage/test_build.py
index 3143b99e..d00c776c 100644
--- a/tests/platforms/linux/appimage/test_build.py
+++ b/tests/platforms/linux/appimage/test_build.py
@@ -24,15 +24,7 @@ from ....utils import create_file
 def first_app(first_app_config, tmp_path):
     """A fixture for the first app, rolled out on disk."""
     # Make it look like the template has been generated
-    app_dir = (
-        tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "linux"
-        / "appimage"
-        / "First App.AppDir"
-    )
+    app_dir = tmp_path / "base_path/build/first-app/linux/appimage/First App.AppDir"
     (app_dir / "usr/app/support").mkdir(parents=True, exist_ok=True)
     (app_dir / "usr/app_packages/firstlib").mkdir(parents=True, exist_ok=True)
     (app_dir / "usr/app_packages/secondlib").mkdir(parents=True, exist_ok=True)
@@ -58,6 +50,7 @@ def build_command(tmp_path, first_app_config):
     command.tools.host_os = "Linux"
     command.tools.host_arch = "x86_64"
     command.use_docker = False
+    command.extra_docker_build_args = []
     command._briefcase_toml[first_app_config] = {
         "paths": {
             "app_path": "First App.AppDir/usr/app",
@@ -161,15 +154,7 @@ def test_build_appimage(build_command, first_app, debug_mode, tmp_path, sub_stre
     build_command.build_app(first_app)
 
     # linuxdeploy was invoked
-    app_dir = (
-        tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "linux"
-        / "appimage"
-        / "First App.AppDir"
-    )
+    app_dir = tmp_path / "base_path/build/first-app/linux/appimage/First App.AppDir"
     expected_env = {
         "PATH": "/usr/local/bin:/usr/bin:/path/to/somewhere",
         "LINUXDEPLOY_OUTPUT_VERSION": "0.0.1",
@@ -203,12 +188,7 @@ def test_build_appimage(build_command, first_app, debug_mode, tmp_path, sub_stre
     # Binary is marked executable
     build_command.tools.os.chmod.assert_called_with(
         tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "linux"
-        / "appimage"
-        / "First_App-0.0.1-x86_64.AppImage",
+        / "base_path/build/first-app/linux/appimage/First_App-0.0.1-x86_64.AppImage",
         0o755,
     )
 
@@ -221,12 +201,7 @@ def test_build_appimage_with_plugin(build_command, first_app, tmp_path, sub_stre
     """A Linux app can be packaged as an AppImage with a plugin."""
     # Mock the existence of some plugins
     gtk_plugin_path = (
-        tmp_path
-        / "briefcase"
-        / "tools"
-        / "linuxdeploy_plugins"
-        / "gtk"
-        / "linuxdeploy-plugin-gtk.sh"
+        tmp_path / "briefcase/tools/linuxdeploy_plugins/gtk/linuxdeploy-plugin-gtk.sh"
     )
     gtk_plugin_path.parent.mkdir(parents=True)
     gtk_plugin_path.touch()
@@ -246,15 +221,7 @@ def test_build_appimage_with_plugin(build_command, first_app, tmp_path, sub_stre
     build_command.build_app(first_app)
 
     # linuxdeploy was invoked
-    app_dir = (
-        tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "linux"
-        / "appimage"
-        / "First App.AppDir"
-    )
+    app_dir = tmp_path / "base_path/build/first-app/linux/appimage/First App.AppDir"
     build_command._subprocess.Popen.assert_called_with(
         [
             os.fsdecode(tmp_path / "briefcase/tools/linuxdeploy-x86_64.AppImage"),
@@ -290,23 +257,13 @@ def test_build_appimage_with_plugin(build_command, first_app, tmp_path, sub_stre
     # Local plugin marked executable
     build_command.tools.os.chmod.assert_any_call(
         tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "linux"
-        / "appimage"
-        / "linuxdeploy-plugin-something.sh",
+        / "base_path/build/first-app/linux/appimage/linuxdeploy-plugin-something.sh",
         0o755,
     )
     # Binary is marked executable
     build_command.tools.os.chmod.assert_called_with(
         tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "linux"
-        / "appimage"
-        / "First_App-0.0.1-x86_64.AppImage",
+        / "base_path/build/first-app/linux/appimage/First_App-0.0.1-x86_64.AppImage",
         0o755,
     )
 
@@ -315,8 +272,7 @@ def test_build_failure(build_command, first_app, tmp_path, sub_stream_kw):
     """If linuxdeploy fails, the build is stopped."""
     # Mock a failure in the build
     build_command._subprocess.Popen.side_effect = subprocess.CalledProcessError(
-        cmd=["linuxdeploy-x86_64.AppImage", "..."],
-        returncode=1,
+        cmd=["linuxdeploy-x86_64.AppImage", "..."], returncode=1
     )
 
     # Invoking the build will raise an error.
@@ -325,15 +281,7 @@ def test_build_failure(build_command, first_app, tmp_path, sub_stream_kw):
         build_command.build_app(first_app)
 
     # linuxdeploy was invoked
-    app_dir = (
-        tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "linux"
-        / "appimage"
-        / "First App.AppDir"
-    )
+    app_dir = tmp_path / "base_path/build/first-app/linux/appimage/First App.AppDir"
     build_command._subprocess.Popen.assert_called_with(
         [
             os.fsdecode(tmp_path / "briefcase/tools/linuxdeploy-x86_64.AppImage"),
@@ -370,7 +318,11 @@ def test_build_failure(build_command, first_app, tmp_path, sub_stream_kw):
     sys.platform == "win32", reason="Windows paths aren't converted in Docker context"
 )
 def test_build_appimage_in_docker(
-    build_command, first_app, sub_stream_kw, tmp_path, monkeypatch
+    build_command,
+    first_app,
+    sub_stream_kw,
+    tmp_path,
+    monkeypatch,
 ):
     """A Linux app can be packaged as an AppImage in a docker container."""
 
@@ -391,19 +343,9 @@ def test_build_appimage_in_docker(
     build_command.tools[first_app].app_context.prepare(
         image_tag=f"briefcase/com.example.first-app:py3.{sys.version_info.minor}",
         dockerfile_path=tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "linux"
-        / "appimage"
-        / "Dockerfile",
+        / "base_path/build/first-app/linux/appimage/Dockerfile",
         app_base_path=tmp_path / "base_path",
-        host_bundle_path=tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "linux"
-        / "appimage",
+        host_bundle_path=tmp_path / "base_path/build/first-app/linux/appimage",
         host_data_path=tmp_path / "briefcase",
         python_version=f"3.{sys.version_info.minor}",
     )
@@ -417,7 +359,7 @@ def test_build_appimage_in_docker(
             "run",
             "--rm",
             "--volume",
-            f"{tmp_path / 'base_path' / 'build' / 'first-app' / 'linux' / 'appimage'}:/app:z",
+            f"{tmp_path / 'base_path/build/first-app/linux/appimage'}:/app:z",
             "--volume",
             f"{build_command.data_path}:/briefcase:z",
             "--env",
@@ -455,12 +397,7 @@ def test_build_appimage_in_docker(
     # Binary is marked executable
     build_command.tools.os.chmod.assert_called_with(
         tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "linux"
-        / "appimage"
-        / "First_App-0.0.1-x86_64.AppImage",
+        / "base_path/build/first-app/linux/appimage/First_App-0.0.1-x86_64.AppImage",
         0o755,
     )
 
@@ -478,12 +415,7 @@ def test_build_appimage_with_plugins_in_docker(
     """A Linux app can be packaged as an AppImage with plugins in a Docker container."""
     # Mock the existence of some plugins
     gtk_plugin_path = (
-        tmp_path
-        / "briefcase"
-        / "tools"
-        / "linuxdeploy_plugins"
-        / "gtk"
-        / "linuxdeploy-plugin-gtk.sh"
+        tmp_path / "briefcase/tools/linuxdeploy_plugins/gtk/linuxdeploy-plugin-gtk.sh"
     )
     gtk_plugin_path.parent.mkdir(parents=True)
     gtk_plugin_path.touch()
@@ -518,19 +450,9 @@ def test_build_appimage_with_plugins_in_docker(
     build_command.tools[first_app].app_context.prepare(
         image_tag=f"briefcase/com.example.first-app:py3.{sys.version_info.minor}",
         dockerfile_path=tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "linux"
-        / "appimage"
-        / "Dockerfile",
+        / "base_path/build/first-app/linux/appimage/Dockerfile",
         app_base_path=tmp_path / "base_path",
-        host_bundle_path=tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "linux"
-        / "appimage",
+        host_bundle_path=tmp_path / "base_path/build/first-app/linux/appimage",
         host_data_path=tmp_path / "briefcase",
         python_version=f"3.{sys.version_info.minor}",
     )
@@ -544,7 +466,7 @@ def test_build_appimage_with_plugins_in_docker(
             "run",
             "--rm",
             "--volume",
-            f"{tmp_path / 'base_path' / 'build' / 'first-app' / 'linux' / 'appimage'}:/app:z",
+            f"{tmp_path / 'base_path/build/first-app/linux/appimage'}:/app:z",
             "--volume",
             f"{build_command.data_path}:/briefcase:z",
             "--env",
@@ -593,23 +515,13 @@ def test_build_appimage_with_plugins_in_docker(
     # Local plugin marked executable
     build_command.tools.os.chmod.assert_any_call(
         tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "linux"
-        / "appimage"
-        / "linuxdeploy-plugin-something.sh",
+        / "base_path/build/first-app/linux/appimage/linuxdeploy-plugin-something.sh",
         0o755,
     )
     # Binary is marked executable
     build_command.tools.os.chmod.assert_called_with(
         tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "linux"
-        / "appimage"
-        / "First_App-0.0.1-x86_64.AppImage",
+        / "base_path/build/first-app/linux/appimage/First_App-0.0.1-x86_64.AppImage",
         0o755,
     )
 
@@ -617,14 +529,13 @@ def test_build_appimage_with_plugins_in_docker(
 def test_build_appimage_with_support_package_update(
     build_command,
     first_app,
-    tmp_path,
     sub_stream_kw,
+    tmp_path,
     capsys,
 ):
     """If a support package update is performed, the user is warned."""
     # To trigger the app package update logic, we need to invoke the full build
-    # command, and fake being on a verified Linux install with a generated
-    # app.
+    # command, and fake being on a verified Linux install with a generated app.
     build_command.tools.host_os = "Linux"
 
     # Mock shutil so we can check for folder deletion
@@ -639,10 +550,7 @@ def test_build_appimage_with_support_package_update(
     first_app.linuxdeploy_plugins = []
 
     # Fake the existence of some source files.
-    create_file(
-        tmp_path / "base_path/src/first_app/app.py",
-        "print('an app')",
-    )
+    create_file(tmp_path / "base_path/src/first_app/app.py", "print('an app')")
 
     # Mock the generated app template
     (build_command.bundle_path(first_app) / "src").mkdir(parents=True)
@@ -662,15 +570,7 @@ def test_build_appimage_with_support_package_update(
     build_command(first_app, update_support=True)
 
     # linuxdeploy was invoked
-    app_dir = (
-        tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "linux"
-        / "appimage"
-        / "First App.AppDir"
-    )
+    app_dir = tmp_path / "base_path/build/first-app/linux/appimage/First App.AppDir"
     build_command._subprocess.Popen.assert_called_with(
         [
             os.fsdecode(tmp_path / "briefcase/tools/linuxdeploy-x86_64.AppImage"),
@@ -701,11 +601,6 @@ def test_build_appimage_with_support_package_update(
     # Binary is marked executable
     build_command.tools.os.chmod.assert_called_with(
         tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "linux"
-        / "appimage"
-        / "First_App-0.0.1-x86_64.AppImage",
+        / "base_path/build/first-app/linux/appimage/First_App-0.0.1-x86_64.AppImage",
         0o755,
     )
diff --git a/tests/platforms/linux/appimage/test_mixin.py b/tests/platforms/linux/appimage/test_mixin.py
index 17cdc3fe..e33f11bd 100644
--- a/tests/platforms/linux/appimage/test_mixin.py
+++ b/tests/platforms/linux/appimage/test_mixin.py
@@ -35,12 +35,7 @@ def test_binary_path(create_command, first_app_config, tmp_path):
     assert (
         binary_path
         == tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "linux"
-        / "appimage"
-        / "First_App-0.0.1-x86_64.AppImage"
+        / "base_path/build/first-app/linux/appimage/First_App-0.0.1-x86_64.AppImage"
     )
 
 
@@ -105,10 +100,11 @@ def test_verify_linux_no_docker(create_command, first_app_config, tmp_path):
     assert not hasattr(create_command.tools, "docker")
 
 
-def test_verify_linux_docker(create_command, tmp_path, first_app_config, monkeypatch):
+def test_verify_linux_docker(create_command, first_app_config, tmp_path, monkeypatch):
     """If Docker is enabled on Linux, the Docker alias is set."""
     create_command.tools.host_os = "Linux"
     create_command.use_docker = True
+    create_command.extra_docker_build_args = ["--option-one", "--option-two"]
 
     # Mock Docker tool verification
     mock__version_compat = MagicMock(spec=Docker._version_compat)
@@ -157,21 +153,12 @@ def test_verify_linux_docker(create_command, tmp_path, first_app_config, monkeyp
         app=first_app_config,
         image_tag="briefcase/com.example.first-app:appimage",
         dockerfile_path=tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "linux"
-        / "appimage"
-        / "Dockerfile",
+        / "base_path/build/first-app/linux/appimage/Dockerfile",
         app_base_path=tmp_path / "base_path",
-        host_bundle_path=tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "linux"
-        / "appimage",
+        host_bundle_path=tmp_path / "base_path/build/first-app/linux/appimage",
         host_data_path=tmp_path / "briefcase",
         python_version=f"3.{sys.version_info.minor}",
+        extra_build_args=["--option-one", "--option-two"],
     )
 
 
@@ -184,6 +171,7 @@ def test_verify_non_linux_docker(
     """If Docker is enabled on non-Linux, the Docker alias is set."""
     create_command.tools.host_os = "Darwin"
     create_command.use_docker = True
+    create_command.extra_docker_build_args = ["--option-one", "--option-two"]
 
     # Mock Docker tool verification
     mock__version_compat = MagicMock(spec=Docker._version_compat)
@@ -232,21 +220,12 @@ def test_verify_non_linux_docker(
         app=first_app_config,
         image_tag="briefcase/com.example.first-app:appimage",
         dockerfile_path=tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "linux"
-        / "appimage"
-        / "Dockerfile",
+        / "base_path/build/first-app/linux/appimage/Dockerfile",
         app_base_path=tmp_path / "base_path",
-        host_bundle_path=tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "linux"
-        / "appimage",
+        host_bundle_path=tmp_path / "base_path/build/first-app/linux/appimage",
         host_data_path=tmp_path / "briefcase",
         python_version=f"3.{sys.version_info.minor}",
+        extra_build_args=["--option-one", "--option-two"],
     )
 
 
@@ -259,9 +238,11 @@ def test_clone_options(tmp_path):
         data_path=tmp_path / "briefcase",
     )
     build_command.use_docker = True
+    build_command.extra_docker_build_args = ["--option-one", "--option-two"]
 
     create_command = build_command.create_command
 
     # Confirm the use_docker option has been cloned
     assert create_command.is_clone
     assert create_command.use_docker
+    assert create_command.extra_docker_build_args == ["--option-one", "--option-two"]
diff --git a/tests/platforms/linux/appimage/test_package.py b/tests/platforms/linux/appimage/test_package.py
index a6e71ac6..3149702f 100644
--- a/tests/platforms/linux/appimage/test_package.py
+++ b/tests/platforms/linux/appimage/test_package.py
@@ -29,12 +29,7 @@ def test_package_app(package_command, first_app_config, tmp_path):
     # Create the app binary
     create_file(
         tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "linux"
-        / "appimage"
-        / "First_App-0.0.1-x86_64.AppImage",
+        / "base_path/build/first-app/linux/appimage/First_App-0.0.1-x86_64.AppImage",
         "AppImage",
     )
 
diff --git a/tests/platforms/linux/appimage/test_run.py b/tests/platforms/linux/appimage/test_run.py
index c2737537..294c9ea8 100644
--- a/tests/platforms/linux/appimage/test_run.py
+++ b/tests/platforms/linux/appimage/test_run.py
@@ -58,12 +58,7 @@ def test_run_app(run_command, first_app_config, tmp_path):
         [
             os.fsdecode(
                 tmp_path
-                / "base_path"
-                / "build"
-                / "first-app"
-                / "linux"
-                / "appimage"
-                / "First_App-0.0.1-x86_64.AppImage"
+                / "base_path/build/first-app/linux/appimage/First_App-0.0.1-x86_64.AppImage"
             )
         ],
         cwd=tmp_path / "home",
@@ -99,12 +94,7 @@ def test_run_app_with_passthrough(run_command, first_app_config, tmp_path):
         [
             os.fsdecode(
                 tmp_path
-                / "base_path"
-                / "build"
-                / "first-app"
-                / "linux"
-                / "appimage"
-                / "First_App-0.0.1-x86_64.AppImage"
+                / "base_path/build/first-app/linux/appimage/First_App-0.0.1-x86_64.AppImage"
             ),
             "foo",
             "--bar",
@@ -136,12 +126,7 @@ def test_run_app_failed(run_command, first_app_config, tmp_path):
         [
             os.fsdecode(
                 tmp_path
-                / "base_path"
-                / "build"
-                / "first-app"
-                / "linux"
-                / "appimage"
-                / "First_App-0.0.1-x86_64.AppImage"
+                / "base_path/build/first-app/linux/appimage/First_App-0.0.1-x86_64.AppImage"
             )
         ],
         cwd=tmp_path / "home",
@@ -168,12 +153,7 @@ def test_run_app_test_mode(run_command, first_app_config, tmp_path):
         [
             os.fsdecode(
                 tmp_path
-                / "base_path"
-                / "build"
-                / "first-app"
-                / "linux"
-                / "appimage"
-                / "First_App-0.0.1-x86_64.AppImage"
+                / "base_path/build/first-app/linux/appimage/First_App-0.0.1-x86_64.AppImage"
             )
         ],
         cwd=tmp_path / "home",
@@ -210,12 +190,7 @@ def test_run_app_test_mode_with_args(run_command, first_app_config, tmp_path):
         [
             os.fsdecode(
                 tmp_path
-                / "base_path"
-                / "build"
-                / "first-app"
-                / "linux"
-                / "appimage"
-                / "First_App-0.0.1-x86_64.AppImage"
+                / "base_path/build/first-app/linux/appimage/First_App-0.0.1-x86_64.AppImage"
             ),
             "foo",
             "--bar",
diff --git a/tests/platforms/linux/system/test_create.py b/tests/platforms/linux/system/test_create.py
index 12a212a7..68498ecf 100644
--- a/tests/platforms/linux/system/test_create.py
+++ b/tests/platforms/linux/system/test_create.py
@@ -18,13 +18,19 @@ def test_default_options(create_command):
 def test_options(create_command):
     """The extra options can be parsed."""
     options, overrides = create_command.parse_options(
-        ["--target", "somevendor:surprising"]
+        [
+            "--target",
+            "somevendor:surprising",
+            "--Xdocker-build=--option-one",
+            "--Xdocker-build=--option-two",
+        ]
     )
 
     assert options == {}
     assert overrides == {}
 
     assert create_command.target_image == "somevendor:surprising"
+    assert create_command.extra_docker_build_args == ["--option-one", "--option-two"]
 
 
 @pytest.mark.parametrize("host_os", ["Windows", "WeirdOS"])
diff --git a/tests/platforms/linux/system/test_mixin__properties.py b/tests/platforms/linux/system/test_mixin__properties.py
index 230b4820..fca7fe3b 100644
--- a/tests/platforms/linux/system/test_mixin__properties.py
+++ b/tests/platforms/linux/system/test_mixin__properties.py
@@ -67,15 +67,7 @@ def test_binary_path(create_command, first_app_config, tmp_path):
     assert (
         create_command.binary_path(first_app_config)
         == tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "somevendor"
-        / "surprising"
-        / "first-app-0.0.1"
-        / "usr"
-        / "bin"
-        / "first-app"
+        / "base_path/build/first-app/somevendor/surprising/first-app-0.0.1/usr/bin/first-app"
     )
 
 
@@ -178,9 +170,11 @@ def test_clone_options(create_command, tmp_path):
         data_path=tmp_path / "briefcase",
     )
     build_command.target_image = "somevendor:surprising"
+    build_command.extra_docker_build_args = ["--option-one", "--option-two"]
 
     create_command = build_command.create_command
 
     # Confirm the use_docker option has been cloned.
     assert create_command.is_clone
     assert create_command.target_image == "somevendor:surprising"
+    assert create_command.extra_docker_build_args == ["--option-one", "--option-two"]
diff --git a/tests/platforms/linux/system/test_mixin__verify.py b/tests/platforms/linux/system/test_mixin__verify.py
index 3ad4dfb1..e20c1554 100644
--- a/tests/platforms/linux/system/test_mixin__verify.py
+++ b/tests/platforms/linux/system/test_mixin__verify.py
@@ -5,7 +5,7 @@ from briefcase.integrations.docker import Docker, DockerAppContext
 from briefcase.integrations.subprocess import Subprocess
 
 
-def test_linux_no_docker(monkeypatch, create_command, first_app_config):
+def test_linux_no_docker(create_command, first_app_config, monkeypatch):
     """If Docker is disabled on Linux, the app_context is Subprocess."""
     create_command.tools.host_os = "Linux"
     create_command.target_image = None
@@ -37,10 +37,11 @@ def test_linux_no_docker(monkeypatch, create_command, first_app_config):
     create_command.verify_system_python.assert_not_called()
 
 
-def test_linux_docker(create_command, tmp_path, first_app_config, monkeypatch):
+def test_linux_docker(create_command, first_app_config, tmp_path, monkeypatch):
     """If Docker is enabled on Linux, the Docker alias is set."""
     create_command.tools.host_os = "Linux"
     create_command.target_image = "somevendor:surprising"
+    create_command.extra_docker_build_args = ["--option-one", "--option-two"]
 
     # Force a dummy vendor:codename for test purposes.
     first_app_config.target_vendor = "somevendor"
@@ -96,21 +97,12 @@ def test_linux_docker(create_command, tmp_path, first_app_config, monkeypatch):
         app=first_app_config,
         image_tag="briefcase/com.example.first-app:somevendor-surprising",
         dockerfile_path=tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "somevendor"
-        / "surprising"
-        / "Dockerfile",
+        / "base_path/build/first-app/somevendor/surprising/Dockerfile",
         app_base_path=tmp_path / "base_path",
-        host_bundle_path=tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "somevendor"
-        / "surprising",
+        host_bundle_path=tmp_path / "base_path/build/first-app/somevendor/surprising",
         host_data_path=tmp_path / "briefcase",
         python_version="3",
+        extra_build_args=["--option-one", "--option-two"],
     )
 
     # Python was also verified
@@ -124,10 +116,11 @@ def test_linux_docker(create_command, tmp_path, first_app_config, monkeypatch):
     create_command.verify_python.assert_not_called()
 
 
-def test_non_linux_docker(create_command, first_app_config, monkeypatch, tmp_path):
+def test_non_linux_docker(create_command, first_app_config, tmp_path, monkeypatch):
     """If Docker is enabled on non-Linux, the Docker alias is set."""
     create_command.tools.host_os = "Darwin"
     create_command.target_image = "somevendor:surprising"
+    create_command.extra_docker_build_args = ["--option-one", "--option-two"]
 
     # Force a dummy vendor:codename for test purposes.
     first_app_config.target_vendor = "somevendor"
@@ -183,21 +176,12 @@ def test_non_linux_docker(create_command, first_app_config, monkeypatch, tmp_pat
         app=first_app_config,
         image_tag="briefcase/com.example.first-app:somevendor-surprising",
         dockerfile_path=tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "somevendor"
-        / "surprising"
-        / "Dockerfile",
+        / "base_path/build/first-app/somevendor/surprising/Dockerfile",
         app_base_path=tmp_path / "base_path",
-        host_bundle_path=tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "somevendor"
-        / "surprising",
+        host_bundle_path=tmp_path / "base_path/build/first-app/somevendor/surprising",
         host_data_path=tmp_path / "briefcase",
         python_version="3",
+        extra_build_args=["--option-one", "--option-two"],
     )
 
     # Python was also verified
diff --git a/tests/platforms/linux/system/test_package__deb.py b/tests/platforms/linux/system/test_package__deb.py
index 9bc7514d..cca9d238 100644
--- a/tests/platforms/linux/system/test_package__deb.py
+++ b/tests/platforms/linux/system/test_package__deb.py
@@ -38,6 +38,10 @@ def package_command(first_app, tmp_path):
     # Make the mock rmtree still remove content
     command.tools.shutil.rmtree.side_effect = shutil.rmtree
 
+    # Mock not using docker
+    command.target_image = None
+    command.extra_docker_build_args = []
+
     return command
 
 
@@ -53,11 +57,8 @@ def first_app_deb(first_app):
     return first_app
 
 
-def test_verify_no_docker(monkeypatch, package_command, first_app_deb):
+def test_verify_no_docker(package_command, first_app_deb, monkeypatch):
     """If not using docker, existence of dpkg-deb is verified."""
-    # Mock not using docker
-    package_command.target_image = None
-
     # Mock the existence of dpkg-deb
     package_command.tools.shutil.which = mock.MagicMock(return_value="/mybin/dpkg-deb")
 
@@ -79,11 +80,11 @@ def test_verify_no_docker(monkeypatch, package_command, first_app_deb):
     ],
 )
 def test_verify_dpkg_deb_missing(
-    monkeypatch,
     package_command,
     first_app_deb,
     vendor_base,
     error_msg,
+    monkeypatch,
 ):
     """If dpkg-deb isn't installed, an error is raised."""
     # Mock distro so packager is found or not appropriately
@@ -92,9 +93,6 @@ def test_verify_dpkg_deb_missing(
     # Mock packager as missing
     package_command.tools.shutil.which = mock.MagicMock(return_value="")
 
-    # Mock not using docker
-    package_command.target_image = None
-
     # Verifying app tools will raise an error
     with pytest.raises(BriefcaseCommandError, match=error_msg):
         package_command.verify_app_tools(first_app_deb)
@@ -103,7 +101,7 @@ def test_verify_dpkg_deb_missing(
     package_command.tools.shutil.which.assert_called_once_with("dpkg-deb")
 
 
-def test_verify_docker(monkeypatch, package_command, first_app_deb):
+def test_verify_docker(package_command, first_app_deb, monkeypatch):
     """If using Docker, no tool checks are needed."""
     # Mock using docker
     package_command.target_image = "somevendor:surprising"
@@ -176,10 +174,7 @@ def test_deb_package(package_command, first_app_deb, tmp_path):
     # The deb was moved into the final location
     package_command.tools.shutil.move.assert_called_once_with(
         bundle_path / "first-app-0.0.1.deb",
-        tmp_path
-        / "base_path"
-        / "dist"
-        / "first-app_0.0.1-1~somevendor-surprising_wonky.deb",
+        tmp_path / "base_path/dist/first-app_0.0.1-1~somevendor-surprising_wonky.deb",
     )
 
 
@@ -232,10 +227,7 @@ def test_deb_re_package(package_command, first_app_deb, tmp_path):
     # The deb was moved into the final location
     package_command.tools.shutil.move.assert_called_once_with(
         bundle_path / "first-app-0.0.1.deb",
-        tmp_path
-        / "base_path"
-        / "dist"
-        / "first-app_0.0.1-1~somevendor-surprising_wonky.deb",
+        tmp_path / "base_path/dist/first-app_0.0.1-1~somevendor-surprising_wonky.deb",
     )
 
 
@@ -324,10 +316,7 @@ def test_deb_package_extra_requirements(package_command, first_app_deb, tmp_path
     # The deb was moved into the final location
     package_command.tools.shutil.move.assert_called_once_with(
         bundle_path / "first-app-0.0.1.deb",
-        tmp_path
-        / "base_path"
-        / "dist"
-        / "first-app_0.0.1-42~somevendor-surprising_wonky.deb",
+        tmp_path / "base_path/dist/first-app_0.0.1-42~somevendor-surprising_wonky.deb",
     )
 
 
diff --git a/tests/platforms/linux/system/test_package__pkg.py b/tests/platforms/linux/system/test_package__pkg.py
index 44d72ddf..4832b1f1 100644
--- a/tests/platforms/linux/system/test_package__pkg.py
+++ b/tests/platforms/linux/system/test_package__pkg.py
@@ -43,6 +43,10 @@ def package_command(first_app, tmp_path):
     # Make the mock rmtree still remove content
     command.tools.shutil.rmtree = mock.MagicMock(side_effect=shutil.rmtree)
 
+    # Mock not using docker
+    command.target_image = None
+    command.extra_docker_build_args = []
+
     return command
 
 
@@ -58,14 +62,7 @@ def first_app_pkg(first_app, tmp_path):
 
     # Mock the side effects of building the app
     usr_dir = (
-        tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "somevendor"
-        / "surprising"
-        / "first-app-0.0.1"
-        / "usr"
+        tmp_path / "base_path/build/first-app/somevendor/surprising/first-app-0.0.1/usr"
     )
 
     # Create the binary
@@ -81,11 +78,8 @@ def first_app_pkg(first_app, tmp_path):
     return first_app
 
 
-def test_verify_no_docker(monkeypatch, package_command, first_app_pkg):
+def test_verify_no_docker(package_command, first_app_pkg, monkeypatch):
     """If not using docker, existence of makepkg is verified."""
-    # Mock not using docker
-    package_command.target_image = None
-
     # Mock the existence of makepkg
     package_command.tools.shutil.which = mock.MagicMock(return_value="/mybin/makepkg")
 
@@ -107,11 +101,11 @@ def test_verify_no_docker(monkeypatch, package_command, first_app_pkg):
     ],
 )
 def test_verify_makepkg_missing(
-    monkeypatch,
     package_command,
     first_app_pkg,
     vendor_base,
     error_msg,
+    monkeypatch,
 ):
     """If makepkg isn't installed, an error is raised."""
     # Mock distro so packager is found or not appropriately
@@ -120,9 +114,6 @@ def test_verify_makepkg_missing(
     # Mock packager as missing
     package_command.tools.shutil.which = mock.MagicMock(return_value="")
 
-    # Mock not using docker
-    package_command.target_image = None
-
     # Verifying app tools will raise an error
     with pytest.raises(BriefcaseCommandError, match=error_msg):
         package_command.verify_app_tools(first_app_pkg)
@@ -131,7 +122,7 @@ def test_verify_makepkg_missing(
     package_command.tools.shutil.which.assert_called_once_with("makepkg")
 
 
-def test_verify_docker(monkeypatch, package_command, first_app_pkg):
+def test_verify_docker(package_command, first_app_pkg, monkeypatch):
     """If using Docker, no tool checks are needed."""
     # Mock using docker
     package_command.target_image = "somevendor:surprising"
diff --git a/tests/platforms/linux/system/test_package__rpm.py b/tests/platforms/linux/system/test_package__rpm.py
index cf472239..e8051aeb 100644
--- a/tests/platforms/linux/system/test_package__rpm.py
+++ b/tests/platforms/linux/system/test_package__rpm.py
@@ -42,6 +42,11 @@ def package_command(first_app, tmp_path):
 
     # Mock the RPM tag, since "somevendor" won't identify as rpm.
     command.rpm_tag = mock.MagicMock(return_value="fcXX")
+
+    # Mock not using docker
+    command.target_image = None
+    command.extra_docker_build_args = []
+
     return command
 
 
@@ -56,14 +61,7 @@ def first_app_rpm(first_app, tmp_path):
 
     # Mock the side effects of building the app
     usr_dir = (
-        tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "somevendor"
-        / "surprising"
-        / "first-app-0.0.1"
-        / "usr"
+        tmp_path / "base_path/build/first-app/somevendor/surprising/first-app-0.0.1/usr"
     )
 
     # Create the binary
@@ -79,11 +77,8 @@ def first_app_rpm(first_app, tmp_path):
     return first_app
 
 
-def test_verify_no_docker(monkeypatch, package_command, first_app_rpm):
+def test_verify_no_docker(package_command, first_app_rpm, monkeypatch):
     """If not using docker, existence of rpmbuild is verified."""
-    # Mock not using docker
-    package_command.target_image = None
-
     # Mock the existence of rpmbuild
     package_command.tools.shutil.which = mock.MagicMock(return_value="/mybin/rpmbuild")
 
@@ -105,11 +100,11 @@ def test_verify_no_docker(monkeypatch, package_command, first_app_rpm):
     ],
 )
 def test_verify_rpmbuild_missing(
-    monkeypatch,
     package_command,
     first_app_rpm,
     vendor_base,
     error_msg,
+    monkeypatch,
 ):
     """If rpmbuild isn't installed, an error is raised."""
     # Mock distro so packager is found or not appropriately
@@ -118,9 +113,6 @@ def test_verify_rpmbuild_missing(
     # Mock packager as missing
     package_command.tools.shutil.which = mock.MagicMock(return_value="")
 
-    # Mock not using docker
-    package_command.target_image = None
-
     # Verifying app tools will raise an error
     with pytest.raises(BriefcaseCommandError, match=error_msg):
         package_command.verify_app_tools(first_app_rpm)
@@ -129,7 +121,7 @@ def test_verify_rpmbuild_missing(
     package_command.tools.shutil.which.assert_called_once_with("rpmbuild")
 
 
-def test_verify_docker(monkeypatch, package_command, first_app_rpm):
+def test_verify_docker(package_command, first_app_rpm, monkeypatch):
     """If using Docker, no tool checks are needed."""
     # Mock using docker
     package_command.target_image = "somevendor:surprising"
@@ -428,11 +420,7 @@ def test_rpm_re_package(package_command, first_app_rpm, tmp_path):
 
     # The rpm was moved into the final location
     package_command.tools.shutil.move.assert_called_once_with(
-        bundle_path
-        / "rpmbuild"
-        / "RPMS"
-        / "wonky"
-        / "first-app-0.0.1-1.fcXX.wonky.rpm",
+        bundle_path / "rpmbuild/RPMS/wonky/first-app-0.0.1-1.fcXX.wonky.rpm",
         tmp_path / "base_path/dist/first-app-0.0.1-1.fcXX.wonky.rpm",
     )
 
@@ -575,7 +563,7 @@ def test_rpm_package_extra_requirements(package_command, first_app_rpm, tmp_path
 
 @pytest.mark.skipif(sys.platform == "win32", reason="Can't build RPMs on Windows")
 def test_rpm_package_failure(package_command, first_app_rpm, tmp_path):
-    """If an packaging doesn't succeed, an error is raised."""
+    """If packaging doesn't succeed, an error is raised."""
     bundle_path = tmp_path / "base_path/build/first-app/somevendor/surprising"
 
     # Mock a packaging failure
diff --git a/tests/platforms/linux/system/test_run.py b/tests/platforms/linux/system/test_run.py
index 843c206d..1439a803 100644
--- a/tests/platforms/linux/system/test_run.py
+++ b/tests/platforms/linux/system/test_run.py
@@ -59,6 +59,7 @@ def run_command(tmp_path, first_app, monkeypatch):
 
     # Disable Docker by default
     command.target_image = None
+    command.extra_docker_build_args = []
 
     command.tools.subprocess._subprocess = mock.MagicMock(spec_set=subprocess)
     command.tools.subprocess.run = mock.MagicMock(spec_set=Subprocess.run)
@@ -248,15 +249,7 @@ def test_run_app(run_command, first_app, sub_kw, tmp_path):
         [
             os.fsdecode(
                 tmp_path
-                / "base_path"
-                / "build"
-                / "first-app"
-                / "somevendor"
-                / "surprising"
-                / "first-app-0.0.1"
-                / "usr"
-                / "bin"
-                / "first-app"
+                / "base_path/build/first-app/somevendor/surprising/first-app-0.0.1/usr/bin/first-app"
             )
         ],
         cwd=os.fsdecode(tmp_path / "home"),
diff --git a/tests/platforms/linux/test_DockerOpenCommand.py b/tests/platforms/linux/test_DockerOpenCommand.py
index 56e41108..ae09ac6b 100644
--- a/tests/platforms/linux/test_DockerOpenCommand.py
+++ b/tests/platforms/linux/test_DockerOpenCommand.py
@@ -47,6 +47,7 @@ def test_open_docker(open_command, first_app_config, tmp_path, monkeypatch):
 
     # Enable docker
     open_command.use_docker = True
+    open_command.extra_docker_build_args = []
 
     # Provide Docker
     monkeypatch.setattr(
@@ -61,19 +62,9 @@ def test_open_docker(open_command, first_app_config, tmp_path, monkeypatch):
     open_command.tools[first_app_config].app_context.prepare(
         image_tag=f"briefcase/com.example.first-app:py3.{sys.version_info.minor}",
         dockerfile_path=tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "linux"
-        / "appimage"
-        / "Dockerfile",
+        / "base_path/build/first-app/linux/appimage/Dockerfile",
         app_base_path=tmp_path / "base_path",
-        host_bundle_path=tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "linux"
-        / "appimage",
+        host_bundle_path=tmp_path / "base_path/build/first-app/linux/appimage",
         host_data_path=tmp_path / "briefcase",
         python_version=f"3.{sys.version_info.minor}",
     )
@@ -83,8 +74,7 @@ def test_open_docker(open_command, first_app_config, tmp_path, monkeypatch):
     # Create the desktop file that would be in the project folder.
     create_file(
         open_command.project_path(first_app_config)
-        / "First App.AppDir"
-        / "com.example.firstapp.desktop",
+        / "First App.AppDir/com.example.firstapp.desktop",
         "FreeDesktop file",
     )
 
@@ -116,8 +106,7 @@ def test_open_no_docker_linux(open_command, first_app_config, tmp_path):
     # Create the desktop file that would be in the project folder.
     create_file(
         open_command.project_path(first_app_config)
-        / "First App.AppDir"
-        / "com.example.firstapp.desktop",
+        / "First App.AppDir/com.example.firstapp.desktop",
         "FreeDesktop file",
     )
 
@@ -140,8 +129,7 @@ def test_open_no_docker_macOS(open_command, first_app_config, tmp_path):
     # Create the desktop file that would be in the project folder.
     create_file(
         open_command.project_path(first_app_config)
-        / "First App.AppDir"
-        / "com.example.firstapp.desktop",
+        / "First App.AppDir/com.example.firstapp.desktop",
         "FreeDesktop file",
     )
 
diff --git a/tests/platforms/linux/test_LocalRequirementsMixin.py b/tests/platforms/linux/test_LocalRequirementsMixin.py
index 2792867c..db3d8002 100644
--- a/tests/platforms/linux/test_LocalRequirementsMixin.py
+++ b/tests/platforms/linux/test_LocalRequirementsMixin.py
@@ -77,20 +77,9 @@ def create_command(no_docker_create_command, first_app_config, tmp_path, monkeyp
     )
     no_docker_create_command.tools[first_app_config].app_context.prepare(
         image_tag="briefcase/com.example.first-app:py3.X",
-        dockerfile_path=tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "tester"
-        / "dummy"
-        / "Dockerfile",
+        dockerfile_path=tmp_path / "base_path/build/first-app/tester/dummy/Dockerfile",
         app_base_path=tmp_path / "base_path",
-        host_bundle_path=tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "tester"
-        / "dummy",
+        host_bundle_path=tmp_path / "base_path/build/first-app/tester/dummy",
         host_data_path=tmp_path / "briefcase",
         python_version="3.X",
     )
@@ -173,7 +162,7 @@ def test_install_app_requirements_in_docker(create_command, first_app_config, tm
             "run",
             "--rm",
             "--volume",
-            f"{tmp_path / 'base_path' / 'build' / 'first-app' / 'tester' / 'dummy'}:/app:z",
+            f"{tmp_path / 'base_path/build/first-app/tester/dummy'}:/app:z",
             "--volume",
             f"{tmp_path / 'briefcase'}:/briefcase:z",
             "briefcase/com.example.first-app:py3.X",
@@ -247,7 +236,7 @@ def test_install_app_requirements_no_docker(
             "--no-python-version-warning",
             "--upgrade",
             "--no-user",
-            f"--target={tmp_path}/base_path/build/first-app/tester/dummy/path/to/app_packages",
+            f"--target={tmp_path / 'base_path/build/first-app/tester/dummy/path/to/app_packages'}",
             "foo==1.2.3",
             "bar>=4.5",
         ],
@@ -308,13 +297,7 @@ def test_install_app_requirements_with_locals(
             "build",
             "--sdist",
             "--outdir",
-            tmp_path
-            / "base_path"
-            / "build"
-            / "first-app"
-            / "tester"
-            / "dummy"
-            / "_requirements",
+            tmp_path / "base_path/build/first-app/tester/dummy/_requirements",
             str(tmp_path / "local/first"),
         ],
         encoding="UTF-8",
@@ -324,23 +307,11 @@ def test_install_app_requirements_with_locals(
     create_command.tools.shutil.copy.mock_calls = [
         call(
             str(tmp_path / "local/second-2.3.4.tar.gz"),
-            tmp_path
-            / "base_path"
-            / "build"
-            / "first-app"
-            / "tester"
-            / "dummy"
-            / "_requirements",
+            tmp_path / "base_path/build/first-app/tester/dummy/_requirements",
         ),
         call(
             str(tmp_path / "local/third-3.4.5-py3-none-any.whl"),
-            tmp_path
-            / "base_path"
-            / "build"
-            / "first-app"
-            / "tester"
-            / "dummy"
-            / "_requirements",
+            tmp_path / "base_path/build/first-app/tester/dummy/_requirements",
         ),
     ]
 
@@ -351,7 +322,7 @@ def test_install_app_requirements_with_locals(
             "run",
             "--rm",
             "--volume",
-            f"{tmp_path / 'base_path' / 'build' / 'first-app' / 'tester' / 'dummy'}:/app:z",
+            f"{tmp_path / 'base_path/build/first-app/tester/dummy'}:/app:z",
             "--volume",
             f"{tmp_path / 'briefcase'}:/briefcase:z",
             "briefcase/com.example.first-app:py3.X",
@@ -428,13 +399,7 @@ def test_install_app_requirements_with_bad_local(
             "build",
             "--sdist",
             "--outdir",
-            tmp_path
-            / "base_path"
-            / "build"
-            / "first-app"
-            / "tester"
-            / "dummy"
-            / "_requirements",
+            tmp_path / "base_path/build/first-app/tester/dummy/_requirements",
             str(tmp_path / "local/first"),
         ],
         encoding="UTF-8",
@@ -504,13 +469,7 @@ def test_install_app_requirements_with_bad_local_file(
     # An attempt was made to copy the package
     create_command.tools.shutil.copy.assert_called_once_with(
         str(tmp_path / "local/missing-2.3.4.tar.gz"),
-        tmp_path
-        / "base_path"
-        / "build"
-        / "first-app"
-        / "tester"
-        / "dummy"
-        / "_requirements",
+        tmp_path / "base_path/build/first-app/tester/dummy/_requirements",
     )
 
     # No attempt was made to build the sdist