Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to target non-native architectures #1392

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 33 additions & 12 deletions src/briefcase/integrations/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
)


Expand All @@ -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:
Expand All @@ -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.
Expand Down Expand Up @@ -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

Expand All @@ -453,27 +469,29 @@ 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
self.host_bundle_path = host_bundle_path
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...",
prefix=self.app.app_name,
)
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",
Expand Down Expand Up @@ -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")
Expand Down
17 changes: 15 additions & 2 deletions src/briefcase/platforms/linux/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -362,18 +366,26 @@ 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

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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
Loading