setup.py
author Matt Harbison <matt_harbison@yahoo.com>
Sun, 20 Aug 2023 16:19:41 -0400
changeset 50877 b3ac579bde41
parent 50803 609a3b8058c3
child 50936 6408777c8fa4
permissions -rw-r--r--
gpg: migrate `opts` to native kwargs

#
# This is the mercurial setup script.
#
# 'python setup.py install', or
# 'python setup.py --help' for more options
import os

# Mercurial can't work on 3.6.0 or 3.6.1 due to a bug in % formatting
# in bytestrings.
supportedpy = ','.join(
    [
        '>=3.6.2',
    ]
)

import sys, platform
import sysconfig


def sysstr(s):
    return s.decode('latin-1')


def eprint(*args, **kwargs):
    kwargs['file'] = sys.stderr
    print(*args, **kwargs)


import ssl

# ssl.HAS_TLSv1* are preferred to check support but they were added in Python
# 3.7. Prior to CPython commit 6e8cda91d92da72800d891b2fc2073ecbc134d98
# (backported to the 3.7 branch), ssl.PROTOCOL_TLSv1_1 / ssl.PROTOCOL_TLSv1_2
# were defined only if compiled against a OpenSSL version with TLS 1.1 / 1.2
# support. At the mentioned commit, they were unconditionally defined.
_notset = object()
has_tlsv1_1 = getattr(ssl, 'HAS_TLSv1_1', _notset)
if has_tlsv1_1 is _notset:
    has_tlsv1_1 = getattr(ssl, 'PROTOCOL_TLSv1_1', _notset) is not _notset
has_tlsv1_2 = getattr(ssl, 'HAS_TLSv1_2', _notset)
if has_tlsv1_2 is _notset:
    has_tlsv1_2 = getattr(ssl, 'PROTOCOL_TLSv1_2', _notset) is not _notset
if not (has_tlsv1_1 or has_tlsv1_2):
    error = """
The `ssl` module does not advertise support for TLS 1.1 or TLS 1.2.
Please make sure that your Python installation was compiled against an OpenSSL
version enabling these features (likely this requires the OpenSSL version to
be at least 1.0.1).
"""
    print(error, file=sys.stderr)
    sys.exit(1)

DYLIB_SUFFIX = sysconfig.get_config_vars()['EXT_SUFFIX']

# Solaris Python packaging brain damage
try:
    import hashlib

    sha = hashlib.sha1()
except ImportError:
    try:
        import sha

        sha.sha  # silence unused import warning
    except ImportError:
        raise SystemExit(
            "Couldn't import standard hashlib (incomplete Python install)."
        )

try:
    import zlib

    zlib.compressobj  # silence unused import warning
except ImportError:
    raise SystemExit(
        "Couldn't import standard zlib (incomplete Python install)."
    )

# The base IronPython distribution (as of 2.7.1) doesn't support bz2
isironpython = False
try:
    isironpython = (
        platform.python_implementation().lower().find("ironpython") != -1
    )
except AttributeError:
    pass

if isironpython:
    sys.stderr.write("warning: IronPython detected (no bz2 support)\n")
else:
    try:
        import bz2

        bz2.BZ2Compressor  # silence unused import warning
    except ImportError:
        raise SystemExit(
            "Couldn't import standard bz2 (incomplete Python install)."
        )

ispypy = "PyPy" in sys.version

import ctypes
import stat, subprocess, time
import re
import shutil
import tempfile

# We have issues with setuptools on some platforms and builders. Until
# those are resolved, setuptools is opt-in except for platforms where
# we don't have issues.
issetuptools = os.name == 'nt' or 'FORCE_SETUPTOOLS' in os.environ
if issetuptools:
    from setuptools import setup
else:
    try:
        from distutils.core import setup
    except ModuleNotFoundError:
        from setuptools import setup
