Fri, 14 Jun 2024 10:57:32 +0200
Added scripts to install or uninstall the eric-ide server from sources.
#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de> # # This is the install script for the eric-ide server. It may be used # to just install the server for remote editing and debugging. # """ Installation script for the eric-ide server. """ import argparse import compileall import contextlib import fnmatch import importlib import io import json import os import re import shutil import subprocess import sys import sysconfig # Define the globals. currDir = os.getcwd() scriptsDir = None modDir = None pyModDir = None distDir = None installPackage = "eric7" doCleanup = True doCompile = True doDepChecks = True sourceDir = "eric" eric7SourceDir = "" def exit(rcode=0): """ Exit the install script. @param rcode result code to report back @type int """ global currDir if sys.platform.startswith("win"): with contextlib.suppress(EOFError): input("Press enter to continue...") # secok os.chdir(currDir) sys.exit(rcode) def initGlobals(): """ Module function to set the values of globals that need more than a simple assignment. """ global modDir, pyModDir, scriptsDir # determine the platform scheme if sys.platform.startswith(("win", "cygwin")): scheme = "nt_user" elif sys.platform == "darwin": scheme = "osx_framework_user" else: scheme = "posix_user" # determine modules directory modDir = sysconfig.get_path("platlib") if not os.access(modDir, os.W_OK): # can't write to the standard path, use the 'user' path instead modDir = sysconfig.get_path("platlib", scheme) pyModDir = modDir # determine the scripts directory scriptsDir = sysconfig.get_path("scripts") if not os.access(scriptsDir, os.W_OK): # can't write to the standard path, use the 'user' path instead scriptsDir = sysconfig.get_path("scripts", scheme) def copyToFile(name, text): """ Copy a string to a file. @param name name of the file @type str @param text contents to copy to the file @type str """ with open(name, "w") as f: f.write(text) def copyTree(src, dst, filters, excludeDirs=None, excludePatterns=None): """ Copy files of a directory tree. @param src name of the source directory @type str @param dst name of the destination directory @type str @param filters list of filter pattern determining the files to be copied @type list of str @param excludeDirs list of (sub)directories to exclude from copying @type list of str @param excludePatterns list of filter pattern determining the files to be skipped @type str """ if excludeDirs is None: excludeDirs = [] if excludePatterns is None: excludePatterns = [] try: names = os.listdir(src) except OSError: # ignore missing directories return for name in names: skipIt = False for excludePattern in excludePatterns: if fnmatch.fnmatch(name, excludePattern): skipIt = True break if not skipIt: srcname = os.path.join(src, name) dstname = os.path.join(dst, name) for fileFilter in filters: if fnmatch.fnmatch(srcname, fileFilter): if not os.path.isdir(dst): os.makedirs(dst) shutil.copy2(srcname, dstname) os.chmod(dstname, 0o644) break else: if os.path.isdir(srcname) and srcname not in excludeDirs: copyTree(srcname, dstname, filters, excludePatterns=excludePatterns) def cleanupSource(dirName): """ Cleanup the sources directory to get rid of leftover files and directories. @param dirName name of the directory to prune @type str """ # step 1: delete the __pycache__ directory and all *.pyc files if os.path.exists(os.path.join(dirName, "__pycache__")): shutil.rmtree(os.path.join(dirName, "__pycache__")) for name in [f for f in os.listdir(dirName) if fnmatch.fnmatch(f, "*.pyc")]: os.remove(os.path.join(dirName, name)) # step 2: descent into subdirectories and delete them if empty for name in os.listdir(dirName): name = os.path.join(dirName, name) if os.path.isdir(name): cleanupSource(name) if len(os.listdir(name)) == 0: os.rmdir(name) def cleanUp(): """ Uninstall the old eric files. """ global installPackage, pyModDir, scriptsDir try: # Cleanup the package directories dirname = os.path.join(pyModDir, installPackage) if os.path.exists(dirname): shutil.rmtree(dirname, ignore_errors=True) except OSError as msg: sys.stderr.write("Error: {0}\nTry install with admin rights.\n".format(msg)) exit(7) # Remove the wrapper scripts rem_wnames = ["eric7_server"] try: for rem_wname in rem_wnames: for rwname in wrapperNames(scriptsDir, rem_wname): if os.path.exists(rwname): os.remove(rwname) except OSError as msg: sys.stderr.write("Error: {0}\nTry install with admin rights.\n".format(msg)) exit(7) def wrapperNames(dname, wfile): """ Create the platform specific names for the wrapper script. @param dname name of the directory to place the wrapper into @type str @param wfile basename (without extension) of the wrapper script @type str @return list containing the names of the wrapper scripts @rtype list of str """ wnames = ( [dname + "\\" + wfile + ".cmd", dname + "\\" + wfile + ".bat"] if sys.platform.startswith(("win", "cygwin")) else [dname + "/" + wfile] ) return wnames def createPyWrapper(pydir, wfile, saveDir): """ Create an executable wrapper for a Python script. @param pydir name of the directory where the Python script will eventually be installed @type str @param wfile basename of the wrapper @type str @param saveDir directory to save the file into @type str @return the platform specific name of the wrapper @rtype str """ # all kinds of Windows systems if sys.platform.startswith(("win", "cygwin")): wname = wfile + ".cmd" wrapper = """@"{0}" "{1}\\{2}.py" %1 %2 %3 %4 %5 %6 %7 %8 %9\n""".format( sys.executable, pydir, wfile ) # Mac OS X elif sys.platform == "darwin": major = sys.version_info.major pyexec = "{0}/bin/python{1}".format(sys.exec_prefix, major) wname = wfile wrapper = ( """#!/bin/sh\n""" """\n""" """exec "{0}" "{1}/{2}.py" "$@"\n""".format(pyexec, pydir, wfile) ) # *nix systems else: wname = wfile wrapper = ( """#!/bin/sh\n""" """\n""" """exec "{0}" "{1}/{2}.py" "$@"\n""".format(sys.executable, pydir, wfile) ) wname = os.path.join(saveDir, wname) copyToFile(wname, wrapper) os.chmod(wname, 0o755) # secok return wname def shutilCopy(src, dst, perm=0o644): """ Wrapper function around shutil.copy() to ensure the permissions. @param src source file name @type str @param dst destination file name or directory name @type str @param perm permissions to be set @type int """ shutil.copy(src, dst) if os.path.isdir(dst): dst = os.path.join(dst, os.path.basename(src)) os.chmod(dst, perm) def pipInstall(packageName, message, force=True): """ Install the given package via pip. @param packageName name of the package to be installed @type str @param message message to be shown to the user @type str @param force flag indicating to perform the installation without asking the user @type bool @return flag indicating a successful installation @rtype bool """ ok = False if force: answer = "y" else: print( "{0}\nShall '{1}' be installed using pip? (Y/n)".format( message, packageName ), end=" ", ) answer = input() # secok if answer in ("", "Y", "y"): exitCode = subprocess.run( # secok [ sys.executable, "-m", "pip", "install", "--prefer-binary", "--upgrade", packageName, ] ).returncode ok = exitCode == 0 return ok def isPipOutdated(): """ Check, if pip is outdated. @return flag indicating an outdated pip @rtype bool """ try: pipOut = ( subprocess.run( # secok [sys.executable, "-m", "pip", "list", "--outdated", "--format=json"], check=True, capture_output=True, text=True, ) .stdout.strip() .splitlines()[0] ) # only the first line contains the JSON data except (OSError, subprocess.CalledProcessError): pipOut = "[]" # default empty list try: jsonList = json.loads(pipOut) except Exception: jsonList = [] for package in jsonList: if isinstance(package, dict) and package["name"] == "pip": print( "'pip' is outdated (installed {0}, available {1})".format( package["version"], package["latest_version"] ) ) return True return False def updatePip(): """ Update the installed pip package. """ global yes2All if yes2All: answer = "y" else: print("Shall 'pip' be updated (recommended)? (Y/n)", end=" ") answer = input() # secok if answer in ("", "Y", "y"): subprocess.run( # secok [sys.executable, "-m", "pip", "install", "--upgrade", "pip"] ) def doDependancyChecks(): """ Perform some dependency checks. """ try: isSudo = os.getuid() == 0 and sys.platform != "darwin" # disregard sudo installs on macOS except AttributeError: isSudo = False print("Checking dependencies") # update pip first even if we don't need to install anything if not isSudo and isPipOutdated(): updatePip() print("\n") # perform dependency checks if sys.version_info < (3, 8, 0) or sys.version_info >= (3, 13, 0): print("Sorry, you must have Python 3.8.0 or higher, but less 3.13.0.") print("Yours is {0}.".format(".".join(str(v) for v in sys.version_info[:3]))) exit(5) requiredModulesList = { # key is pip project name # value is tuple of package name, pip install constraint "coverage": ("coverage", ">=6.5.0"), "EditorConfig": ("editorconfig", ""), } # check required modules print("Required Packages") print("-----------------") requiredMissing = False for requiredPackage in sorted(requiredModulesList): try: importlib.import_module(requiredModulesList[requiredPackage][0]) print("Found", requiredPackage) except ImportError as err: if isSudo: print("Required '{0}' could not be detected.".format(requiredPackage)) requiredMissing = True else: pipInstall( requiredPackage + requiredModulesList[requiredPackage][1], "Required '{0}' could not be detected.{1}".format( requiredPackage, "\nError: {0}".format(err) ), force=True, ) if requiredMissing: print("Some required packages are missing and could not be installed.") print("Install them manually.") print() print("All dependencies ok.") print() def installEricServer(): """ Actually perform the installation steps. @return result code @rtype int """ global distDir, sourceDir, modDir, scriptsDir # set install prefix, if not None targetDir = ( os.path.normpath(os.path.join(distDir, installPackage)) if distDir else os.path.join(modDir, installPackage) ) if not os.path.isdir(targetDir): os.makedirs(targetDir) # Create the platform specific wrapper. tmpScriptsDir = "install_scripts" if not os.path.isdir(tmpScriptsDir): os.mkdir(tmpScriptsDir) wrapper = createPyWrapper(targetDir, "eric7_server", tmpScriptsDir) try: # Install the files # copy the various parts of eric-ide server for package in ("DebugClients", "RemoteServer"): copyTree( os.path.join(eric7SourceDir, package), os.path.join(targetDir, package), ["*.py", "*.pyc", "*.pyo", "*.pyw"], ) # copy the needed parts of SystemUtilities os.makedirs(os.path.join(targetDir, "SystemUtilities")) for module in ("__init__.py", "FileSystemUtilities.py", "OSUtilities.py"): shutilCopy( os.path.join(eric7SourceDir, "SystemUtilities", module), os.path.join(targetDir, "SystemUtilities"), ) # copy the top level files for module in ("__init__.py", "__version__.py", "eric7_server.py"): shutilCopy(os.path.join(eric7SourceDir, module), targetDir) # copy the license and README files for infoFile in ("LICENSE.txt", "README-server.md"): shutilCopy(os.path.join(sourceDir, "docs", infoFile), targetDir) # copy the wrapper shutilCopy(wrapper, scriptsDir, perm=0o755) shutil.rmtree(tmpScriptsDir) except OSError as msg: sys.stderr.write("\nError: {0}\nTry install with admin rights.\n".format(msg)) return 7 return 0 def createArgumentParser(): """ Function to create an argument parser. @return created argument parser object @rtype argparse.ArgumentParser """ parser = argparse.ArgumentParser( description="Install eric-ide server from the source code tree." ) parser.add_argument( "-d", metavar="dir", default=modDir, help="directory where eric-ide server files will be installed" " (default: {0})".format(modDir), ) parser.add_argument( "-b", metavar="dir", default=scriptsDir, help="directory where the binaries will be installed (default: {0})".format( scriptsDir ), ) if not sys.platform.startswith(("win", "cygwin")): parser.add_argument( "-i", metavar="dir", default=distDir, help="temporary install prefix (default: {0})".format(distDir), ) parser.add_argument( "-c", action="store_false", help="don't cleanup old installation first", ) parser.add_argument( "-z", action="store_false", help="don't compile the installed Python files", ) parser.add_argument( "-x", action="store_false", help="don't perform dependency checks (use on your own risk)", ) return parser def main(argv): """ The main function of the script. @param argv the list of command line arguments @type list of str """ global modDir, doCleanup, doCompile, doDepChecks, distDir, scriptsDir global sourceDir, eric7SourceDir if sys.version_info < (3, 8, 0) or sys.version_info >= (4, 0, 0): print("Sorry, the eric debugger requires Python 3.8 or better for running.") exit(5) if os.path.dirname(argv[0]): os.chdir(os.path.dirname(argv[0])) initGlobals() parser = createArgumentParser() args = parser.parse_args() modDir = args.d scriptsDir = args.b doDepChecks = args.x doCleanup = args.c doCompile = args.z if not sys.platform.startswith(("win", "cygwin")) and args.i: distDir = os.path.normpath(args.i) # check dependencies if doDepChecks: doDependancyChecks() installFromSource = not os.path.isdir(sourceDir) if installFromSource: sourceDir = os.path.abspath("..") eric7SourceDir = ( os.path.join(sourceDir, "eric7") if os.path.exists(os.path.join(sourceDir, "eric7")) else os.path.join(sourceDir, "src", "eric7") ) # cleanup source if installing from source if installFromSource: print("Cleaning up source ...", end="", flush=True) cleanupSource(os.path.join(eric7SourceDir, "DebugClients")) print(" Done") # cleanup old installation try: if doCleanup: print("Cleaning up old installation ...", end="", flush=True) if distDir: shutil.rmtree(distDir, ignore_errors=True) else: cleanUp() print(" Done") except OSError as msg: sys.stderr.write("Error: {0}\nTry install as root.\n".format(msg)) exit(7) if doCompile: print("Compiling source files ...", end="", flush=True) skipRe = re.compile(r"DebugClients[\\/]Python[\\/]") sys.stdout = io.StringIO() if distDir: compileall.compile_dir( os.path.join(eric7SourceDir, "DebugClients"), ddir=os.path.join(distDir, modDir, installPackage), rx=skipRe, quiet=True, ) else: compileall.compile_dir( os.path.join(eric7SourceDir, "DebugClients"), ddir=os.path.join(modDir, installPackage), rx=skipRe, quiet=True, ) sys.stdout = sys.__stdout__ print(" Done") print("Installing eric-ide server ...", end="", flush=True) res = installEricServer() print(" Done") print("Installation complete.") print() exit(res) if __name__ == "__main__": try: main(sys.argv) except SystemExit: raise except Exception: print( """An internal error occured. Please report all the output""" """ of the program,\nincluding the following traceback, to""" """ eric-bugs@eric-ide.python-projects.org.\n""" ) raise # # eflag: noqa = M801