scripts/install-debugclients.py

Sun, 13 Apr 2025 15:53:14 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sun, 13 Apr 2025 15:53:14 +0200
branch
eric7
changeset 11217
856628e8a303
parent 11148
15e30f0c76a8
permissions
-rw-r--r--

Project
- Removed support for `pyqt5` project type.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Copyright (c) 2016 - 2025 Detlev Offenbach <detlev@die-offenbachs.de>
#
# This is the install script for the eric debug client. It may be used
# to just install the debug clients for remote debugging.
#

"""
Installation script for the eric debug clients.
"""

import argparse
import compileall
import contextlib
import fnmatch
import importlib
import io
import json
import os
import re
import shutil
import subprocess  # secok
import sys
import sysconfig

# Define the globals.
currDir = os.getcwd()
modDir = None
pyModDir = None
distDir = None
installPackage = "eric7"
doCleanup = True
doCompile = True
proxy = None
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

    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
        if sys.platform.startswith(("win", "cygwin")):
            scheme = "nt_user"
        elif sys.platform == "darwin":
            scheme = "osx_framework_user"
        else:
            scheme = "posix_user"
        modDir = sysconfig.get_path("platlib", scheme)
    pyModDir = modDir


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 debug client files.
    """
    global pyModDir, installPackage

    try:
        # Cleanup the install 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)


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 installEricDebugClients():
    """
    Actually perform the installation steps.

    @return result code
    @rtype int
    """
    global distDir, sourceDir, modDir

    # set install prefix, if not None
    targetDir = (
        os.path.normpath(os.path.join(distDir, installPackage))
        if distDir
        else os.path.join(modDir, installPackage)
    )

    try:
        # Install the files
        # copy the various parts of eric debug clients
        copyTree(
            os.path.join(eric7SourceDir, "DebugClients"),
            os.path.join(targetDir, "DebugClients"),
            ["*.py", "*.pyc", "*.pyo", "*.pyw"],
        )

        # copy the top level package file
        shutilCopy(os.path.join(eric7SourceDir, "__init__.py"), targetDir)

        # copy the license file
        shutilCopy(os.path.join(sourceDir, "docs", "LICENSE.txt"), targetDir)

    except OSError as msg:
        sys.stderr.write("\nError: {0}\nTry install with admin rights.\n".format(msg))
        return 7

    return 0


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
    """
    global proxy

    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"):
        args = [
            sys.executable,
            "-m",
            "pip",
            "install",
            "--prefer-binary",
            "--upgrade",
        ]
        if proxy:
            args.append(f"--proxy={proxy}")
        args.append(packageName)
        exitCode = subprocess.run(args).returncode  # secok
        ok = exitCode == 0

    return ok


def isPipOutdated():
    """
    Check, if pip is outdated.

    @return flag indicating an outdated pip
    @rtype bool
    """
    global proxy

    try:
        args = [
            sys.executable,
            "-m",
            "pip",
            "list",
            "--outdated",
            "--format=json",
        ]
        if proxy:
            args.append(f"--proxy={proxy}")
        pipOut = (
            subprocess.run(  # secok
                args,
                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 proxy

    print("Shall 'pip' be updated (recommended)? (Y/n)", end=" ")
    answer = input()  # secok
    if answer in ("", "Y", "y"):
        args = [
            sys.executable,
            "-m",
            "pip",
            "install",
            "--upgrade",
        ]
        if proxy:
            args.append(f"--proxy={proxy}")
        args.append("pip")
        subprocess.run(args)  # secok


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, 9, 0) or sys.version_info >= (3, 14, 0):
        print("Sorry, you must have Python 3.9.0 or higher, but less 3.14.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"),
    }

    # 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 createArgumentParser():
    """
    Function to create an argument parser.

    @return created argument parser object
    @rtype argparse.ArgumentParser
    """
    parser = argparse.ArgumentParser(
        description="Install eric7 debug clients from the source code tree."
    )

    parser.add_argument(
        "-d",
        metavar="dir",
        default=modDir,
        help="directory where eric debug client files will be installed"
        " (default: {0})".format(modDir),
    )
    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(
        "--proxy",
        default=None,
        metavar="url",
        help="HTTP proxy url will be used with pip (default: no proxy used)",
    )

    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, distDir, proxy
    global sourceDir, eric7SourceDir

    if sys.version_info < (3, 9, 0) or sys.version_info >= (4, 0, 0):
        print("Sorry, the eric debugger requires Python 3.9 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
    doCleanup = args.c
    doCompile = args.z
    proxy = args.proxy
    if not sys.platform.startswith(("win", "cygwin")) and args.i:
        distDir = os.path.normpath(args.i)

    # check dependencies
    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 debug clients ...", end="", flush=True)
    res = installEricDebugClients()
    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 = M-801

eric ide

mercurial