Skip to content

Commit

Permalink
Improve support for arbitrary machine architectures
Browse files Browse the repository at this point in the history
- Prevent running on 32bit Windows or 32bit Python on 64bit Windows
- Support building an AppImage for i386; verify LinuxDeploy for all Commands
- Support building Linux System packages on i386
- Support installing JDK for armv7/8
- Let users know when Android SDK must be manually installed
- Derive the Linux system target distro architecture from the build environment
- Download a 32bit JDK and 32bit standalone Python when host Python is 32bit
  • Loading branch information
rmartin16 committed Jul 23, 2023
1 parent 2b29a42 commit 5e1b94f
Show file tree
Hide file tree
Showing 10 changed files with 224 additions and 99 deletions.
2 changes: 2 additions & 0 deletions src/briefcase/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,7 @@ def _download_support_package(self, app: AppConfig):
python_version_tag=self.python_version_tag,
platform=self.platform,
host_arch=self.tools.host_arch,
is_32bit=self.tools.is_32bit_python,
)

support_package_url = self.support_package_url(support_revision)
Expand Down Expand Up @@ -384,6 +385,7 @@ def _download_support_package(self, app: AppConfig):
python_version_tag=self.python_version_tag,
platform=self.platform,
host_arch=self.tools.host_arch,
is_32bit=self.tools.is_32bit_python,
) from e

