scripts/install-server.py

branch
server
changeset 10781
0e3d6e22efaf
child 10801
5859861e7a1f
--- /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

eric ide

mercurial