setup.py 55.9 KB
Newer Older
1 2
#
# This is the mercurial setup script.
3
#
4 5
# 'python setup.py install', or
# 'python setup.py --help' for more options
6

7 8
import os

9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
# 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
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',
    ]
)
30

31
import sys, platform
32
import sysconfig
33

34
if sys.version_info[0] >= 3:
35 36
    printf = eval('print')
    libdir_escape = 'unicode_escape'
37

38 39
    def sysstr(s):
        return s.decode('latin-1')
40 41


42
else:
43
    libdir_escape = 'string_escape'
44

45 46 47
    def printf(*args, **kwargs):
        f = kwargs.get('file', sys.stdout)
        end = kwargs.get('end', '\n')
48
        f.write(b' '.join(args) + end)
49

50 51
    def sysstr(s):
        return s
52

53

54 55 56 57 58
# 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'):
59 60 61 62
    pip_message = (
        'This may be due to an out of date pip. '
        'Make sure you have pip >= 9.0.1.'
    )
63 64
    try:
        import pip
65

66
        pip_version = tuple([int(x) for x in pip.__version__.split('.')[:3]])
67
        if pip_version < (9, 0, 1):
68 69
            pip_message = (
                'Your pip version is out of date, please install '
70 71
                'pip >= 9.0.1. pip {} detected.'.format(pip.__version__)
            )
72 73 74 75 76 77 78 79 80
        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}