from distutils.ccompiler import new_compiler
from distutils.core import Command, Extension
from distutils.dist import Distribution
from distutils.command.build import build
from distutils.command.build_ext import build_ext
from distutils.command.build_py import build_py
from distutils.command.build_scripts import build_scripts
from distutils.command.install import install
from distutils.command.install_lib import install_lib
from distutils.command.install_scripts import install_scripts
from distutils import log
from distutils.spawn import spawn, find_executable
from distutils import file_util
from distutils.errors import (
    CCompilerError,
    DistutilsError,
    DistutilsExecError,
)
from distutils.sysconfig import get_python_inc


def write_if_changed(path, content):
    """Write content to a file iff the content hasn't changed."""
    if os.path.exists(path):
        with open(path, 'rb') as fh:
            current = fh.read()
    else:
        current = b''

    if current != content:
        with open(path, 'wb') as fh:
            fh.write(content)


scripts = ['hg']
if os.name == 'nt':
    # We remove hg.bat if we are able to build hg.exe.
    scripts.append('contrib/win32/hg.bat')


def cancompile(cc, code):
    tmpdir = tempfile.mkdtemp(prefix='hg-install-')
    devnull = oldstderr = None
    try:
        fname = os.path.join(tmpdir, 'testcomp.c')
        f = open(fname, 'w')
        f.write(code)
        f.close()
        # Redirect stderr to /dev/null to hide any error messages
        # from the compiler.
        # This will have to be changed if we ever have to check
        # for a function on Windows.
        devnull = open('/dev/null', 'w')
        oldstderr = os.dup(sys.stderr.fileno())
        os.dup2(devnull.fileno(), sys.stderr.fileno())
        objects = cc.compile([fname], output_dir=tmpdir)
        cc.link_executable(objects, os.path.join(tmpdir, "a.out"))
        return True
    except Exception:
        return False
    finally:
        if oldstderr is not None:
            os.dup2(oldstderr, sys.stderr.fileno())
        if devnull is not None:
            devnull.close()
        shutil.rmtree(tmpdir)


# simplified version of distutils.ccompiler.CCompiler.has_function
# that actually removes its temporary files.
def hasfunction(cc, funcname):
    code = 'int main(void) { %s(); }\n' % funcname
    return cancompile(cc, code)


def hasheader(cc, headername):
    code = '#include <%s>\nint main(void) { return 0; }\n' % headername
    return cancompile(cc, code)


# py2exe needs to be installed to work
try:
    import py2exe

    py2exe.patch_distutils()
    py2exeloaded = True
    # import py2exe's patched Distribution class
    from distutils.core import Distribution
except ImportError:
    py2exeloaded = False


def runcmd(cmd, env, cwd=None):
    p = subprocess.Popen(
        cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, cwd=cwd
    )
    out, err = p.communicate()
    return p.returncode, out, err


class hgcommand:
    def __init__(self, cmd, env):
        self.cmd = cmd
        self.env = env

    def run(self, args):
        cmd = self.cmd + args
        returncode, out, err = runcmd(cmd, self.env)
        err = filterhgerr(err)
        if err:
            print("stderr from '%s':" % (' '.join(cmd)), file=sys.stderr)
            print(err, file=sys.stderr)
        if returncode != 0:
            return b''
        return out


def filterhgerr(err):
    # If root is executing setup.py, but the repository is owned by
    # another user (as in "sudo python setup.py install") we will get
    # trust warnings since the .hg/hgrc file is untrusted. That is
    # fine, we don't want to load it anyway.  Python may warn about
    # a missing __init__.py in mercurial/locale, we also ignore that.
    err = [
        e
        for e in err.splitlines()
        if (
            not e.startswith(b'not trusting file')
            and not e.startswith(b'warning: Not importing')
            and not e.startswith(b'obsolete feature not enabled')
            and not e.startswith(b'*** failed to import extension')
            and not e.startswith(b'devel-warn:')
            and not (
                e.startswith(b'(third party extension')
                and e.endswith(b'or newer of Mercurial; disabling)')
            )
        )
    ]
    return b'\n'.join(b'  ' + e for e in err)


