Skip to content
Snippets Groups Projects
setup.py 56.9 KiB
Newer Older
mpm's avatar
mpm committed
#
# This is the mercurial setup script.
# 'python setup.py install', or
# 'python setup.py --help' for more options
supportedpy = '~= 2.7'
if os.environ.get('HGALLOWPYTHON3', ''):
    # Mercurial will never work on Python 3 before 3.5 due to a lack
    # of % formatting on bytestrings, and can't work on 3.6.0 or 3.6.1
    # due to a bug in % formatting in bytestrings.
    # We cannot support Python 3.5.0, 3.5.1, 3.5.2 because of bug in
    # codecs.escape_encode() where it raises SystemError on empty bytestring
    # bug link: https://bugs.python.org/issue25270
    #
    # TODO: when we actually work on Python 3, use this string as the
    # actual supportedpy string.
    supportedpy = ','.join(
        [
            '>=2.7',
            '!=3.0.*',
            '!=3.1.*',
            '!=3.2.*',
            '!=3.3.*',
            '!=3.4.*',
            '!=3.5.0',
            '!=3.5.1',
            '!=3.5.2',
            '!=3.6.0',
            '!=3.6.1',
        ]
    )
if sys.version_info[0] >= 3:
    printf = eval('print')
    libdir_escape = 'unicode_escape'
    def sysstr(s):
        return s.decode('latin-1')
    libdir_escape = 'string_escape'
    def printf(*args, **kwargs):
        f = kwargs.get('file', sys.stdout)
        end = kwargs.get('end', '\n')
        f.write(b' '.join(args) + end)
# Attempt to guide users to a modern pip - this means that 2.6 users
# should have a chance of getting a 4.2 release, and when we ratchet
# the version requirement forward again hopefully everyone will get
# something that works for them.
if sys.version_info < (2, 7, 0, 'final'):
    pip_message = (
        'This may be due to an out of date pip. '
        'Make sure you have pip >= 9.0.1.'
    )
        pip_version = tuple([int(x) for x in pip.__version__.split('.')[:3]])
        if pip_version < (9, 0, 1):
            pip_message = (
                'Your pip version is out of date, please install '
                'pip >= 9.0.1. pip {} detected.'.format(pip.__version__)
            )
        else:
            # pip is new enough - it must be something else
            pip_message = ''
    except Exception:
        pass
    error = """
Mercurial does not support Python older than 2.7.
Python {py} detected.
{pip}
""".format(
        py=sys.version_info, pip=pip_message
    )
    printf(error, file=sys.stderr)
    sys.exit(1)

# We don't yet officially support Python 3. But we want to allow developers to
# hack on. Detect and disallow running on Python 3 by default. But provide a
# backdoor to enable working on Python 3.
if sys.version_info[0] != 2:
    badpython = True

    # Allow Python 3 from source checkouts.
    if os.path.isdir('.hg') or 'HGPYTHON3' in os.environ:
        badpython = False

    if badpython:
        error = """
Python {py} detected.

Mercurial currently has beta support for Python 3 and use of Python 2.7 is
recommended for the best experience.

Please re-run with Python 2.7 for a faster, less buggy experience.

If you would like to beta test Mercurial with Python 3, this error can
be suppressed by defining the HGPYTHON3 environment variable when invoking
this command. No special environment variables or configuration changes are
necessary to run `hg` with Python 3.

See https://www.mercurial-scm.org/wiki/Python3 for more on Mercurial's
Python 3 support.
""".format(
            py='.'.join('%d' % x for x in sys.version_info[0:2])
        )

        printf(error, file=sys.stderr)
        sys.exit(1)

if sys.version_info[0] >= 3:
    DYLIB_SUFFIX = sysconfig.get_config_vars()['EXT_SUFFIX']
else:
    # deprecated in Python 3
    DYLIB_SUFFIX = sysconfig.get_config_vars()['SO']