81 82 83
""".format(
        py=sys.version_info, pip=pip_message
    )
84 85 86
    printf(error, file=sys.stderr)
    sys.exit(1)

87 88 89 90 91 92
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']

93 94 95
# Solaris Python packaging brain damage
try:
    import hashlib
96

97
    sha = hashlib.sha1()
98
except ImportError:
99 100
    try:
        import sha
101 102

        sha.sha  # silence unused import warning
103
    except ImportError:
104
        raise SystemExit(
105 106
            "Couldn't import standard hashlib (incomplete Python install)."
        )
107 108 109

try:
    import zlib
110 111

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

117 118
# The base IronPython distribution (as of 2.7.1) doesn't support bz2
isironpython = False
119
try:
120 121 122
    isironpython = (
        platform.python_implementation().lower().find("ironpython") != -1
    )
123
except AttributeError:
124 125 126
    pass

if isironpython:
127
    sys.stderr.write("warning: IronPython detected (no bz2 support)\n")
128 129 130
else:
    try:
        import bz2
131 132

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

138 139
ispypy = "PyPy" in sys.version

140 141 142 143 144
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'
145

146
import ctypes
147
import errno
148
import stat, subprocess, time
149
import re
150 151
import shutil
import tempfile
152
from distutils import log
153

154 155 156
# 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.
157
issetuptools = os.name == 'nt' or 'FORCE_SETUPTOOLS' in os.environ
158
if issetuptools:
159 160 161
    from setuptools import setup
else:
    from distutils.core import setup
162
from distutils.ccompiler import new_compiler
163
from distutils.core import Command, Extension
164
from distutils.dist import Distribution
165
from distutils.command.build import build
166
from distutils.command.build_ext import build_ext
167
from distutils.command.build_py import build_py
168
from distutils.command.build_scripts import build_scripts
169
from distutils.command.install import install
170
from distutils.command.install_lib import install_lib
171
from distutils.command.install_scripts import install_scripts
172
from distutils.spawn import spawn, find_executable
173
from distutils import file_util
174 175 176 177 178
from distutils.errors import (
    CCompilerError,
    DistutilsError,
    DistutilsExecError,
)
179
from distutils.sysconfig import get_python_inc, get_config_var
180
from distutils.version import StrictVersion
181

182 183 184
# Explain to distutils.StrictVersion how our release candidates are versionned
StrictVersion.version_re = re.compile(r'^(\d+)\.(\d+)(\.(\d+))?-?(rc(\d+))?$')

185

186 187 188 189 190 191 192 193 194 195 196 197
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)

198

199 200
scripts = ['hg']
if os.name == 'nt':
201
    # We remove hg.bat if we are able to build hg.exe.
202
    scripts.append('contrib/win32/hg.bat')
Matt Mackall committed
203

204

205
def cancompile(cc, code):
206
    tmpdir = tempfile.mkdtemp(prefix='hg-install-')
207
    devnull = oldstderr = None
208
    try:
209
        fname = os.path.join(tmpdir, 'testcomp.c')
210
        f = open(fname, 'w')
211
        f.write(code)
212 213 214 215 216 217 218 219 220 221
        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"))
222
        return True
223 224
    except Exception:
        return False
225
    finally:
226 227 228 229
        if oldstderr is not None:
            os.dup2(oldstderr, sys.stderr.fileno())
        if devnull is not None:
            devnull.close()
230 231
        shutil.rmtree(tmpdir)

232

233 234 235 236 237 238
# 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)

239

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

244

245 246
# py2exe needs to be installed to work
try:
247
    import py2exe
248 249

    py2exe.Distribution  # silence unused import warning
250
    py2exeloaded = True
251 252
    # import py2exe's patched Distribution class
    from distutils.core import Distribution
253
except ImportError:
254
    py2exeloaded = False
255

256

257
def runcmd(cmd, env, cwd=None):
258 259 260
    p = subprocess.Popen(
        cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, cwd=cwd
    )
261
    out, err = p.communicate()
262
    return p.returncode, out, err
263

264

265
class hgcommand(object):
266 267 268
    def __init__(self, cmd, env):
        self.cmd = cmd
        self.env = env
269 270 271 272

    def run(self, args):
        cmd = self.cmd + args
        returncode, out, err = runcmd(cmd, self.env)
273
        err = filterhgerr(err)
274 275
        if err or returncode != 0:
            printf("stderr from '%s':" % (' '.join(cmd)), file=sys.stderr)
276
            printf(err, file=sys.stderr)
277 278
            return ''
        return out
279

280

281 282 283 284 285 286
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.
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301
    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)')
            )
        )
    ]
302 303
    return b'\n'.join(b'  ' + e for e in err)

304

305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
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
322 323 324
    # 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.
325
    check_cmd = ['log', '-r.', '-Ttest']
326 327 328 329 330 331 332
    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)
333 334 335 336 337 338 339 340

    # 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
341
    if retcode == 0 and not filterhgerr(err):
342 343
        return hgcommand(hgcmd, hgenv)

344 345 346 347 348
    raise SystemExit(
        'Unable to find a working hg binary to extract the '
        'version from the repository tags'
    )

349 350 351 352

def localhgenv():
    """Get an environment dictionary to use for invoking or importing
    mercurial from the local repository."""
353 354
    # Execute hg out of this directory with a custom environment which takes
    # care to not use any hgrc files and do no localization.
355 356 357 358 359 360
    env = {
        'HGMODULEPOLICY': 'py',
        'HGRCPATH': '',
        'LANGUAGE': 'C',
        'PATH': '',
    }  # make pypi modules that use os.environ['PATH'] happy
361 362 363 364 365 366
    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 committed
367
    return env
368

369

370
version = ''
371

372
if os.path.isdir('.hg'):
373
    hg = findhg()
374
    cmd = ['log', '-r', '.', '--template', '{tags}\n']
375 376
    numerictags = [t for t in sysstr(hg.run(cmd)).split() if t[0:1].isdigit()]
    hgid = sysstr(hg.run(['id', '-i'])).strip()
377 378 379 380 381 382
    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')
383
    if numerictags:  # tag(s) found
384
        version = numerictags[-1]
385
        if hgid.endswith('+'):  # propagate the dirty status to the tag
386
            version += '+'
387
    else:  # no tag found
388
        ltagcmd = ['parents', '--template', '{latesttag}']
389
        ltag = sysstr(hg.run(ltagcmd))
390 391
        changessincecmd = ['log', '-T', 'x\n', '-r', "only(.,'%s')" % ltag]
        changessince = len(hg.run(changessincecmd).splitlines())
392
        version = '%s+%s-%s' % (ltag, changessince, hgid)
393 394
    if version.endswith('+'):
        version += time.strftime('%Y%m%d')
395
elif os.path.exists('.hg_archival.txt'):
396 397 398
    kw = dict(
        [[t.strip() for t in l.split(':', 1)] for l in open('.hg_archival.txt')]
    )
399
    if 'tag' in kw:
400
        version = kw['tag']
401
    elif 'latesttag' in kw:
402 403 404 405
        if 'changessincelatesttag' in kw:
            version = '%(latesttag)s+%(changessincelatesttag)s-%(node).12s' % kw
        else:
            version = '%(latesttag)s+%(latesttagdistance)s-%(node).12s' % kw
406 407
    else:
        version = kw.get('node', '')[:12]
408 409

if version:
410 411 412 413
    versionb = version
    if not isinstance(versionb, bytes):
        versionb = versionb.encode('ascii')

414 415 416 417 418 419 420 421 422
    write_if_changed(
        'mercurial/__version__.py',
        b''.join(
            [
                b'# this file is autogenerated by setup.py\n'
                b'version = b"%s"\n' % versionb,
            ]
        ),
    )
423 424

try:
425 426
    oldpolicy = os.environ.get('HGMODULEPOLICY', None)
    os.environ['HGMODULEPOLICY'] = 'py'
427
    from mercurial import __version__
428

429 430
    version = __version__.version
except ImportError:
431
    version = b'unknown'
432 433 434 435 436
finally:
    if oldpolicy is None:
        del os.environ['HGMODULEPOLICY']
    else:
        os.environ['HGMODULEPOLICY'] = oldpolicy
Matt Mackall committed
437

438

439 440 441
class hgbuild(build):
    # Insert hgbuildmo first so that files in mercurial/locale/ are found
    # when build_py is run next.
442
    sub_commands = [('build_mo', None)] + build.sub_commands
443

444

445
class hgbuildmo(build):
446 447 448 449 450

    description = "build translations (.mo files)"

    def run(self):
        if not find_executable('msgfmt'):
451 452 453 454
            self.warn(
                "could not find msgfmt executable, no translations "
                "will be built"
            )
455 456 457 458 459 460 461 462 463 464 465 466 467 468
            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')
469 470
            mobuildfile = join('mercurial', mofile)
            cmd = ['msgfmt', '-v', '-o', mobuildfile, pofile]
471 472 473
            if sys.platform != 'sunos5':
                # msgfmt on Solaris does not know about -c
                cmd.append('-c')
474 475
            self.mkpath(join('mercurial', modir))
            self.make_file([pofile], mobuildfile, spawn, (cmd,))
476

477

478
class hgdist(Distribution):
479
    pure = False
480
    rust = hgrustext is not None
481
    cffi = ispypy
482

483 484
    global_options = Distribution.global_options + [
        ('pure', None, "use pure (slow) Python code instead of C extensions"),
485
        ('rust', None, "use Rust extensions additionally to C extensions"),
486
    ]
487

488 489 490 491 492
    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)

493

494 495 496
# This is ugly as a one-liner. So use a variable.
buildextnegops = dict(getattr(build_ext, 'negative_options', {}))
buildextnegops['no-zstd'] = 'zstd'
497
buildextnegops['no-rust'] = 'rust'
498

499

500
class hgbuildext(build_ext):
501 502 503
    user_options = build_ext.user_options + [
        ('zstd', None, 'compile zstd bindings [default]'),
        ('no-zstd', None, 'do not compile zstd bindings'),
504 505 506 507 508 509
        (
            'rust',
            None,
            'compile Rust extensions if they are in use '
            '(requires Cargo) [default]',
        ),
510
        ('no-rust', None, 'do not compile Rust extensions'),
511 512
    ]

513
    boolean_options = build_ext.boolean_options + ['zstd', 'rust']
514 515 516 517
    negative_opt = buildextnegops

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

520 521
        return build_ext.initialize_options(self)

522 523 524 525 526 527 528 529
    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)

530
    def build_extensions(self):
531 532 533 534 535 536
        ruststandalones = [
            e for e in self.extensions if isinstance(e, RustStandaloneExtension)
        ]
        self.extensions = [
            e for e in self.extensions if e not in ruststandalones
        ]
537 538
        # Filter out zstd if disabled via argument.
        if not self.zstd:
539 540 541
            self.extensions = [
                e for e in self.extensions if e.name != 'mercurial.zstd'
            ]
542

543 544 545 546 547 548
        # 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)
549

550
        return build_ext.build_extensions(self)
551 552

    def build_extension(self, ext):
553 554 555 556 557 558
        if (
            self.distribution.rust
            and self.rust
            and isinstance(ext, RustExtension)
        ):
            ext.rustbuild()
559 560 561
        try:
            build_ext.build_extension(self, ext)
        except CCompilerError:
562
            if not getattr(ext, 'optional', False):
563
                raise
564 565 566 567
            log.warn(
                "Failed to build optional extension '%s' (skipping)", ext.name
            )

568

569 570
class hgbuildscripts(build_scripts):
    def run(self):
571
        if os.name != 'nt' or self.distribution.pure:
572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593
            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)

594

595
class hgbuildpy(build_py):
596 597 598 599 600
    def finalize_options(self):
        build_py.finalize_options(self)

        if self.distribution.pure:
            self.distribution.ext_modules = []
601
        elif self.distribution.cffi:
602
            from mercurial.cffi import (
603 604
                bdiffbuild,
                mpatchbuild,
605
            )
606 607 608 609 610

            exts = [
                mpatchbuild.ffi.distutils_extension(),
                bdiffbuild.ffi.distutils_extension(),
            ]
611
            # cffi modules go here
612
            if sys.platform == 'darwin':
613
                from mercurial.cffi import osutilbuild
614

615
                exts.append(osutilbuild.ffi.distutils_extension())
616
            self.distribution.ext_modules = exts
617
        else:
618 619
            h = os.path.join(get_python_inc(), 'Python.h')
            if not os.path.exists(h):
620 621 622 623
                raise SystemExit(
                    'Python headers are required to build '
                    'Mercurial but weren\'t found in %s' % h
                )
624

625
    def run(self):
626 627 628
        basepath = os.path.join(self.build_lib, 'mercurial')
        self.mkpath(basepath)

629
        rust = self.distribution.rust
630 631
        if self.distribution.pure:
            modulepolicy = 'py'
632
        elif self.build_lib == '.':
633 634
            # in-place build should run without rebuilding and Rust extensions
            modulepolicy = 'rust+c-allow' if rust else 'allow'
635
        else:
636
            modulepolicy = 'rust+c' if rust else 'c'
637

638 639 640 641 642 643 644
        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)
645 646

        build_py.run(self)
647

648

649 650 651 652 653 654 655 656 657 658 659 660 661
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):
662 663
            with open(self._indexfilename, 'w') as f:
                f.write('# empty\n')
664 665

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

676 677 678
        with open(self._indexfilename, 'wb') as f:
            f.write(b'# this file is autogenerated by setup.py\n')
            f.write(b'docs = ')
679
            f.write(out)
680

681

682 683
class buildhgexe(build_ext):
    description = 'compile hg.exe from mercurial/exewrapper.c'
684
    user_options = build_ext.user_options + [
685 686 687 688 689 690 691
        (
            'long-paths-support',
            None,
            'enable support for long paths on '
            'Windows (off by default and '
            'experimental)',
        ),
692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707
    ]

    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
708 709 710 711

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

716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733
        pythonlib = None

        if getattr(sys, 'dllhandle', None):
            # 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
            )
734

735 736 737 738 739 740 741 742 743
            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]

        if not pythonlib:
744
            log.warn(
745
                'could not determine Python DLL filename; assuming pythonXY'
746
            )
747 748

            hv = sys.hexversion
749
            pythonlib = b'python%d%d' % (hv >> 24, (hv >> 16) & 0xFF)
750 751

        log.info('using %s as Python library name' % pythonlib)
752
        with open('mercurial/hgpythonlib.h', 'wb') as f:
753 754
            f.write(b'/* this file is autogenerated by setup.py */\n')
            f.write(b'#define HGPYTHONLIB "%s"\n' % pythonlib)
755 756 757 758 759

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

760 761 762 763 764
        objects = self.compiler.compile(
            ['mercurial/exewrapper.c'],
            output_dir=self.build_temp,
            macros=macros,
        )
765
        dir = os.path.dirname(self.get_ext_fullpath('dummy'))
766
        self.hgtarget = os.path.join(dir, 'hg')
767 768 769
        self.compiler.link_executable(
            objects, self.hgtarget, libraries=[], output_dir=self.build_temp
        )
770 771 772 773
        if self.long_paths_support:
            self.addlongpathsmanifest()

    def addlongpathsmanifest(self):
774
        r"""Add manifest pieces so that hg.exe understands long paths
