--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/scripts/install-server.py Fri Jun 14 10:57:32 2024 +0200 @@ -0,0 +1,659 @@ +#!/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