# Solaris Python packaging brain damage
try:
    import hashlib
    sha = hashlib.sha1()

        sha.sha  # silence unused import warning
            "Couldn't import standard hashlib (incomplete Python install)."
        )

    zlib.compressobj  # silence unused import warning
        "Couldn't import standard zlib (incomplete Python install)."
    )
# The base IronPython distribution (as of 2.7.1) doesn't support bz2
isironpython = False
    isironpython = (
        platform.python_implementation().lower().find("ironpython") != -1
    )
except AttributeError:
    sys.stderr.write("warning: IronPython detected (no bz2 support)\n")

        bz2.BZ2Compressor  # silence unused import warning
            "Couldn't import standard bz2 (incomplete Python install)."
        )
ispypy = "PyPy" in sys.version

hgrustext = os.environ.get('HGWITHRUSTEXT')
# TODO record it for proper rebuild upon changes
# (see mercurial/__modulepolicy__.py)
if hgrustext != 'cpython' and hgrustext is not None:
    hgrustext = 'direct-ffi'
import stat, subprocess, time
# 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
    from setuptools import setup
else:
    from distutils.core 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.spawn import spawn, find_executable
from distutils.errors import (
    CCompilerError,
    DistutilsError,
    DistutilsExecError,
)
from distutils.sysconfig import get_python_inc, get_config_var
from distutils.version import StrictVersion
# Explain to distutils.StrictVersion how our release candidates are versionned
StrictVersion.version_re = re.compile(r'^(\d+)\.(\d+)(\.(\d+))?-?(rc(\d+))?$')

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')
Matt Mackall's avatar
Matt Mackall committed

def cancompile(cc, code):
    tmpdir = tempfile.mkdtemp(prefix='hg-install-')
        fname = os.path.join(tmpdir, 'testcomp.c')
        f = open(fname, 'w')
        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"))
    except Exception:
        return False
        if oldstderr is not None:
            os.dup2(oldstderr, sys.stderr.fileno())
        if devnull is not None:
            devnull.close()
# 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.Distribution  # silence unused import warning
    # import py2exe's patched Distribution class
    from distutils.core import Distribution
def runcmd(cmd, env, cwd=None):
    p = subprocess.Popen(
        cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, cwd=cwd
    )
Matt Harbison's avatar
Matt Harbison committed
    out, err = p.communicate()
    return p.returncode, out, err
class hgcommand(object):
    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)
        if err or returncode != 0:
            printf("stderr from '%s':" % (' '.join(cmd)), file=sys.stderr)
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':
        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):
    raise SystemExit(
        'Unable to find a working hg binary to extract the '
        'version from the repository tags'
    )


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']
Jun Wu's avatar
Jun Wu committed
    return env
    cmd = ['log', '-r', '.', '--template', '{tags}\n']
    numerictags = [t for t in sysstr(hg.run(cmd)).split() if t[0:1].isdigit()]
    hgid = sysstr(hg.run(['id', '-i'])).strip()
    if not hgid:
        # Bail out if hg is having problems interacting with this repository,
        # rather than falling through and producing a bogus version number.
        # Continuing with an invalid version number will break extensions
        # that define minimumhgversion.
        raise SystemExit('Unable to determine hg version from local repository')
    if numerictags:  # tag(s) found
        version = numerictags[-1]
        if hgid.endswith('+'):  # propagate the dirty status to the tag
    else:  # no tag found
        ltagcmd = ['parents', '--template', '{latesttag}']
        ltag = sysstr(hg.run(ltagcmd))
        changessincecmd = ['log', '-T', 'x\n', '-r', "only(.,'%s')" % ltag]
        changessince = len(hg.run(changessincecmd).splitlines())
        version = '%s+%s-%s' % (ltag, changessince, hgid)
    if version.endswith('+'):
        version += time.strftime('%Y%m%d')