775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800

        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
801 802 803 804 805 806 807 808 809 810
        self.spawn(
            [
                'mt.exe',
                '-nologo',
                '-manifest',
                manfname,
                inputresource,
                outputresource,
            ]
        )
811 812
        log.info("done updating hg.exe's manifest")
        os.remove(manfname)
813

814 815 816 817 818
    @property
    def hgexepath(self):
        dir = os.path.dirname(self.get_ext_fullpath('dummy'))
        return os.path.join(self.build_temp, dir, 'hg.exe')

819

820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858
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(
859 860
                [sys.executable, 'gendoc.py', root], os.environ, cwd='doc'
            )
861
            if res:
862 863 864
                raise SystemExit(
                    'error running gendoc.py: %s' % '\n'.join([out, err])
                )
865 866 867 868 869 870 871 872 873 874 875

            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,
876 877
                cwd='doc',
            )
878
            if res:
879 880 881
                raise SystemExit(
                    'error running gendoc: %s' % '\n'.join([out, err])
                )
882 883 884 885 886 887 888

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

        def genman(root):
            log.info('generating doc/%s' % root)
            res, out, err = runcmd(
889 890 891 892 893 894 895 896 897 898 899
                [
                    sys.executable,
                    'runrst',
                    'hgmanpage',
                    '--halt',
                    'warning',
                    '--strip-elements-with-class',
                    'htmlonly',
                    '%s.txt' % root,
                    root,
                ],
900
                os.environ,
901 902
                cwd='doc',
            )
