Skip to content

Commit

Permalink
[WIP] Add support to target non-native architectures
Browse files Browse the repository at this point in the history
  • Loading branch information
rmartin16 committed Jul 29, 2023
1 parent a093588 commit 5d6f223
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 46 deletions.
106 changes: 62 additions & 44 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,43 @@ 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 cache_image(self, image_tag: str):
"""Ensures an image is available and cached locally.
While many Docker commands for an image will pull that image in-line with the
command if it isn't already cached, this pollutes the console output with
details about pulling the image. This can be particularly troublesome when the
output from a command run inside a container using the image is desired.
Note: This will not update an already cached image if a newer version is
available in the registry.
: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()

if not image_id:
try:
self.tools.subprocess.run(
["docker", "pull", "--platform", self.platform, image_tag],
check=True,
)
except subprocess.CalledProcessError as e:
raise BriefcaseCommandError(
f"Unable to obtain the Docker image for {image_tag}. "
"Is the image name correct?"
) from e

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 @@ -271,10 +316,7 @@ def _is_user_mapping_enabled(self, image_tag: str | None = None) -> bool:
"/host_write_test", host_write_test_path.name
)

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",
"alpine" if image_tag is None else image_tag,
Expand Down Expand Up @@ -323,32 +365,6 @@ def _is_user_mapping_enabled(self, image_tag: str | None = None) -> bool:

return is_user_mapped

def cache_image(self, image_tag: str):
"""Ensures an image is available and cached locally.
While many Docker commands for an image will pull that image in-line with the
command if it isn't already cached, this pollutes the console output with
details about pulling the image. This can be particularly troublesome when the
output from a command run inside a container using the image is desired.
Note: This will not update an already cached image if a newer version is
available in the registry.
:param image_tag: Image name/tag to pull if not locally cached
"""
image_id = self.tools.subprocess.check_output(
["docker", "images", "-q", image_tag]
).strip()

if not image_id:
try:
self.tools.subprocess.run(["docker", "pull", image_tag], check=True)
except subprocess.CalledProcessError as e:
raise BriefcaseCommandError(
f"Unable to obtain the Docker image for {image_tag}. "
"Is the image name correct?"
) from e

def check_output(self, args: list[SubprocessArgT], image_tag: str) -> str:
"""Run a process inside a Docker container, capturing output.
Expand All @@ -366,13 +382,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 @@ -388,6 +398,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_data_path(self) -> PurePosixPath:
Expand All @@ -405,6 +416,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 @@ -436,6 +448,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 @@ -447,27 +460,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 @@ -534,6 +549,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 @@ -351,7 +351,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 @@ -361,18 +365,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 @@ -566,6 +578,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 @@ -763,7 +776,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

0 comments on commit 5d6f223

Please sign in to comment.