elif os.path.exists('.hg_archival.txt'):
    kw = dict(
        [[t.strip() for t in l.split(':', 1)] for l in open('.hg_archival.txt')]
    )
        if 'changessincelatesttag' in kw:
            version = '%(latesttag)s+%(changessincelatesttag)s-%(node).12s' % kw
        else:
            version = '%(latesttag)s+%(latesttagdistance)s-%(node).12s' % kw
        version = kw.get('node', '')[:12]
    versionb = version
    if not isinstance(versionb, bytes):
        versionb = versionb.encode('ascii')

    write_if_changed(
        'mercurial/__version__.py',
        b''.join(
            [
                b'# this file is autogenerated by setup.py\n'
                b'version = b"%s"\n' % versionb,
            ]
        ),
    )
    oldpolicy = os.environ.get('HGMODULEPOLICY', None)
    os.environ['HGMODULEPOLICY'] = 'py'
    from mercurial import __version__
    version = __version__.version
except ImportError:
finally:
    if oldpolicy is None:
        del os.environ['HGMODULEPOLICY']
    else:
        os.environ['HGMODULEPOLICY'] = oldpolicy
class hgbuild(build):
    # Insert hgbuildmo first so that files in mercurial/locale/ are found
    # when build_py is run next.
    sub_commands = [('build_mo', None)] + build.sub_commands
Matt Mackall's avatar
Matt Mackall committed
class hgbuildmo(build):

    description = "build translations (.mo files)"

    def run(self):
        if not find_executable('msgfmt'):
            self.warn(
                "could not find msgfmt executable, no translations "
                "will be built"
            )
            return

        podir = 'i18n'
        if not os.path.isdir(podir):
            self.warn("could not find %s/ directory" % podir)
            return

        join = os.path.join
        for po in os.listdir(podir):
            if not po.endswith('.po'):
                continue
            pofile = join(podir, po)
            modir = join('locale', po[:-3], 'LC_MESSAGES')
            mofile = join(modir, 'hg.mo')
            mobuildfile = join('mercurial', mofile)
            cmd = ['msgfmt', '-v', '-o', mobuildfile, pofile]
            if sys.platform != 'sunos5':
                # msgfmt on Solaris does not know about -c
                cmd.append('-c')
            self.mkpath(join('mercurial', modir))
            self.make_file([pofile], mobuildfile, spawn, (cmd,))