903
            if res:
904 905 906
                raise SystemExit(
                    'error running runrst: %s' % '\n'.join([out, err])
                )
907 908 909 910 911 912

            normalizecrlf('doc/%s' % root)

        def genhtml(root):
            log.info('generating doc/%s.html' % root)
            res, out, err = runcmd(
913 914 915 916 917 918 919 920 921 922 923 924
                [
                    sys.executable,
                    'runrst',
                    'html',
                    '--halt',
                    'warning',
                    '--link-stylesheet',
                    '--stylesheet-path',
                    'style.css',
                    '%s.txt' % root,
                    '%s.html' % root,
                ],
925
                os.environ,
926 927
                cwd='doc',
            )
928
            if res:
929 930 931
                raise SystemExit(
                    'error running runrst: %s' % '\n'.join([out, err])
                )
932 933 934 935

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

        # This logic is duplicated in doc/Makefile.
936 937
        sources = set(
            f
938
            for f in os.listdir('mercurial/helptext')
939 940
            if re.search(r'[0-9]\.txt$', f)
        )
941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956

        # 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)

957

958
class hginstall(install):
959 960

    user_options = install.user_options + [
961 962 963 964 965 966 967 968 969 970
        (
            'old-and-unmanageable',
            None,
            'noop, present for eggless setuptools compat',
        ),
        (
            'single-version-externally-managed',
            None,
            'noop, present for eggless setuptools compat',
        ),
971 972 973 974 975
    ]

    # Also helps setuptools not be sad while we refuse to create eggs.
    single_version_externally_managed = True

