From 30ee4a800dc220f5b2c7542c413be759f12b0412 Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Thu, 13 Jul 2023 20:04:15 -0400 Subject: [PATCH] Machine architecture checks - Prevent running on 32bit Windows or 32bit Python on 64bit Windows - Support i386 for AppImage - Support building Linux System packages on i386 - Support installing JDK for armv7/8 - Let users know when Android SDK must be manually installed --- src/briefcase/exceptions.py | 12 +++++ src/briefcase/integrations/android_sdk.py | 34 ++++++++---- src/briefcase/integrations/java.py | 57 ++++++++++++--------- src/briefcase/integrations/linuxdeploy.py | 17 +++++- src/briefcase/platforms/linux/appimage.py | 3 +- src/briefcase/platforms/linux/system.py | 1 + src/briefcase/platforms/windows/__init__.py | 25 +++++++-- 7 files changed, 110 insertions(+), 39 deletions(-) diff --git a/src/briefcase/exceptions.py b/src/briefcase/exceptions.py index 005f78096..f5f684e98 100644 --- a/src/briefcase/exceptions.py +++ b/src/briefcase/exceptions.py @@ -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 diff --git a/src/briefcase/integrations/android_sdk.py b/src/briefcase/integrations/android_sdk.py index 59bf8e776..08f31f059 100644 --- a/src/briefcase/integrations/android_sdk.py +++ b/src/briefcase/integrations/android_sdk.py @@ -15,6 +15,7 @@ from briefcase.console import InputDisabled, select_option from briefcase.exceptions import ( BriefcaseCommandError, + IncompatibleToolError, InvalidDeviceError, MissingToolError, ) @@ -57,15 +58,28 @@ 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": {"x86_64": "mac", "arm64": "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: @@ -74,11 +88,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: diff --git a/src/briefcase/integrations/java.py b/src/briefcase/integrations/java.py index 4f041349e..483905b48 100644 --- a/src/briefcase/integrations/java.py +++ b/src/briefcase/integrations/java.py @@ -5,7 +5,11 @@ 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 @@ -13,10 +17,10 @@ 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}" @@ -26,23 +30,30 @@ 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.""" + try: + 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 + }[self.tools.host_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/" @@ -64,7 +75,7 @@ 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( [ @@ -72,7 +83,7 @@ def version_from_path(cls, tools: ToolCache, java_path: str | Path) -> str: "-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 @@ -279,7 +290,7 @@ def install(self): jdk_zip_path.unlink() # Zip file no longer needed once unpacked. - # The tarball will unpack into /tools/jdk-17.0.7+7 + # The tarball will unpack into /tools/jdk-17.0.8+7 # (or whatever name matches the current release). # We turn this into /tools/java so we have a consistent name. java_unpack_path = ( diff --git a/src/briefcase/integrations/linuxdeploy.py b/src/briefcase/integrations/linuxdeploy.py index 9d5d9f5f5..7b1e8dc01 100644 --- a/src/briefcase/integrations/linuxdeploy.py +++ b/src/briefcase/integrations/linuxdeploy.py @@ -11,6 +11,7 @@ BriefcaseCommandError, CorruptToolError, MissingToolError, + UnsupportedHostError, ) from briefcase.integrations.base import ManagedTool, Tool, ToolCache @@ -48,6 +49,18 @@ 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() @@ -208,7 +221,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: @@ -319,7 +332,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: diff --git a/src/briefcase/platforms/linux/appimage.py b/src/briefcase/platforms/linux/appimage.py index f9916a5f4..35600e428 100644 --- a/src/briefcase/platforms/linux/appimage.py +++ b/src/briefcase/platforms/linux/appimage.py @@ -44,7 +44,8 @@ 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) diff --git a/src/briefcase/platforms/linux/system.py b/src/briefcase/platforms/linux/system.py index 829f4dabd..2adb63cc6 100644 --- a/src/briefcase/platforms/linux/system.py +++ b/src/briefcase/platforms/linux/system.py @@ -61,6 +61,7 @@ def linux_arch(self): "x86_64": "amd64", "aarch64": "arm64", "armv6l": "armhf", + "i686": "i386", }.get(self.tools.host_arch, self.tools.host_arch) def build_path(self, app): diff --git a/src/briefcase/platforms/windows/__init__.py b/src/briefcase/platforms/windows/__init__.py index fbd2c18f0..7d77b9db9 100644 --- a/src/briefcase/platforms/windows/__init__.py +++ b/src/briefcase/platforms/windows/__init__.py @@ -8,7 +8,7 @@ from briefcase.commands import CreateCommand, PackageCommand, RunCommand from briefcase.config import AppConfig, parsed_version -from briefcase.exceptions import BriefcaseCommandError +from briefcase.exceptions import BriefcaseCommandError, UnsupportedHostError from briefcase.integrations.windows_sdk import WindowsSDK from briefcase.integrations.wix import WiX @@ -27,6 +27,24 @@ def distribution_path(self, app): suffix = "zip" if app.packaging_format == "zip" else "msi" return self.dist_path / f"{app.formal_name}-{app.version}.{suffix}" + def verify_host(self): + super().verify_host() + # the stub app only supports x86-64 right now + if self.tools.host_arch != "AMD64": + raise UnsupportedHostError( + f"Windows applications cannot be built on an {self.tools.host_arch} machine." + ) + # Python is 32bit if its pointers can only address with 32 bits or fewer. + # 64bit Python is required to ensure 64bit wheels are installed/created for the app. + if self.tools.sys.maxsize <= 2**32: + raise UnsupportedHostError( + """\ +Windows applications cannot be built on a 32bit version of Python. + +Install a 64bit version of Python and run Briefcase again. +""" + ) + class WindowsCreateCommand(CreateCommand): def support_package_filename(self, support_revision): @@ -34,8 +52,9 @@ def support_package_filename(self, support_revision): def support_package_url(self, support_revision): return ( - f"https://www.python.org/ftp/python/{self.python_version_tag}.{support_revision}/" - + self.support_package_filename(support_revision) + f"https://www.python.org/ftp/python/" + f"{self.python_version_tag}.{support_revision}/" + f"{self.support_package_filename(support_revision)}" ) def output_format_template_context(self, app: AppConfig):