def _write_requirements_file(
Expand Down
16 changes: 14 additions & 2 deletions src/briefcase/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,18 @@ def __init__(self, tool):
super().__init__(msg=f"Unable to locate {tool!r}. Has it been installed?")


class IncompatibleToolError(BriefcaseCommandError):
def __init__(self, tool: str, env_var: str):
self.tool = tool
super().__init__(
msg=f"""\
Briefcase cannot install {tool} on this machine.
Install {tool} manually and specify the installation directory in the {env_var} environment variable.
"""
)


class NonManagedToolError(BriefcaseCommandError):
def __init__(self, tool):
self.tool = tool
Expand Down Expand Up @@ -153,9 +165,9 @@ def __init__(self, app_bundle_path):


class MissingSupportPackage(BriefcaseCommandError):
def __init__(self, python_version_tag, platform, host_arch):
def __init__(self, python_version_tag, platform, host_arch, is_32bit):
self.python_version_tag = python_version_tag
self.platform = platform
self.platform = f"{'32 bit ' if is_32bit else ''}{platform}"
self.host_arch = host_arch
super().__init__(
f"""\
Expand Down
41 changes: 31 additions & 10 deletions src/briefcase/integrations/android_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from briefcase.console import InputDisabled, select_option
from briefcase.exceptions import (
BriefcaseCommandError,
IncompatibleToolError,
InvalidDeviceError,
MissingToolError,
)
Expand Down Expand Up @@ -57,15 +58,35 @@ def __init__(self, tools: ToolCache, root_path: Path):

@property
def cmdline_tools_url(self) -> str:
"""The Android SDK Command-Line Tools URL appropriate to the current operating
system."""
platform_name = self.tools.host_os.lower()
if self.tools.host_os.lower() == "darwin":
platform_name = "mac"
elif self.tools.host_os.lower() == "windows": # pragma: no branch
platform_name = "win"
"""The Android SDK Command-Line Tools URL appropriate for the current machine.
return f"https://dl.google.com/android/repository/commandlinetools-{platform_name}-{self.cmdline_tools_version}_latest.zip" # noqa: E501
The SDK largely only supports typical development environments; if a machine is
using an unsupported architecture, `sdkmanager` will error while installing the
emulator as a dependency of the build-tools. However, for some of the platforms
that are unsupported by sdkmanager, users can set up their own SDK install.
"""
try:
platform_name = {
"Darwin": {
"arm64": "mac",
"x86_64": "mac",
},
"Linux": {
"x86_64": "linux",
},
"Windows": {
"AMD64": "win",
},
}[self.tools.host_os][self.tools.host_arch]
except KeyError as e:
raise IncompatibleToolError(
tool=self.full_name, env_var="ANDROID_HOME"
) from e

return (
f"https://dl.google.com/android/repository/commandlinetools-"
f"{platform_name}-{self.cmdline_tools_version}_latest.zip"
)

@property
def cmdline_tools_path(self) -> Path:
Expand All @@ -74,11 +95,11 @@ def cmdline_tools_path(self) -> Path:
@property
def cmdline_tools_version(self) -> str:
# This is the version of the Android SDK Command-line tools that
# are current as of May 2022. These tools can generally self-update,
# are current as of July 2023. These tools can generally self-update,
# so using a fixed download URL isn't a problem.
# However, if/when this version number is changed, ensure that the
# checks done during verification include any required upgrade steps.
return "8092744"
return "9477386"

@property
def cmdline_tools_version_path(self) -> Path:
Expand Down
2 changes: 2 additions & 0 deletions src/briefcase/integrations/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ def __init__(

self.host_arch = self.platform.machine()
self.host_os = self.platform.system()
# Python is 32bit if its pointers can only address with 32 bits or fewer
self.is_32bit_python = self.sys.maxsize <= 2**32

self.app_tools: DefaultDict[AppConfig, ToolCache] = defaultdict(
lambda: ToolCache(
Expand Down
69 changes: 45 additions & 24 deletions src/briefcase/integrations/java.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,22 @@
import subprocess
from pathlib import Path

from briefcase.exceptions import BriefcaseCommandError, MissingToolError
from briefcase.exceptions import (
BriefcaseCommandError,
IncompatibleToolError,
MissingToolError,
)
from briefcase.integrations.base import ManagedTool, ToolCache


class JDK(ManagedTool):
name = "java"
full_name = "Java JDK"

# As of 12 May 2023, 17.0.7+7 is the current OpenJDK
# As of July 2023, 17.0.8+7 is the current OpenJDK
# https://adoptium.net/temurin/releases/
JDK_MAJOR_VER = "17"
JDK_RELEASE = "17.0.7"
JDK_RELEASE = "17.0.8"
JDK_BUILD = "7"
JDK_INSTALL_DIR_NAME = f"java{JDK_MAJOR_VER}"

Expand All @@ -26,28 +30,45 @@ def __init__(self, tools: ToolCache, java_home: Path):

@property
def OpenJDK_download_url(self):
arch = {
"x86_64": "x64", # Linux\macOS x86-64
"aarch64": "aarch64", # Linux arm64
"armv6l": "arm", # Linux arm
"arm64": "aarch64", # macOS arm64
"AMD64": "x64", # Windows x86-64
}.get(self.tools.host_arch)

platform = {
"Darwin": "mac",
"Windows": "windows",
"Linux": "linux",
}.get(self.tools.host_os)

extension = {
"Windows": "zip",
}.get(self.tools.host_os, "tar.gz")
"""The OpenJDK download URL appropriate for the current machine."""
jdk_download_arch = self.tools.host_arch
# use a 32bit JDK if using 32bit Python
if self.tools.is_32bit_python:
if self.tools.host_arch == "aarch64":
jdk_download_arch = "arm"
elif self.tools.host_arch == "x86_64":
jdk_download_arch = "i686"
else:
# don't download 64bit version for 32bit Python
jdk_download_arch = "unknown-arch"
try:
jdk_download_arch = {
"armv7l": "arm", # Linux armv7
"armv8l": "arm", # Linux armv8
"aarch64": "aarch64", # Linux arm64
"arm64": "aarch64", # macOS arm64
"x86_64": "x64", # Linux/macOS x86-64
"AMD64": "x64", # Windows x86-64
}[jdk_download_arch]

platform = {
"Darwin": "mac",
"Windows": "windows",
"Linux": "linux",
}[self.tools.host_os]

extension = {
"Darwin": "tar.gz",
"Linux": "tar.gz",
"Windows": "zip",
}[self.tools.host_os]
except KeyError as e:
raise IncompatibleToolError(tool=self.full_name, env_var="JAVA_HOME") from e

return (
f"https://github.com/adoptium/temurin{self.JDK_MAJOR_VER}-binaries/"
f"releases/download/jdk-{self.JDK_RELEASE}+{self.JDK_BUILD}/"
f"OpenJDK{self.JDK_MAJOR_VER}U-jdk_{arch}_{platform}_hotspot_"
f"OpenJDK{self.JDK_MAJOR_VER}U-jdk_{jdk_download_arch}_{platform}_hotspot_"
f"{self.JDK_RELEASE}_{self.JDK_BUILD}.{extension}"
)

Expand All @@ -64,15 +85,15 @@ def version_from_path(cls, tools: ToolCache, java_path: str | Path) -> str:
:param tools: ToolCache of available tools
:param java_path: File path to a candidate JDK install
:return: JDK release version; e.g. "17.0.7"
:return: JDK release version; e.g. "17.0.8"
"""
output = tools.subprocess.check_output(
[
os.fsdecode(Path(java_path) / "bin" / "javac"),
"-version",
],
)
# javac's output should look like "javac 17.0.7\n"
# javac's output should look like "javac 17.0.8\n"
return output.strip("\n").split(" ")[1]

@classmethod
Expand Down Expand Up @@ -278,7 +299,7 @@ def install(self):

jdk_zip_path.unlink() # Zip file no longer needed once unpacked.

# The tarball will unpack into <briefcase data dir>/tools/jdk-17.0.7+7
# The tarball will unpack into <briefcase data dir>/tools/jdk-17.0.8+7
# (or whatever name matches the current release).
# We turn this into <briefcase data dir>/tools/java so we have a consistent name.
java_unpack_path = (
Expand Down
20 changes: 18 additions & 2 deletions src/briefcase/integrations/linuxdeploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
BriefcaseCommandError,
CorruptToolError,
MissingToolError,
UnsupportedHostError,
)
from briefcase.integrations.base import ManagedTool, Tool, ToolCache

Expand Down Expand Up @@ -48,6 +49,21 @@ def download_url(self) -> str:
def file_path(self) -> Path:
"""The folder on the local filesystem that contains the file_name."""

@classmethod
def arch(cls, host_os: str, host_arch: str):
# always use the x86-64 arch on macOS since Docker
# containers are always run in an x86-64 VM
arch = host_arch if host_os != "Darwin" else "x86_64"
try:
return {
"x86_64": "x86_64",
"i686": "i386",
}[arch]
except KeyError as e:
raise UnsupportedHostError(
f"Linux AppImages cannot be built on {host_arch}."
) from e

def exists(self) -> bool:
return (self.file_path / self.file_name).is_file()

Expand Down Expand Up @@ -208,7 +224,7 @@ class LinuxDeployQtPlugin(LinuxDeployPluginBase, ManagedTool):

@property
def file_name(self) -> str:
return f"linuxdeploy-plugin-qt-{self.tools.host_arch}.AppImage"
return f"linuxdeploy-plugin-qt-{self.arch(self.tools.host_os, self.tools.host_arch)}.AppImage"

@property
def download_url(self) -> str:
Expand Down Expand Up @@ -319,7 +335,7 @@ def file_path(self) -> Path:

@property
def file_name(self) -> str:
return f"linuxdeploy-{self.tools.host_arch}.AppImage"
return f"linuxdeploy-{self.arch(self.tools.host_os, self.tools.host_arch)}.AppImage"

@property
def download_url(self) -> str:
Expand Down
12 changes: 11 additions & 1 deletion src/briefcase/platforms/linux/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,20 @@ def support_package_url(self, support_revision):
System packages don't use a support package; this is defined by
the template, so this method won't be invoked
"""
python_download_arch = self.tools.host_arch
# use a 32bit Python if host Python is 32bit
if self.tools.is_32bit_python:
if self.tools.host_arch == "aarch64":
python_download_arch = "armv7"
elif self.tools.host_arch == "x86_64":
python_download_arch = "i686"
else:
# don't download 64bit version for 32bit Python
python_download_arch = "unknown-arch"
version, datestamp = support_revision.split("+")
return (
"https://github.com/indygreg/python-build-standalone/releases/download/"
f"{datestamp}/cpython-{support_revision}-{self.tools.host_arch}-unknown-linux-gnu-install_only.tar.gz"
f"{datestamp}/cpython-{support_revision}-{python_download_arch}-unknown-linux-gnu-install_only.tar.gz"
)

def vendor_details(self, freedesktop_info):
Expand Down
13 changes: 7 additions & 6 deletions src/briefcase/platforms/linux/appimage.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,20 @@ def project_path(self, app):

def binary_name(self, app):
safe_name = app.formal_name.replace(" ", "_")
return f"{safe_name}-{app.version}-{self.tools.host_arch}.AppImage"
arch = LinuxDeploy.arch(self.tools.host_os, self.tools.host_arch)
return f"{safe_name}-{app.version}-{arch}.AppImage"

def binary_path(self, app):
return self.bundle_path(app) / self.binary_name(app)

def distribution_path(self, app):
return self.dist_path / self.binary_name(app)

def verify_tools(self):
"""Verify LinuxDeploy is available."""
super().verify_tools()
LinuxDeploy.verify(tools=self.tools)

def add_options(self, parser):
super().add_options(parser)
parser.add_argument(
Expand Down Expand Up @@ -213,11 +219,6 @@ class LinuxAppImageOpenCommand(LinuxAppImageMostlyPassiveMixin, DockerOpenComman
class LinuxAppImageBuildCommand(LinuxAppImageMixin, BuildCommand):
description = "Build a Linux AppImage."

def verify_tools(self):
"""Verify the AppImage linuxdeploy tool and plugins exist."""
super().verify_tools()
LinuxDeploy.verify(tools=self.tools)

def build_app(self, app: AppConfig, **kwargs): # pragma: no-cover-if-is-windows
"""Build an application.
Expand Down
Loading

0 comments on commit 5e1b94f

Please sign in to comment.