def findhg():
    """Try to figure out how we should invoke hg for examining the local
    repository contents.

    Returns an hgcommand object."""
    # By default, prefer the "hg" command in the user's path.  This was
    # presumably the hg command that the user used to create this repository.
    #
    # This repository may require extensions or other settings that would not
    # be enabled by running the hg script directly from this local repository.
    hgenv = os.environ.copy()
    # Use HGPLAIN to disable hgrc settings that would change output formatting,
    # and disable localization for the same reasons.
    hgenv['HGPLAIN'] = '1'
    hgenv['LANGUAGE'] = 'C'
    hgcmd = ['hg']
    # Run a simple "hg log" command just to see if using hg from the user's
    # path works and can successfully interact with this repository.  Windows
    # gives precedence to hg.exe in the current directory, so fall back to the
    # python invocation of local hg, where pythonXY.dll can always be found.
    check_cmd = ['log', '-r.', '-Ttest']
    if os.name != 'nt' or not os.path.exists("hg.exe"):
        try:
            retcode, out, err = runcmd(hgcmd + check_cmd, hgenv)
        except EnvironmentError:
            retcode = -1
        if retcode == 0 and not filterhgerr(err):
            return hgcommand(hgcmd, hgenv)

    # Fall back to trying the local hg installation.
    hgenv = localhgenv()
    hgcmd = [sys.executable, 'hg']
    try:
        retcode, out, err = runcmd(hgcmd + check_cmd, hgenv)
    except EnvironmentError:
        retcode = -1
    if retcode == 0 and not filterhgerr(err):
        return hgcommand(hgcmd, hgenv)

    eprint("/!\\")
    eprint(r"/!\ Unable to find a working hg binary")
    eprint(r"/!\ Version cannot be extract from the repository")
    eprint(r"/!\ Re-run the setup once a first version is built")
    return None


def localhgenv():
    """Get an environment dictionary to use for invoking or importing
    mercurial from the local repository."""
    # Execute hg out of this directory with a custom environment which takes
    # care to not use any hgrc files and do no localization.
    env = {
        'HGMODULEPOLICY': 'py',
        'HGRCPATH': '',
        'LANGUAGE': 'C',
        'PATH': '',
    }  # make pypi modules that use os.environ['PATH'] happy
    if 'LD_LIBRARY_PATH' in os.environ:
        env['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH']
    if 'SystemRoot' in os.environ:
        # SystemRoot is required by Windows to load various DLLs.  See:
        # https://bugs.python.org/issue13524#msg148850
        env['SystemRoot'] = os.environ['SystemRoot']
    return env


version = ''


def _try_get_version():
    hg = findhg()
    if hg is None:
        return ''
    hgid = None
    numerictags = []
    cmd = ['log', '-r', '.', '--template', '{tags}\n']
    pieces = sysstr(hg.run(cmd)).split()
    numerictags = [t for t in pieces if t[0:1].isdigit()]
    hgid = sysstr(hg.run(['id', '-i'])).strip()
    if hgid.count('+') == 2:
        hgid = hgid.replace("+", ".", 1)
    if not hgid:
        eprint("/!\\")
        eprint(r"/!\ Unable to determine hg version from local repository")
        eprint(r"/!\ Failed to retrieve current revision tags")
        return ''
    if numerictags:  # tag(s) found
        version = numerictags[-1]
        if hgid.endswith('+'):  # propagate the dirty status to the tag
            version += '+'
    else:  # no tag found on the checked out revision
        ltagcmd = ['log', '--rev', 'wdir()', '--template', '{latesttag}']
        ltag = sysstr(hg.run(ltagcmd))
        if not ltag:
            eprint("/!\\")
            eprint(r"/!\ Unable to determine hg version from local repository")
            eprint(
                r"/!\ Failed to retrieve current revision distance to lated tag"
            )
            return ''
        changessincecmd = [
            'log',
            '-T',
            'x\n',