976 977 978 979
    def get_sub_commands(self):
        # Screen out egg related commands to prevent egg generation.  But allow
        # mercurial.egg-info generation, since that is part of modern
        # packaging.
980
        excl = set(['bdist_egg'])
981 982
        return filter(lambda x: x not in excl, install.get_sub_commands(self))

983

984 985 986 987 988 989 990 991 992 993 994 995 996 997
class hginstalllib(install_lib):
    '''
    This is a specialization of install_lib that replaces the copy_file used
    there so that it supports setting the mode of files after copying them,
    instead of just preserving the mode that the files originally had.  If your
    system has a umask of something like 027, preserving the permissions when
    copying will lead to a broken install.

    Note that just passing keep_permissions=False to copy_file would be
    insufficient, as it might still be applying a umask.
    '''

    def run(self):
        realcopyfile = file_util.copy_file
998

999 1000 1001 1002 1003 1004 1005 1006
        def copyfileandsetmode(*args, **kwargs):
            src, dst = args[0], args[1]
            dst, copied = realcopyfile(*args, **kwargs)
            if copied:
                st = os.stat(src)
                # Persist executable bit (apply it to group and other if user
                # has it)
                if st[stat.ST_MODE] & stat.S_IXUSR:
1007
                    setmode = int('0755', 8)
1008
                else:
1009 1010 1011 1012
                    setmode = int('0644', 8)
                m = stat.S_IMODE(st[stat.ST_MODE])
                m = (m & ~int('0777', 8)) | setmode
                os.chmod(dst, m)
1013

1014 1015 1016 1017 1018 1019
        file_util.copy_file = copyfileandsetmode
        try:
            install_lib.run(self)
        finally:
            file_util.copy_file = realcopyfile

1020

1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034
class hginstallscripts(install_scripts):
    '''
    This is a specialization of install_scripts that replaces the @LIBDIR@ with
    the configured directory for modules. If possible, the path is made relative
    to the directory for scripts.
    '''

    def initialize_options(self):
        install_scripts.initialize_options(self)

        self.install_lib = None

    def finalize_options(self):
        install_scripts.finalize_options(self)
1035
        self.set_undefined_options('install', ('install_lib', 'install_lib'))
1036 1037 1038 1039

    def run(self):
        install_scripts.run(self)