class hgdist(Distribution):
    rust = hgrustext is not None
    global_options = Distribution.global_options + [
        ('pure', None, "use pure (slow) Python code instead of C extensions"),
        ('rust', None, "use Rust extensions additionally to C extensions"),
    def has_ext_modules(self):
        # self.ext_modules is emptied in hgbuildpy.finalize_options which is
        # too late for some cases
        return not self.pure and Distribution.has_ext_modules(self)

# This is ugly as a one-liner. So use a variable.
buildextnegops = dict(getattr(build_ext, 'negative_options', {}))
buildextnegops['no-zstd'] = 'zstd'
buildextnegops['no-rust'] = 'rust'
class hgbuildext(build_ext):
    user_options = build_ext.user_options + [
        ('zstd', None, 'compile zstd bindings [default]'),
        ('no-zstd', None, 'do not compile zstd bindings'),
        (
            'rust',
            None,
            'compile Rust extensions if they are in use '
            '(requires Cargo) [default]',
        ),
        ('no-rust', None, 'do not compile Rust extensions'),
    boolean_options = build_ext.boolean_options + ['zstd', 'rust']
    negative_opt = buildextnegops

    def initialize_options(self):
        self.zstd = True
        self.rust = True

        return build_ext.initialize_options(self)

    def finalize_options(self):
        # Unless overridden by the end user, build extensions in parallel.
        # Only influences behavior on Python 3.5+.
        if getattr(self, 'parallel', None) is None:
            self.parallel = True

        return build_ext.finalize_options(self)

    def build_extensions(self):
        ruststandalones = [
            e for e in self.extensions if isinstance(e, RustStandaloneExtension)
        ]
        self.extensions = [
            e for e in self.extensions if e not in ruststandalones
        ]
        # Filter out zstd if disabled via argument.
        if not self.zstd:
            self.extensions = [
                e for e in self.extensions if e.name != 'mercurial.zstd'
            ]
        # Build Rust standalon extensions if it'll be used
        # and its build is not explictely disabled (for external build
        # as Linux distributions would do)
        if self.distribution.rust and self.rust and hgrustext != 'direct-ffi':
            for rustext in ruststandalones:
                rustext.build('' if self.inplace else self.build_lib)
        return build_ext.build_extensions(self)

    def build_extension(self, ext):
        if (
            self.distribution.rust
            and self.rust
            and isinstance(ext, RustExtension)
        ):
            ext.rustbuild()
        try:
            build_ext.build_extension(self, ext)
        except CCompilerError:
            if not getattr(ext, 'optional', False):
            log.warn(
                "Failed to build optional extension '%s' (skipping)", ext.name
            )

class hgbuildscripts(build_scripts):
    def run(self):
        if os.name != 'nt' or self.distribution.pure:
            return build_scripts.run(self)

        exebuilt = False
        try:
            self.run_command('build_hgexe')
            exebuilt = True
        except (DistutilsError, CCompilerError):
            log.warn('failed to build optional hg.exe')

        if exebuilt:
            # Copying hg.exe to the scripts build directory ensures it is
            # installed by the install_scripts command.
            hgexecommand = self.get_finalized_command('build_hgexe')
            dest = os.path.join(self.build_dir, 'hg.exe')
            self.mkpath(self.build_dir)
            self.copy_file(hgexecommand.hgexepath, dest)

            # Remove hg.bat because it is redundant with hg.exe.
            self.scripts.remove('contrib/win32/hg.bat')

        return build_scripts.run(self)

class hgbuildpy(build_py):
    def finalize_options(self):
        build_py.finalize_options(self)

        if self.distribution.pure:
            self.distribution.ext_modules = []
            from mercurial.cffi import (
                bdiffbuild,
                mpatchbuild,

            exts = [
                mpatchbuild.ffi.distutils_extension(),
                bdiffbuild.ffi.distutils_extension(),
            ]
            if sys.platform == 'darwin':
                from mercurial.cffi import osutilbuild
                exts.append(osutilbuild.ffi.distutils_extension())
            h = os.path.join(get_python_inc(), 'Python.h')
            if not os.path.exists(h):
                raise SystemExit(
                    'Python headers are required to build '
                    'Mercurial but weren\'t found in %s' % h
                )
    def run(self):
        basepath = os.path.join(self.build_lib, 'mercurial')
        self.mkpath(basepath)

        rust = self.distribution.rust
        if self.distribution.pure:
            modulepolicy = 'py'
        elif self.build_lib == '.':
            # in-place build should run without rebuilding and Rust extensions
            modulepolicy = 'rust+c-allow' if rust else 'allow'
            modulepolicy = 'rust+c' if rust else 'c'
        content = b''.join(
            [
                b'# this file is autogenerated by setup.py\n',
                b'modulepolicy = b"%s"\n' % modulepolicy.encode('ascii'),
            ]
        )
        write_if_changed(os.path.join(basepath, '__modulepolicy__.py'), content)
        build_py.run(self)
class buildhgextindex(Command):
    description = 'generate prebuilt index of hgext (for frozen package)'
    user_options = []
    _indexfilename = 'hgext/__index__.py'

    def initialize_options(self):
        pass

    def finalize_options(self):
        pass

    def run(self):
        if os.path.exists(self._indexfilename):
timeless's avatar
timeless committed
            with open(self._indexfilename, 'w') as f:
                f.write('# empty\n')

        # here no extension enabled, disabled() lists up everything
        code = (
            'import pprint; from mercurial import extensions; '
            'pprint.pprint(extensions.disabled())'
        )
        returncode, out, err = runcmd(
            [sys.executable, '-c', code], localhgenv()
        )
        if err or returncode != 0:
            raise DistutilsExecError(err)

        with open(self._indexfilename, 'wb') as f:
            f.write(b'# this file is autogenerated by setup.py\n')
            f.write(b'docs = ')
timeless's avatar
timeless committed
            f.write(out)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
class buildhgexe(build_ext):
    description = 'compile hg.exe from mercurial/exewrapper.c'
    user_options = build_ext.user_options + [
        (
            'long-paths-support',
            None,
            'enable support for long paths on '
            'Windows (off by default and '
            'experimental)',
        ),
    ]

    LONG_PATHS_MANIFEST = """
    <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
        <application>
            <windowsSettings
            xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
                <ws2:longPathAware>true</ws2:longPathAware>
            </windowsSettings>
        </application>
    </assembly>"""

    def initialize_options(self):
        build_ext.initialize_options(self)
        self.long_paths_support = False
Adrian Buehlmann's avatar
Adrian Buehlmann committed

    def build_extensions(self):
        if os.name != 'nt':
            return
        if isinstance(self.compiler, HackedMingw32CCompiler):
            self.compiler.compiler_so = self.compiler.compiler  # no -mdll
            self.compiler.dll_libraries = []  # no -lmsrvc90

        # Different Python installs can have different Python library
        # names. e.g. the official CPython distribution uses pythonXY.dll
        # and MinGW uses libpythonX.Y.dll.
        _kernel32 = ctypes.windll.kernel32
        _kernel32.GetModuleFileNameA.argtypes = [
            ctypes.c_void_p,
            ctypes.c_void_p,
            ctypes.c_ulong,
        ]
        _kernel32.GetModuleFileNameA.restype = ctypes.c_ulong
        size = 1000
        buf = ctypes.create_string_buffer(size + 1)
        filelen = _kernel32.GetModuleFileNameA(
            sys.dllhandle, ctypes.byref(buf), size
        )

        if filelen > 0 and filelen != size:
            dllbasename = os.path.basename(buf.value)
            if not dllbasename.lower().endswith(b'.dll'):
                raise SystemExit(
                    'Python DLL does not end with .dll: %s' % dllbasename
                )
            pythonlib = dllbasename[:-4]
        else:
            log.warn(
                'could not determine Python DLL filename; ' 'assuming pythonXY'
            )
            pythonlib = 'python%d%d' % (hv >> 24, (hv >> 16) & 0xFF)

        log.info('using %s as Python library name' % pythonlib)
timeless's avatar
timeless committed
        with open('mercurial/hgpythonlib.h', 'wb') as f:
            f.write(b'/* this file is autogenerated by setup.py */\n')
            f.write(b'#define HGPYTHONLIB "%s"\n' % pythonlib)

        macros = None
        if sys.version_info[0] >= 3:
            macros = [('_UNICODE', None), ('UNICODE', None)]

        objects = self.compiler.compile(
            ['mercurial/exewrapper.c'],
            output_dir=self.build_temp,
            macros=macros,
        )
Adrian Buehlmann's avatar
Adrian Buehlmann committed
        dir = os.path.dirname(self.get_ext_fullpath('dummy'))
        self.hgtarget = os.path.join(dir, 'hg')
        self.compiler.link_executable(
            objects, self.hgtarget, libraries=[], output_dir=self.build_temp
        )
        if self.long_paths_support:
            self.addlongpathsmanifest()

    def addlongpathsmanifest(self):
        r"""Add manifest pieces so that hg.exe understands long paths

        This is an EXPERIMENTAL feature, use with care.
        To enable long paths support, one needs to do two things:
        - build Mercurial with --long-paths-support option
        - change HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\
                 LongPathsEnabled to have value 1.

        Please ignore 'warning 81010002: Unrecognized Element "longPathAware"';
        it happens because Mercurial uses mt.exe circa 2008, which is not
        yet aware of long paths support in the manifest (I think so at least).
        This does not stop mt.exe from embedding/merging the XML properly.

        Why resource #1 should be used for .exe manifests? I don't know and
        wasn't able to find an explanation for mortals. But it seems to work.
        """
        exefname = self.compiler.executable_filename(self.hgtarget)
        fdauto, manfname = tempfile.mkstemp(suffix='.hg.exe.manifest')
        os.close(fdauto)
        with open(manfname, 'w') as f:
            f.write(self.LONG_PATHS_MANIFEST)
        log.info("long paths manifest is written to '%s'" % manfname)
        inputresource = '-inputresource:%s;#1' % exefname
        outputresource = '-outputresource:%s;#1' % exefname
        log.info("running mt.exe to update hg.exe's manifest in-place")
        # supplying both -manifest and -inputresource to mt.exe makes
        # it merge the embedded and supplied manifests in the -outputresource
        self.spawn(
            [
                'mt.exe',
                '-nologo',
                '-manifest',
                manfname,
                inputresource,
                outputresource,
            ]
        )
        log.info("done updating hg.exe's manifest")
        os.remove(manfname)
    @property
    def hgexepath(self):
        dir = os.path.dirname(self.get_ext_fullpath('dummy'))
        return os.path.join(self.build_temp, dir, 'hg.exe')

class hgbuilddoc(Command):
    description = 'build documentation'
    user_options = [
        ('man', None, 'generate man pages'),
        ('html', None, 'generate html pages'),
    ]

    def initialize_options(self):
        self.man = None
        self.html = None

    def finalize_options(self):
        # If --man or --html are set, only generate what we're told to.
        # Otherwise generate everything.
        have_subset = self.man is not None or self.html is not None

        if have_subset:
            self.man = True if self.man else False
            self.html = True if self.html else False
        else:
            self.man = True
            self.html = True

    def run(self):
        def normalizecrlf(p):
            with open(p, 'rb') as fh:
                orig = fh.read()

            if b'\r\n' not in orig:
                return

            log.info('normalizing %s to LF line endings' % p)
            with open(p, 'wb') as fh:
                fh.write(orig.replace(b'\r\n', b'\n'))

        def gentxt(root):
            txt = 'doc/%s.txt' % root
            log.info('generating %s' % txt)
            res, out, err = runcmd(
                [sys.executable, 'gendoc.py', root], os.environ, cwd='doc'
            )
                raise SystemExit(
                    'error running gendoc.py: %s' % '\n'.join([out, err])
                )

            with open(txt, 'wb') as fh:
                fh.write(out)

        def gengendoc(root):
            gendoc = 'doc/%s.gendoc.txt' % root

            log.info('generating %s' % gendoc)
            res, out, err = runcmd(
                [sys.executable, 'gendoc.py', '%s.gendoc' % root],
                os.environ,
                raise SystemExit(
                    'error running gendoc: %s' % '\n'.join([out, err])
                )

            with open(gendoc, 'wb') as fh:
                fh.write(out)

        def genman(root):
            log.info('generating doc/%s' % root)
            res, out, err = runcmd(
                [
                    sys.executable,
                    'runrst',
                    'hgmanpage',
                    '--halt',
                    'warning',
                    '--strip-elements-with-class',
                    'htmlonly',
                    '%s.txt' % root,
                    root,
                ],
                os.environ,
                raise SystemExit(
                    'error running runrst: %s' % '\n'.join([out, err])
                )

            normalizecrlf('doc/%s' % root)

        def genhtml(root):
            log.info('generating doc/%s.html' % root)
            res, out, err = runcmd(
                [
                    sys.executable,
                    'runrst',
                    'html',
                    '--halt',
                    'warning',
                    '--link-stylesheet',
                    '--stylesheet-path',
                    'style.css',
                    '%s.txt' % root,
                    '%s.html' % root,
                ],
                os.environ,
                raise SystemExit(
                    'error running runrst: %s' % '\n'.join([out, err])
                )

            normalizecrlf('doc/%s.html' % root)

        # This logic is duplicated in doc/Makefile.
        sources = set(
            f
            for f in os.listdir('mercurial/help')
            if re.search(r'[0-9]\.txt$', f)
        )

        # common.txt is a one-off.
        gentxt('common')

        for source in sorted(sources):
            assert source[-4:] == '.txt'
            root = source[:-4]

            gentxt(root)
            gengendoc(root)

            if self.man:
                genman(root)
            if self.html:
                genhtml(root)

class hginstall(install):

    user_options = install.user_options + [
        (
            'old-and-unmanageable',
            None,
            'noop, present for eggless setuptools compat',
        ),
        (