diff --git a/src/briefcase/integrations/docker.py b/src/briefcase/integrations/docker.py index 86e219b5c..0b58cd6ea 100644 --- a/src/briefcase/integrations/docker.py +++ b/src/briefcase/integrations/docker.py @@ -111,7 +111,12 @@ class Docker(Tool): "Linux": "https://docs.docker.com/engine/install/#server", } - def __init__(self, tools: ToolCache, image_tag: str | None = None): + def __init__( + self, + tools: ToolCache, + platform: str | None = None, + image_tag: str | None = None, + ): """A wrapper for the user-installed Docker. :param tools: ToolCache of available tools @@ -121,18 +126,21 @@ def __init__(self, tools: ToolCache, image_tag: str | None = None): at all bound to the instance. """ super().__init__(tools=tools) + self.platform = platform self.is_user_mapped = self._is_user_mapping_enabled(image_tag) @classmethod def verify_install( cls, tools: ToolCache, + platform: str | None = None, image_tag: str | None = None, **kwargs, ) -> Docker: """Verify Docker is installed and operational. :param tools: ToolCache of available tools + :param platform: The platform to use to run images, e.g. linux/aarch64 :param image_tag: An optional image used during verification to access attributes of the local Docker environment. This image is not bound to the instance and only used during instantiation. @@ -145,7 +153,7 @@ def verify_install( cls._user_access(tools=tools) cls._buildx_installed(tools=tools) - tools.docker = Docker(tools=tools, image_tag=image_tag) + tools.docker = Docker(tools=tools, platform=platform, image_tag=image_tag) return tools.docker @classmethod @@ -212,6 +220,13 @@ def _buildx_installed(cls, tools: ToolCache): except subprocess.CalledProcessError: raise BriefcaseCommandError(cls.BUILDX_PLUGIN_MISSING) + @property + def _base_run_cmd(self): + cmd = ["docker", "run", "--rm"] + if self.platform: + cmd.extend(["--platform", self.platform]) + return cmd + def _write_test_path(self) -> Path: """Host system filepath to perform write test from a container.""" return Path.cwd() / "build" / "container_write_test" @@ -276,10 +291,7 @@ def _is_user_mapping_enabled(self, image_tag: str | None = None) -> bool: # log irrelevant errors when the image may just have a simple typo self.cache_image(image_tag) - docker_run_cmd = [ - "docker", - "run", - "--rm", + docker_run_cmd = self._base_run_cmd + [ "--volume", f"{host_write_test_path.parent}:{container_write_test_path.parent}:z", image_tag, @@ -339,6 +351,7 @@ def cache_image(self, image_tag: str): :param image_tag: Image name/tag to pull if not locally cached """ + # TODO:PR: ensure this check validates the platform for the image image_id = self.tools.subprocess.check_output( ["docker", "images", "-q", image_tag] ).strip() @@ -378,7 +391,7 @@ def check_output(self, args: list[SubprocessArgT], image_tag: str) -> str: # This ensures that "docker.check_output()" behaves as closely to # "subprocess.check_output()" as possible. return self.tools.subprocess.check_output( - ["docker", "run", "--rm", image_tag] + args + self._base_run_cmd + [image_tag] + args ) @@ -394,6 +407,7 @@ def __init__(self, tools: ToolCache, app: AppConfig): self.host_data_path: Path self.image_tag: str self.python_version: str + self.platform: str | None @property def docker_briefcase_path(self) -> PurePosixPath: @@ -411,6 +425,7 @@ def verify_install( host_bundle_path: Path, host_data_path: Path, python_version: str, + platform: str | None = None, **kwargs, ) -> DockerAppContext: """Verify that docker is available as an app-bound tool. @@ -442,6 +457,7 @@ def verify_install( host_bundle_path=host_bundle_path, host_data_path=host_data_path, python_version=python_version, + platform=platform, ) return tools[app].app_context @@ -453,6 +469,7 @@ def prepare( host_bundle_path: Path, host_data_path: Path, python_version: str, + platform: str | None, ): """Create/update the Docker image from the app's Dockerfile.""" self.app_base_path = app_base_path @@ -460,6 +477,7 @@ def prepare( self.host_data_path = host_data_path self.image_tag = image_tag self.python_version = python_version + self.platform = platform self.tools.logger.info( "Building Docker container image...", @@ -467,13 +485,13 @@ def prepare( ) with self.tools.input.wait_bar("Building Docker image..."): with self.tools.logger.context("Docker"): + build_cmd = ["docker", "buildx", "build", "--progress", "plain"] + if self.platform: + build_cmd.extend(["--platform", self.platform]) try: self.tools.subprocess.run( - [ - "docker", - "build", - "--progress", - "plain", + build_cmd + + [ "--tag", self.image_tag, "--file", @@ -542,6 +560,9 @@ def _dockerize_args( """ docker_args = ["docker", "run", "--rm"] + if self.platform: + docker_args.extend(["--platform", self.platform]) + # Add "-it" if in interactive mode if interactive: docker_args.append("-it") diff --git a/src/briefcase/platforms/linux/system.py b/src/briefcase/platforms/linux/system.py index c322ab7c1..dbdec6a00 100644 --- a/src/briefcase/platforms/linux/system.py +++ b/src/briefcase/platforms/linux/system.py @@ -352,7 +352,11 @@ def verify_tools(self): """If we're using Docker, verify that it is available.""" super().verify_tools() if self.use_docker: - Docker.verify(tools=self.tools, image_tag=self.target_image) + Docker.verify( + tools=self.tools, + image_tag=self.target_image, + platform=self.target_arch, + ) def add_options(self, parser): super().add_options(parser) @@ -362,11 +366,18 @@ def add_options(self, parser): help="Docker base image tag for the distribution to target for the build (e.g., `ubuntu:jammy`)", required=False, ) + parser.add_argument( + "--target-arch", + dest="target_arch", + help="Docker platform name for target architecture for app (e.g. linux/aarch64)", + required=False, + ) def parse_options(self, extra): """Extract the target_image option.""" options = super().parse_options(extra) self.target_image = options.pop("target") + self.target_arch = options.pop("target_arch") return options @@ -374,6 +385,7 @@ def clone_options(self, command): """Clone the target_image option.""" super().clone_options(command) self.target_image = command.target_image + self.target_arch = command.target_arch def verify_python(self, app: AppConfig): """Verify that the version of Python being used to build the app in Docker is @@ -574,6 +586,7 @@ def verify_app_tools(self, app: AppConfig): host_bundle_path=self.bundle_path(app), host_data_path=self.data_path, python_version=app.python_version_tag, + platform=self.target_arch, ) # Check the system Python on the target system to see if it is @@ -771,7 +784,7 @@ def build_app(self, app: AppConfig, **kwargs): path.chmod(new_perms) with self.input.wait_bar("Stripping binary..."): - self.tools.subprocess.check_output(["strip", self.binary_path(app)]) + self.tools[app].app_context.check_output(["strip", self.binary_path(app)]) class LinuxSystemRunCommand(LinuxSystemPassiveMixin, RunCommand):