1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058
        # It only makes sense to replace @LIBDIR@ with the install path if
        # the install path is known. For wheels, the logic below calculates
        # the libdir to be "../..". This is because the internal layout of a
        # wheel archive looks like:
        #
        #   mercurial-3.6.1.data/scripts/hg
        #   mercurial/__init__.py
        #
        # When installing wheels, the subdirectories of the "<pkg>.data"
        # directory are translated to system local paths and files therein
        # are copied in place. The mercurial/* files are installed into the
        # site-packages directory. However, the site-packages directory
        # isn't known until wheel install time. This means we have no clue
        # at wheel generation time what the installed site-packages directory
        # will be. And, wheels don't appear to provide the ability to register
        # custom code to run during wheel installation. This all means that
        # we can't reliably set the libdir in wheels: the default behavior
        # of looking in sys.path must do.

1059 1060 1061 1062
        if (
            os.path.splitdrive(self.install_dir)[0]
            != os.path.splitdrive(self.install_lib)[0]
        ):
1063 1064 1065 1066
            # can't make relative paths from one drive to another, so use an
            # absolute path instead
            libdir = self.install_lib
        else:
1067
            libdir = os.path.relpath(self.install_lib, self.install_dir)
1068 1069

        for outfile in self.outfiles:
1070 1071
            with open(outfile, 'rb') as fp:
                data = fp.read()
1072 1073

            # skip binary files
1074
            if b'\0' in data:
1075 1076
                continue

1077 1078 1079 1080
            # During local installs, the shebang will be rewritten to the final
            # install path. During wheel packaging, the shebang has a special
            # value.
            if data.startswith(b'#!python'):
1081 1082 1083 1084
                log.info(
                    'not rewriting @LIBDIR@ in %s because install path '
                    'not known' % outfile
                )
1085 1086
                continue

1087
            data = data.replace(b'@LIBDIR@', libdir.encode(libdir_escape))
1088 1089
            with open(outfile, 'wb') as fp:
                fp.write(data)
1090

1091

1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125
# virtualenv installs custom distutils/__init__.py and
# distutils/distutils.cfg files which essentially proxy back to the
# "real" distutils in the main Python install. The presence of this
# directory causes py2exe to pick up the "hacked" distutils package
# from the virtualenv and "import distutils" will fail from the py2exe
# build because the "real" distutils files can't be located.
#
# We work around this by monkeypatching the py2exe code finding Python
# modules to replace the found virtualenv distutils modules with the
# original versions via filesystem scanning. This is a bit hacky. But
# it allows us to use virtualenvs for py2exe packaging, which is more
# deterministic and reproducible.
#
# It's worth noting that the common StackOverflow suggestions for this
# problem involve copying the original distutils files into the
# virtualenv or into the staging directory after setup() is invoked.
# The former is very brittle and can easily break setup(). Our hacking
# of the found modules routine has a similar result as copying the files
# manually. But it makes fewer assumptions about how py2exe works and
# is less brittle.

# This only catches virtualenvs made with virtualenv (as opposed to
# venv, which is likely what Python 3 uses).
py2exehacked = py2exeloaded and getattr(sys, 'real_prefix', None) is not None

if py2exehacked:
    from distutils.command.py2exe import py2exe as buildpy2exe
    from py2exe.mf import Module as py2exemodule

    class hgbuildpy2exe(buildpy2exe):
        def find_needed_modules(self, mf, files, modules):
            res = buildpy2exe.find_needed_modules(self, mf, files, modules)

            # Replace virtualenv's distutils modules with the real ones.
1126 1127 1128 1129 1130 1131
            modules = {}
            for k, v in res.modules.items():
                if k != 'distutils' and not k.startswith('distutils.'):
                    modules[k] = v

            res.modules = modules
1132 1133

            import opcode
1134 1135 1136 1137

            distutilsreal = os.path.join(
                os.path.dirname(opcode.__file__), 'distutils'
            )
1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157

            for root, dirs, files in os.walk(distutilsreal):
                for f in sorted(files):
                    if not f.endswith('.py'):
                        continue

                    full = os.path.join(root, f)

                    parents = ['distutils']

                    if root != distutilsreal:
                        rel = os.path.relpath(root, distutilsreal)
                        parents.extend(p for p in rel.split(os.sep))

                    modname = '%s.%s' % ('.'.join(parents), f[:-3])

                    if modname.startswith('distutils.tests.'):
                        continue

                    if modname.endswith('.__init__'):
1158
                        modname = modname[: -len('.__init__')]
1159 1160 1161 1162
                        path = os.path.dirname(full)
                    else:
                        path = None

1163 1164 1165
                    res.modules[modname] = py2exemodule(
                        modname, full, path=path
                    )
