#!/usr/bin/env python3 import sys import platform import os import subprocess import shutil from pythonforandroid.logger import info, warning, error class Prerequisite(object): name = "Default" homebrew_formula_name = "" mandatory = dict(linux=False, darwin=False) installer_is_supported = dict(linux=False, darwin=False) def is_valid(self): if self.checker(): info(f"Prerequisite {self.name} is met") return (True, "") elif not self.mandatory[sys.platform]: warning( f"Prerequisite {self.name} is not met, but is marked as non-mandatory" ) else: error(f"Prerequisite {self.name} is not met") def checker(self): if sys.platform == "darwin": return self.darwin_checker() elif sys.platform == "linux": return self.linux_checker() else: raise Exception("Unsupported platform") def ask_to_install(self): if ( os.environ.get("PYTHONFORANDROID_PREREQUISITES_INSTALL_INTERACTIVE", "1") == "1" ): res = input( f"Do you want automatically install prerequisite {self.name}? [y/N] " ) if res.lower() == "y": return True else: return False else: info( "Session is not interactive (usually this happens during a CI run), so let's consider it as a YES" ) return True def install(self): info(f"python-for-android can automatically install prerequisite: {self.name}") if self.ask_to_install(): if sys.platform == "darwin": self.darwin_installer() elif sys.platform == "linux": self.linux_installer() else: raise Exception("Unsupported platform") else: info( f"Skipping installation of prerequisite {self.name} as per user request" ) def show_helper(self): if sys.platform == "darwin": self.darwin_helper() elif sys.platform == "linux": self.linux_helper() else: raise Exception("Unsupported platform") def install_is_supported(self): return self.installer_is_supported[sys.platform] def linux_checker(self): raise Exception(f"Unsupported prerequisite check on linux for {self.name}") def darwin_checker(self): raise Exception(f"Unsupported prerequisite check on macOS for {self.name}") def linux_installer(self): raise Exception(f"Unsupported prerequisite installer on linux for {self.name}") def darwin_installer(self): raise Exception(f"Unsupported prerequisite installer on macOS for {self.name}") def darwin_helper(self): info(f"No helper available for prerequisite: {self.name} on macOS") def linux_helper(self): info(f"No helper available for prerequisite: {self.name} on linux") def _darwin_get_brew_formula_location_prefix(self, formula, installed=False): opts = ["--installed"] if installed else [] p = subprocess.Popen( ["brew", "--prefix", formula, *opts], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) _stdout_res, _stderr_res = p.communicate() if p.returncode != 0: error(_stderr_res.decode("utf-8").strip()) return None else: return _stdout_res.decode("utf-8").strip() def darwin_pkg_config_location(self): warning( f"pkg-config location is not supported on macOS for prerequisite: {self.name}" ) return "" def linux_pkg_config_location(self): warning( f"pkg-config location is not supported on linux for prerequisite: {self.name}" ) return "" @property def pkg_config_location(self): if sys.platform == "darwin": return self.darwin_pkg_config_location() elif sys.platform == "linux": return self.linux_pkg_config_location() class HomebrewPrerequisite(Prerequisite): name = "homebrew" mandatory = dict(linux=False, darwin=True) installer_is_supported = dict(linux=False, darwin=False) def darwin_checker(self): return shutil.which("brew") is not None def darwin_helper(self): info( "Installer for homebrew is not yet supported on macOS," "the nice news is that the installation process is easy!" "See: https://brew.sh for further instructions." ) class JDKPrerequisite(Prerequisite): name = "JDK" mandatory = dict(linux=False, darwin=True) installer_is_supported = dict(linux=False, darwin=True) min_supported_version = 11 def darwin_checker(self): if "JAVA_HOME" in os.environ: info("Found JAVA_HOME environment variable, using it") jdk_path = os.environ["JAVA_HOME"] else: jdk_path = self._darwin_get_libexec_jdk_path(version=None) return self._darwin_jdk_is_supported(jdk_path) def _darwin_get_libexec_jdk_path(self, version=None): version_args = [] if version is not None: version_args = ["-v", version] return ( subprocess.run( ["/usr/libexec/java_home", *version_args], stdout=subprocess.PIPE, ) .stdout.strip() .decode() ) def _darwin_jdk_is_supported(self, jdk_path): if not jdk_path: return False javac_bin = os.path.join(jdk_path, "bin", "javac") if not os.path.exists(javac_bin): return False p = subprocess.Popen( [javac_bin, "-version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) _stdout_res, _stderr_res = p.communicate() if p.returncode != 0: error("Failed to run javac to check JDK version") return False if not _stdout_res: _stdout_res = _stderr_res res = _stdout_res.strip().decode() major_version = int(res.split(" ")[-1].split(".")[0]) if major_version >= self.min_supported_version: info(f"Found a valid JDK at {jdk_path}") return True else: error(f"JDK {self.min_supported_version} or higher is required") return False def darwin_helper(self): info( "python-for-android requires a JDK 11 or higher to be installed on macOS," "but seems like you don't have one installed." ) info( "If you think that a valid JDK is already installed, please verify that " "you have a JDK 11 or higher installed and that `/usr/libexec/java_home` " "shows the correct path." ) info( "If you have multiple JDK installations, please make sure that you have " "`JAVA_HOME` environment variable set to the correct JDK installation." ) def darwin_installer(self): info( "Looking for a JDK 11 or higher installation which is not the default one ..." ) jdk_path = self._darwin_get_libexec_jdk_path(version="11+") if not self._darwin_jdk_is_supported(jdk_path): info("We're unlucky, there's no JDK 11 or higher installation available") base_url = "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.2%2B8/" if platform.machine() == "arm64": filename = "OpenJDK17U-jdk_aarch64_mac_hotspot_17.0.2_8.tar.gz" else: filename = "OpenJDK17U-jdk_x64_mac_hotspot_17.0.2_8.tar.gz" info(f"Downloading {filename} from {base_url}") subprocess.check_output( [ "curl", "-L", f"{base_url}{filename}", "-o", f"/tmp/{filename}", ] ) user_library_java_path = os.path.expanduser( "~/Library/Java/JavaVirtualMachines" ) info(f"Extracting {filename} to {user_library_java_path}") subprocess.check_output( [ "mkdir", "-p", user_library_java_path, ], ) subprocess.check_output( ["tar", "xzf", f"/tmp/{filename}", "-C", user_library_java_path], ) jdk_path = self._darwin_get_libexec_jdk_path(version="17.0.2+8") info(f"Setting JAVA_HOME to {jdk_path}") os.environ["JAVA_HOME"] = jdk_path class OpenSSLPrerequisite(Prerequisite): name = "openssl" homebrew_formula_name = "openssl@1.1" mandatory = dict(linux=False, darwin=True) installer_is_supported = dict(linux=False, darwin=True) def darwin_checker(self): return ( self._darwin_get_brew_formula_location_prefix( self.homebrew_formula_name, installed=True ) is not None ) def darwin_pkg_config_location(self): return os.path.join( self._darwin_get_brew_formula_location_prefix(self.homebrew_formula_name), "lib/pkgconfig", ) def darwin_installer(self): info("Installing OpenSSL ...") subprocess.check_output(["brew", "install", self.homebrew_formula_name]) class AutoconfPrerequisite(Prerequisite): name = "autoconf" mandatory = dict(linux=False, darwin=True) installer_is_supported = dict(linux=False, darwin=True) def darwin_checker(self): return ( self._darwin_get_brew_formula_location_prefix("autoconf", installed=True) is not None ) def darwin_installer(self): info("Installing Autoconf ...") subprocess.check_output(["brew", "install", "autoconf"]) class AutomakePrerequisite(Prerequisite): name = "automake" mandatory = dict(linux=False, darwin=True) installer_is_supported = dict(linux=False, darwin=True) def darwin_checker(self): return ( self._darwin_get_brew_formula_location_prefix("automake", installed=True) is not None ) def darwin_installer(self): info("Installing Automake ...") subprocess.check_output(["brew", "install", "automake"]) class LibtoolPrerequisite(Prerequisite): name = "libtool" mandatory = dict(linux=False, darwin=True) installer_is_supported = dict(linux=False, darwin=True) def darwin_checker(self): return ( self._darwin_get_brew_formula_location_prefix("libtool", installed=True) is not None ) def darwin_installer(self): info("Installing Libtool ...") subprocess.check_output(["brew", "install", "libtool"]) class PkgConfigPrerequisite(Prerequisite): name = "pkg-config" mandatory = dict(linux=False, darwin=True) installer_is_supported = dict(linux=False, darwin=True) def darwin_checker(self): return ( self._darwin_get_brew_formula_location_prefix("pkg-config", installed=True) is not None ) def darwin_installer(self): info("Installing Pkg-Config ...") subprocess.check_output(["brew", "install", "pkg-config"]) class CmakePrerequisite(Prerequisite): name = "cmake" mandatory = dict(linux=False, darwin=True) installer_is_supported = dict(linux=False, darwin=True) def darwin_checker(self): return ( self._darwin_get_brew_formula_location_prefix("cmake", installed=True) is not None ) def darwin_installer(self): info("Installing cmake ...") subprocess.check_output(["brew", "install", "cmake"]) def get_required_prerequisites(platform="linux"): DEFAULT_PREREQUISITES = dict( darwin=[ HomebrewPrerequisite(), AutoconfPrerequisite(), AutomakePrerequisite(), LibtoolPrerequisite(), PkgConfigPrerequisite(), CmakePrerequisite(), OpenSSLPrerequisite(), JDKPrerequisite(), ], linux=[], all_platforms=[], ) return DEFAULT_PREREQUISITES["all_platforms"] + DEFAULT_PREREQUISITES[platform] def check_and_install_default_prerequisites(): prerequisites_not_met = [] warning( "prerequisites.py is experimental and does not support all prerequisites yet." ) warning("Please report any issues to the python-for-android issue tracker.") # Phase 1: Check if all prerequisites are met and add the ones # which are not to `prerequisites_not_met` for prerequisite in get_required_prerequisites(sys.platform): if not prerequisite.is_valid(): prerequisites_not_met.append(prerequisite) # Phase 2: Setup/Install all prerequisites that are not met # (where possible), otherwise show an helper. for prerequisite in prerequisites_not_met: prerequisite.show_helper() if prerequisite.install_is_supported(): prerequisite.install() if __name__ == "__main__": check_and_install_default_prerequisites()