diff --git a/Scripts/InstallFEX.py b/Scripts/InstallFEX.py new file mode 100755 index 000000000..0cf361b17 --- /dev/null +++ b/Scripts/InstallFEX.py @@ -0,0 +1,399 @@ +#!/usr/bin/python3 +import os +import subprocess +import sys +import tempfile + +_Arch = None +def GetArch(): + global _Arch + + if _Arch == None: + _Arch = subprocess.check_output(['uname', '-m']).decode("utf-8").strip() + return _Arch + +_Distro = None +def GetDistro(): + global _Distro + + # Query files in order + # /etc/lsb-release + # /etc/os-release + + if _Distro == None: + if os.path.exists("/etc/lsb-release"): + File = open("/etc/lsb-release", "r") + Lines = File.readlines() + File.close() + + Found = 0 + Distro = "" + Version = "" + for Line in Lines: + Key, Val = Line.split("=", 1) + + if Key == "DISTRIB_ID": + Distro = Val.strip().lower() + Found+=1 + if Key == "DISTRIB_RELEASE": + Version = Val.strip() + Found+=1 + + if Found == 2: + _Distro = [Distro, Version] + return _Distro + + if os.path.exists("/etc/os-release"): + File = open("/etc/os-release", "r") + Lines = File.readlines() + File.close() + + Found = 0 + Distro = "" + Version = "" + for Line in Lines: + Key, Val = Line.split("=", 1) + + if Key == "ID": + Distro = Val.strip() + Found+=1 + if Key == "VERSION_ID": + # Strip the double quotes from the version id + Version = Val.strip()[1:-1] + Found+=1 + + if Found == 2: + _Distro = [Distro, Version] + return _Distro + + # Unknown + _Distro = ["Unknown", "0.0"] + + return _Distro + +def IsSupportedArch(): + Arch = GetArch() + return Arch == "aarch64" + +def IsSupportedDistro(): + Distro = GetDistro() + + # We only support Ubuntu + if Distro[0] == "ubuntu": + # We only support what is available in ppa:fex-emu/fex + return Distro[1] == "20.04" or \ + Distro[1] == "21.04" or \ + Distro[1] == "21.10" or \ + Distro[1] == "22.04" + + return False + +_ArchVersion = None +def ListContainsRequired(Features, RequiredFeatures): + for Req in RequiredFeatures: + if not Req in Features: + return False + return True + +def GetCPUFeaturesVersion(): + global _ArchVersion + + # Also LOR but kernel doesn't expose this + v8_1Mandatory = ["atomics", "asimdrdm", "crc32"] + v8_2Mandatory = v8_1Mandatory + ["dcpop"] + v8_3Mandatory = v8_2Mandatory + ["fcma", "jscvt", "lrcpc", "paca", "pacg"] + v8_4Mandatory = v8_3Mandatory + ["asimddp", "flagm", "ilrcpc", "uscat"] + + # fphp asimdhp asimddp + + if _ArchVersion == None: + File = open("/proc/cpuinfo", "r") + Lines = File.readlines() + File.close() + + # Minimum spec is ARMv8.0 + _ArchVersion = "8.0" + for Line in Lines: + if "Features" in Line: + Features = Line.split(":")[1].strip().split(" ") + + # We don't care beyond 8.4 right now + if ListContainsRequired(Features, v8_4Mandatory): + _ArchVersion = "8.4" + elif ListContainsRequired(Features, v8_3Mandatory): + _ArchVersion = "8.3" + elif ListContainsRequired(Features, v8_2Mandatory): + _ArchVersion = "8.2" + elif ListContainsRequired(Features, v8_1Mandatory): + _ArchVersion = "8.1" + break; + + return _ArchVersion + +_PPAInstalled = None +FEXPPA = "http://ppa.launchpad.net/fex-emu/fex/ubuntu" + +def GetPPAStatus(): + global _PPAInstalled + + if _PPAInstalled == None: + _PPAInstalled = False + + CacheResults = subprocess.check_output(['apt-cache', 'policy']).decode("utf-8") + + for Line in CacheResults.split("\n"): + if "http" in Line: + Line = Line.strip() + LineSplit = Line.split(" ") + + # 'status' 'URL' 'series' 'arch' 'type' + if LineSplit[1] == FEXPPA: + _PPAInstalled = True + break + + return _PPAInstalled + +def InstallPPA(): + print ("Installing PPA: ppa:fex-emu/fex") + print ("This bit will ask for your password") + + DidInstall = False + try: + CmdResult = subprocess.call(["sudo", "add-apt-repository", "-y", "ppa:fex-emu/fex"]) + DidInstall = CmdResult == 0 + except KeyboardInterrupt: + DidInstall = False + pass + + if DidInstall: + print("PPA installed") + else: + print("PPA failed to install") + + return DidInstall + +ARMVersionToPackage = { + "8.0": "fex-emu-armv8.0", + "8.1": "fex-emu-armv8.0", + "8.2": "fex-emu-armv8.2", + "8.3": "fex-emu-armv8.2", + "8.4": "fex-emu-armv8.4", +} + +def GetPackagesToInstall(): + return [ + ARMVersionToPackage[GetCPUFeaturesVersion()], + "fex-emu-binfmt32", + "fex-emu-binfmt64", + ] + +def UpdatePPA(): + print ("Updating apt sources") + print ("This bit will ask for your password") + + DidUpdate = False + try: + CmdResult = subprocess.call(["sudo", "apt-get", "update"]) + DidUpdate = CmdResult == 0 + except KeyboardInterrupt: + DidUpdate = False + pass + + if DidUpdate: + print("PPA installed") + else: + print("PPA failed to install") + + return DidUpdate + +def CheckAndInstallPackageUpdates(): + PackagesToInstall = GetPackagesToInstall() + for Package in PackagesToInstall[:]: + UpgradableStatus = subprocess.check_output(["apt", "list", "--upgradable", Package]).decode("utf-8") + Found = False + for Line in UpgradableStatus.split("\n"): + # If the package exists to be upgraded then it will appear in this list + # We need to check multiple lines + # $ apt list --upgradable + # With upgrade available + # Listing... Done + # / [upgradable from: ] + # Without upgrade available + # Listing... Done + # + if Package in Line and "upgradable" in Line: + Found = True + + if Found == False: + PackagesToInstall.remove(Package) + + if len(PackagesToInstall) > 0: + print ("Found updates for packages: {}".format(PackagesToInstall)) + print ("This bit may ask for your password") + + DidInstall = False + try: + CmdResult = subprocess.call(["sudo", "apt-get", "-y", "install"] + PackagesToInstall) + DidInstall = CmdResult == 0 + except KeyboardInterrupt: + print ("Keyboard interrupt") + DidInstall = False + pass + + if DidInstall: + print("Packages updated") + else: + print("Packages failed to update") + + return DidInstall + + return True + +def CheckPackageInstallStatus(): + PackagesToInstall = GetPackagesToInstall() + for Package in PackagesToInstall[:]: + CmdResult = subprocess.call(["dpkg", "-s", Package], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + if CmdResult == 0: + PackagesToInstall.remove(Package) + + return PackagesToInstall + +def InstallPackages(Packages): + print("Installing packages: {}".format(Packages)) + + DidInstall = False + try: + CmdResult = subprocess.call(["sudo", "apt-get", "-y", "install"] + Packages) + DidInstall = CmdResult == 0 + except KeyboardInterrupt: + print ("Keyboard interrupt") + DidInstall = False + pass + + if DidInstall: + print("Packages installed") + else: + print("Packages failed to install") + + return DidInstall + +_RootFSPath = None +def GetRootFSPath(): + global _RootFSPath + + if _RootFSPath == None: + # Follows the same logic as FEXCore::Config::GetDataDirectory() + HomeDir = os.getenv("HOME") + if HomeDir == None: + HomeDir = os.getenv("PWD") + if HomeDir == None: + HomeDir = "." + + Path = HomeDir + DataXDG = os.getenv("XDG_DATA_HOME") + if DataXDG != None: + Path = DataXDG + + Path = Path + "/.fex-emu" + + DataOverride = os.getenv("FEX_APP_DATA_LOCATION") + + if DataOverride != None: + Path = DataOverride + + _RootFSPath = Path + "/RootFS/" + + return _RootFSPath + +def CheckRootFSInstallStatus(): + # Matches what is available on https://rootfs.fex-emu.org/file/fex-rootfs/RootFS_links.txt + UbuntuVersionToRootFS = { + "20.04": "Ubuntu_21_04.sqsh", + "21.04": "Ubuntu_21_04.sqsh", + "21.10": "Ubuntu_21_10.sqsh", + "22.04": "Ubuntu_21_10.sqsh", + } + + return os.path.exists(GetRootFSPath() + UbuntuVersionToRootFS[GetDistro()[1]]) + +def TryInstallRootFS(): + DidInstall = False + try: + CmdResult = subprocess.call(["FEXRootFSFetcher"]) + DidInstall = CmdResult == 0 + except KeyboardInterrupt: + print ("Keyboard interrupt") + DidInstall = False + pass + return DidInstall + +def TryBasicProgramExecution(): + return subprocess.call(["FEXInterpreter", "/usr/bin/uname", "-a"]) == 0 + +def ExitWithStatus(Status): + # Remove the cached credentials + subprocess.call(["sudo", "-K"]) + sys.exit(Status) + +def main(): + # Only run on supported arch + if not IsSupportedArch(): + print ( "{} is not a supported architecture".format(GetArch())) + ExitWithStatus(-1) + + if not IsSupportedDistro(): + Distro = GetDistro() + print ( "'{} {}' is not a supported distro".format(Distro[0], Distro[1])) + ExitWithStatus(-1) + + if GetDistro()[0] == "ubuntu": + print ("Getting PPA status: {}".format(("NotInstalled", "Installed")[GetPPAStatus()])) + + if GetPPAStatus(): + if not UpdatePPA(): + print ("apt sources failed to update. Not continuing") + ExitWithStatus(-1) + if not CheckAndInstallPackageUpdates(): + print ("apt packages failed to update. Not continuing") + ExitWithStatus(-1) + else: + if not InstallPPA(): + print ("PPA failed to install. Not continuing") + ExitWithStatus(-1) + + Packages = CheckPackageInstallStatus() + if len(Packages) > 0: + if not InstallPackages(Packages): + print ("Failed to install packages. Not continuing") + ExitWithStatus(-1) + + if not CheckRootFSInstallStatus(): + print ("RootFS not found. Running FEXRootFSFetcher to get rootfs") + if not TryInstallRootFS(): + print ("Failed to install RootFS. Not continuing") + ExitWithStatus(-1) + + print ("FEX is now installed. Trying basic program run") + if not TryBasicProgramExecution(): + print ("FEXInterpreter failed to run. Not continuing") + ExitWithStatus(-1) + + print ("") + print ("===================================================") + print ("FEX test run executed. You should be set to run FEX") + print ("===================================================") + print ("Usage examples:") + print ("# steam is a bash script. Wrap with FEXBash") + print ("\tFEXBash steam") + print ("# Full path execution execution will wrap the application if it exists in the rootfs") + print ("\tFEXInterpreter /usr/bin/uname") + print ("# Freestanding x86/x86-64 programs can be executed directly. binfmt_misc will redirect to FEX") + print ("\t$HOME/PetalCrashOnline.AppImage") + print ("# If you need a terminal that emulates everything.") + print ("# Run FEXBash without arguments. Double check uname to see if running under FEX") + print ("\tFEXBash") + + ExitWithStatus(0) + +if __name__ == "__main__": + sys.exit(main())