1166 1167 1168 1169 1170 1171

            if 'distutils' not in res.modules:
                raise SystemExit('could not find distutils modules')

            return res

1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185

cmdclass = {
    'build': hgbuild,
    'build_doc': hgbuilddoc,
    'build_mo': hgbuildmo,
    'build_ext': hgbuildext,
    'build_py': hgbuildpy,
    'build_scripts': hgbuildscripts,
    'build_hgextindex': buildhgextindex,
    'install': hginstall,
    'install_lib': hginstalllib,
    'install_scripts': hginstallscripts,
    'build_hgexe': buildhgexe,
}
1186

1187 1188 1189
if py2exehacked:
    cmdclass['py2exe'] = hgbuildpy2exe

1190 1191 1192 1193
packages = [
    'mercurial',
    'mercurial.cext',
    'mercurial.cffi',
1194
    'mercurial.defaultrc',
1195 1196
    'mercurial.helptext',
    'mercurial.helptext.internals',
1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221
    'mercurial.hgweb',
    'mercurial.interfaces',
    'mercurial.pure',
    'mercurial.thirdparty',
    'mercurial.thirdparty.attr',
    'mercurial.thirdparty.zope',
    'mercurial.thirdparty.zope.interface',
    'mercurial.utils',
    'mercurial.revlogutils',
    'mercurial.testing',
    'hgext',
    'hgext.convert',
    'hgext.fsmonitor',
    'hgext.fastannotate',
    'hgext.fsmonitor.pywatchman',
    'hgext.highlight',
    'hgext.infinitepush',
    'hgext.largefiles',
    'hgext.lfs',
    'hgext.narrow',
    'hgext.remotefilelog',
    'hgext.zeroconf',
    'hgext3rd',
    'hgdemandimport',
]
1222
if sys.version_info[0] == 2:
1223 1224 1225 1226 1227 1228
    packages.extend(
        [
            'mercurial.thirdparty.concurrent',
            'mercurial.thirdparty.concurrent.futures',
        ]
    )
1229

1230 1231 1232 1233 1234 1235
if 'HG_PY2EXE_EXTRA_INSTALL_PACKAGES' in os.environ:
    # py2exe can't cope with namespace packages very well, so we have to
    # install any hgext3rd.* extensions that we want in the final py2exe
    # image here. This is gross, but you gotta do what you gotta do.
    packages.extend(os.environ['HG_PY2EXE_EXTRA_INSTALL_PACKAGES'].split(' '))

1236 1237 1238 1239 1240
common_depends = [
    'mercurial/bitmanipulation.h',
    'mercurial/compat.h',
    'mercurial/cext/util.h',
]
1241
common_include_dirs = ['mercurial']
1242

1243
osutil_cflags = []
1244 1245
osutil_ldflags = []

Jun Wu committed
1246
# platform specific macros
1247
for plat, func in [('bsd', 'setproctitle')]:
Jun Wu committed
1248
    if re.search(plat, sys.platform) and hasfunction(new_compiler(), func):
1249 1250
        osutil_cflags.append('-DHAVE_%s' % func.upper())

1251
for plat, macro, code in [
1252 1253 1254 1255
    (
        'bsd|darwin',
        'BSD_STATFS',
        '''
1256 1257 1258
     #include <sys/param.h>
     #include <sys/mount.h>
     int main() { struct statfs s; return sizeof(s.f_fstypename); }
1259 1260 1261 1262 1263 1264
     ''',
    ),
    (
        'linux',
        'LINUX_STATFS',
        '''
1265 1266 1267
     #include <linux/magic.h>
     #include <sys/vfs.h>
     int main() { struct statfs s; return sizeof(s.f_type); }
1268 1269
     ''',
    ),
1270 1271 1272 1273
]:
    if re.search(plat, sys.platform) and cancompile(new_compiler(), code):
        osutil_cflags.append('-DHAVE_%s' % macro)

1274 1275 1276
if sys.platform == 'darwin':
    osutil_ldflags += ['-framework', 'ApplicationServices']

1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292
xdiff_srcs = [
    'mercurial/thirdparty/xdiff/xdiffi.c',
    'mercurial/thirdparty/xdiff/xprepare.c',
    'mercurial/thirdparty/xdiff/xutils.c',
]

xdiff_headers = [
    'mercurial/thirdparty/xdiff/xdiff.h',
    'mercurial/thirdparty/xdiff/xdiffi.h',
    'mercurial/thirdparty/xdiff/xinclude.h',
    'mercurial/thirdparty/xdiff/xmacros.h',
    'mercurial/thirdparty/xdiff/xprepare.h',
    'mercurial/thirdparty/xdiff/xtypes.h',
    'mercurial/thirdparty/xdiff/xutils.h',
]

1293

1294 1295 1296
class RustCompilationError(CCompilerError):
    """Exception class for Rust compilation errors."""

1297

1298
class RustExtension(Extension):
1299
    """Base classes for concrete Rust Extension classes.
1300 1301 1302 1303
    """

    rusttargetdir = os.path.join('rust', 'target', 'release')

1304 1305 1306
    def __init__(
        self, mpath, sources, rustlibname, subcrate, py3_features=None, **kw
    ):
1307 1308
        Extension.__init__(self, mpath, sources, **kw)
        srcdir = self.rustsrcdir = os.path.join('rust', subcrate)
1309
        self.py3_features = py3_features
1310 1311 1312 1313 1314 1315 1316 1317

        # adding Rust source and control files to depends so that the extension
        # gets rebuilt if they've changed
        self.depends.append(os.path.join(srcdir, 'Cargo.toml'))
        cargo_lock = os.path.join(srcdir, 'Cargo.lock')
        if os.path.exists(cargo_lock):
            self.depends.append(cargo_lock)
        for dirpath, subdir, fnames in os.walk(os.path.join(srcdir, 'src')):
1318 1319 1320 1321 1322
            self.depends.extend(
                os.path.join(dirpath, fname)
                for fname in fnames
                if os.path.splitext(fname)[1] == '.rs'
            )
1323

1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336
    @staticmethod
    def rustdylibsuffix():
        """Return the suffix for shared libraries produced by rustc.

        See also: https://doc.rust-lang.org/reference/linkage.html
        """
        if sys.platform == 'darwin':
            return '.dylib'
        elif os.name == 'nt':
            return '.dll'
        else:
            return '.so'

1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349
    def rustbuild(self):
        env = os.environ.copy()
        if 'HGTEST_RESTOREENV' in env:
            # Mercurial tests change HOME to a temporary directory,
            # but, if installed with rustup, the Rust toolchain needs
            # HOME to be correct (otherwise the 'no default toolchain'
            # error message is issued and the build fails).
            # This happens currently with test-hghave.t, which does
            # invoke this build.

            # Unix only fix (os.path.expanduser not really reliable if
            # HOME is shadowed like this)
            import pwd
1350

1351 1352
            env['HOME'] = pwd.getpwuid(os.getuid()).pw_dir

1353
        cargocmd = ['cargo', 'rustc', '-vv', '--release']
1354
        if sys.version_info[0] == 3 and self.py3_features is not None:
1355 1356 1357
            cargocmd.extend(
                ('--features', self.py3_features, '--no-default-features')
            )
1358
        cargocmd.append('--')
1359
        if sys.platform == 'darwin':
1360 1361 1362
            cargocmd.extend(
                ("-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup")
            )
1363 1364 1365 1366 1367 1368 1369
        try:
            subprocess.check_call(cargocmd, env=env, cwd=self.rustsrcdir)
        except OSError as exc:
            if exc.errno == errno.ENOENT:
                raise RustCompilationError("Cargo not found")
            elif exc.errno == errno.EACCES:
                raise RustCompilationError(
1370 1371
                    "Cargo found, but permisssion to execute it is denied"
                )
1372 1373 1374 1375 1376
            else:
                raise
        except subprocess.CalledProcessError:
            raise RustCompilationError(
                "Cargo failed. Working directory: %r, "
1377
                "command: %r, environment: %r"
1378 1379 1380
                % (self.rustsrcdir, cargocmd, env)
            )

1381

1382 1383 1384
class RustEnhancedExtension(RustExtension):
    """A C Extension, conditionally enhanced with Rust code.

1385 1386 1387
    If the HGWITHRUSTEXT environment variable is set to something else
    than 'cpython', the Rust sources get compiled and linked within
    the C target shared library object.
1388 1389 1390
    """

    def __init__(self, mpath, sources, rustlibname, subcrate, **kw):
1391 1392 1393
        RustExtension.__init__(
            self, mpath, sources, rustlibname, subcrate, **kw
        )
1394 1395 1396 1397 1398 1399
        if hgrustext != 'direct-ffi':
            return
        self.extra_compile_args.append('-DWITH_RUST')
        self.libraries.append(rustlibname)
        self.library_dirs.append(self.rusttargetdir)

1400 1401 1402 1403
    def rustbuild(self):
        if hgrustext == 'direct-ffi':
            RustExtension.rustbuild(self)

1404