##// END OF EJS Templates
setup: make debug simpler by adding a `__repr__` to `hgcommand`...
marmoute -
r52053:3c5b66d0 default
parent child Browse files
Show More
@@ -1,1825 +1,1828 b''
1 1 #
2 2 # This is the mercurial setup script.
3 3 #
4 4 # 'python setup.py install', or
5 5 # 'python setup.py --help' for more options
6 6 import os
7 7
8 8 # Mercurial can't work on 3.6.0 or 3.6.1 due to a bug in % formatting
9 9 # in bytestrings.
10 10 supportedpy = ','.join(
11 11 [
12 12 '>=3.6.2',
13 13 ]
14 14 )
15 15
16 16 import sys, platform
17 17 import sysconfig
18 18
19 19
20 20 def sysstr(s):
21 21 return s.decode('latin-1')
22 22
23 23
24 24 def eprint(*args, **kwargs):
25 25 kwargs['file'] = sys.stderr
26 26 print(*args, **kwargs)
27 27
28 28
29 29 import ssl
30 30
31 31 # ssl.HAS_TLSv1* are preferred to check support but they were added in Python
32 32 # 3.7. Prior to CPython commit 6e8cda91d92da72800d891b2fc2073ecbc134d98
33 33 # (backported to the 3.7 branch), ssl.PROTOCOL_TLSv1_1 / ssl.PROTOCOL_TLSv1_2
34 34 # were defined only if compiled against a OpenSSL version with TLS 1.1 / 1.2
35 35 # support. At the mentioned commit, they were unconditionally defined.
36 36 _notset = object()
37 37 has_tlsv1_1 = getattr(ssl, 'HAS_TLSv1_1', _notset)
38 38 if has_tlsv1_1 is _notset:
39 39 has_tlsv1_1 = getattr(ssl, 'PROTOCOL_TLSv1_1', _notset) is not _notset
40 40 has_tlsv1_2 = getattr(ssl, 'HAS_TLSv1_2', _notset)
41 41 if has_tlsv1_2 is _notset:
42 42 has_tlsv1_2 = getattr(ssl, 'PROTOCOL_TLSv1_2', _notset) is not _notset
43 43 if not (has_tlsv1_1 or has_tlsv1_2):
44 44 error = """
45 45 The `ssl` module does not advertise support for TLS 1.1 or TLS 1.2.
46 46 Please make sure that your Python installation was compiled against an OpenSSL
47 47 version enabling these features (likely this requires the OpenSSL version to
48 48 be at least 1.0.1).
49 49 """
50 50 print(error, file=sys.stderr)
51 51 sys.exit(1)
52 52
53 53 DYLIB_SUFFIX = sysconfig.get_config_vars()['EXT_SUFFIX']
54 54
55 55 # Solaris Python packaging brain damage
56 56 try:
57 57 import hashlib
58 58
59 59 sha = hashlib.sha1()
60 60 except ImportError:
61 61 try:
62 62 import sha
63 63
64 64 sha.sha # silence unused import warning
65 65 except ImportError:
66 66 raise SystemExit(
67 67 "Couldn't import standard hashlib (incomplete Python install)."
68 68 )
69 69
70 70 try:
71 71 import zlib
72 72
73 73 zlib.compressobj # silence unused import warning
74 74 except ImportError:
75 75 raise SystemExit(
76 76 "Couldn't import standard zlib (incomplete Python install)."
77 77 )
78 78
79 79 # The base IronPython distribution (as of 2.7.1) doesn't support bz2
80 80 isironpython = False
81 81 try:
82 82 isironpython = (
83 83 platform.python_implementation().lower().find("ironpython") != -1
84 84 )
85 85 except AttributeError:
86 86 pass
87 87
88 88 if isironpython:
89 89 sys.stderr.write("warning: IronPython detected (no bz2 support)\n")
90 90 else:
91 91 try:
92 92 import bz2
93 93
94 94 bz2.BZ2Compressor # silence unused import warning
95 95 except ImportError:
96 96 raise SystemExit(
97 97 "Couldn't import standard bz2 (incomplete Python install)."
98 98 )
99 99
100 100 ispypy = "PyPy" in sys.version
101 101
102 102 import ctypes
103 103 import stat, subprocess, time
104 104 import re
105 105 import shutil
106 106 import tempfile
107 107
108 108 # We have issues with setuptools on some platforms and builders. Until
109 109 # those are resolved, setuptools is opt-in except for platforms where
110 110 # we don't have issues.
111 111 issetuptools = os.name == 'nt' or 'FORCE_SETUPTOOLS' in os.environ
112 112 if issetuptools:
113 113 from setuptools import setup
114 114 else:
115 115 try:
116 116 from distutils.core import setup
117 117 except ModuleNotFoundError:
118 118 from setuptools import setup
119 119 from distutils.ccompiler import new_compiler
120 120 from distutils.core import Command, Extension
121 121 from distutils.dist import Distribution
122 122 from distutils.command.build import build
123 123 from distutils.command.build_ext import build_ext
124 124 from distutils.command.build_py import build_py
125 125 from distutils.command.build_scripts import build_scripts
126 126 from distutils.command.install import install
127 127 from distutils.command.install_lib import install_lib
128 128 from distutils.command.install_scripts import install_scripts
129 129 from distutils import log
130 130 from distutils.spawn import spawn, find_executable
131 131 from distutils import file_util
132 132 from distutils.errors import (
133 133 CCompilerError,
134 134 DistutilsError,
135 135 DistutilsExecError,
136 136 )
137 137 from distutils.sysconfig import get_python_inc
138 138
139 139
140 140 def write_if_changed(path, content):
141 141 """Write content to a file iff the content hasn't changed."""
142 142 if os.path.exists(path):
143 143 with open(path, 'rb') as fh:
144 144 current = fh.read()
145 145 else:
146 146 current = b''
147 147
148 148 if current != content:
149 149 with open(path, 'wb') as fh:
150 150 fh.write(content)
151 151
152 152
153 153 scripts = ['hg']
154 154 if os.name == 'nt':
155 155 # We remove hg.bat if we are able to build hg.exe.
156 156 scripts.append('contrib/win32/hg.bat')
157 157
158 158
159 159 def cancompile(cc, code):
160 160 tmpdir = tempfile.mkdtemp(prefix='hg-install-')
161 161 devnull = oldstderr = None
162 162 try:
163 163 fname = os.path.join(tmpdir, 'testcomp.c')
164 164 f = open(fname, 'w')
165 165 f.write(code)
166 166 f.close()
167 167 # Redirect stderr to /dev/null to hide any error messages
168 168 # from the compiler.
169 169 # This will have to be changed if we ever have to check
170 170 # for a function on Windows.
171 171 devnull = open('/dev/null', 'w')
172 172 oldstderr = os.dup(sys.stderr.fileno())
173 173 os.dup2(devnull.fileno(), sys.stderr.fileno())
174 174 objects = cc.compile([fname], output_dir=tmpdir)
175 175 cc.link_executable(objects, os.path.join(tmpdir, "a.out"))
176 176 return True
177 177 except Exception:
178 178 return False
179 179 finally:
180 180 if oldstderr is not None:
181 181 os.dup2(oldstderr, sys.stderr.fileno())
182 182 if devnull is not None:
183 183 devnull.close()
184 184 shutil.rmtree(tmpdir)
185 185
186 186
187 187 # simplified version of distutils.ccompiler.CCompiler.has_function
188 188 # that actually removes its temporary files.
189 189 def hasfunction(cc, funcname):
190 190 code = 'int main(void) { %s(); }\n' % funcname
191 191 return cancompile(cc, code)
192 192
193 193
194 194 def hasheader(cc, headername):
195 195 code = '#include <%s>\nint main(void) { return 0; }\n' % headername
196 196 return cancompile(cc, code)
197 197
198 198
199 199 # py2exe needs to be installed to work
200 200 try:
201 201 import py2exe
202 202
203 203 py2exe.patch_distutils()
204 204 py2exeloaded = True
205 205 # import py2exe's patched Distribution class
206 206 from distutils.core import Distribution
207 207 except ImportError:
208 208 py2exeloaded = False
209 209
210 210
211 211 def runcmd(cmd, env, cwd=None):
212 212 p = subprocess.Popen(
213 213 cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, cwd=cwd
214 214 )
215 215 out, err = p.communicate()
216 216 return p.returncode, out, err
217 217
218 218
219 219 class hgcommand:
220 220 def __init__(self, cmd, env):
221 221 self.cmd = cmd
222 222 self.env = env
223 223
224 def __repr__(self):
225 return f"<hgcommand cmd={self.cmd} env={self.env}>"
226
224 227 def run(self, args):
225 228 cmd = self.cmd + args
226 229 returncode, out, err = runcmd(cmd, self.env)
227 230 err = filterhgerr(err)
228 231 if err:
229 232 print("stderr from '%s':" % (' '.join(cmd)), file=sys.stderr)
230 233 print(err, file=sys.stderr)
231 234 if returncode != 0:
232 235 return b''
233 236 return out
234 237
235 238
236 239 def filterhgerr(err):
237 240 # If root is executing setup.py, but the repository is owned by
238 241 # another user (as in "sudo python setup.py install") we will get
239 242 # trust warnings since the .hg/hgrc file is untrusted. That is
240 243 # fine, we don't want to load it anyway. Python may warn about
241 244 # a missing __init__.py in mercurial/locale, we also ignore that.
242 245 err = [
243 246 e
244 247 for e in err.splitlines()
245 248 if (
246 249 not e.startswith(b'not trusting file')
247 250 and not e.startswith(b'warning: Not importing')
248 251 and not e.startswith(b'obsolete feature not enabled')
249 252 and not e.startswith(b'*** failed to import extension')
250 253 and not e.startswith(b'devel-warn:')
251 254 and not (
252 255 e.startswith(b'(third party extension')
253 256 and e.endswith(b'or newer of Mercurial; disabling)')
254 257 )
255 258 )
256 259 ]
257 260 return b'\n'.join(b' ' + e for e in err)
258 261
259 262
260 263 def findhg():
261 264 """Try to figure out how we should invoke hg for examining the local
262 265 repository contents.
263 266
264 267 Returns an hgcommand object."""
265 268 # By default, prefer the "hg" command in the user's path. This was
266 269 # presumably the hg command that the user used to create this repository.
267 270 #
268 271 # This repository may require extensions or other settings that would not
269 272 # be enabled by running the hg script directly from this local repository.
270 273 hgenv = os.environ.copy()
271 274 # Use HGPLAIN to disable hgrc settings that would change output formatting,
272 275 # and disable localization for the same reasons.
273 276 hgenv['HGPLAIN'] = '1'
274 277 hgenv['LANGUAGE'] = 'C'
275 278 hgcmd = ['hg']
276 279 # Run a simple "hg log" command just to see if using hg from the user's
277 280 # path works and can successfully interact with this repository. Windows
278 281 # gives precedence to hg.exe in the current directory, so fall back to the
279 282 # python invocation of local hg, where pythonXY.dll can always be found.
280 283 check_cmd = ['log', '-r.', '-Ttest']
281 284 attempts = []
282 285
283 286 def attempt(cmd, env):
284 287 try:
285 288 retcode, out, err = runcmd(hgcmd + check_cmd, hgenv)
286 289 res = (True, retcode, out, err)
287 290 if retcode == 0 and not filterhgerr(err):
288 291 return True
289 292 except EnvironmentError as e:
290 293 res = (False, e)
291 294 attempts.append((cmd, res))
292 295 return False
293 296
294 297 if os.name != 'nt' or not os.path.exists("hg.exe"):
295 298 if attempt(hgcmd + check_cmd, hgenv):
296 299 return hgcommand(hgcmd, hgenv)
297 300
298 301 # Fall back to trying the local hg installation.
299 302 hgenv = localhgenv()
300 303 hgcmd = [sys.executable, 'hg']
301 304 if attempt(hgcmd + check_cmd, hgenv):
302 305 return hgcommand(hgcmd, hgenv)
303 306
304 307 eprint("/!\\")
305 308 eprint(r"/!\ Unable to find a working hg binary")
306 309 eprint(r"/!\ Version cannot be extracted from the repository")
307 310 eprint(r"/!\ Re-run the setup once a first version is built")
308 311 eprint(r"/!\ Attempts:")
309 312 for i, e in enumerate(attempts):
310 313 eprint(r"/!\ attempt #%d:" % (i))
311 314 eprint(r"/!\ cmd: ", e[0])
312 315 res = e[1]
313 316 if res[0]:
314 317 eprint(r"/!\ return code:", res[1])
315 318 eprint("/!\\ std output:\n%s" % (res[2].decode()), end="")
316 319 eprint("/!\\ std error:\n%s" % (res[3].decode()), end="")
317 320 else:
318 321 eprint(r"/!\ exception: ", res[1])
319 322 return None
320 323
321 324
322 325 def localhgenv():
323 326 """Get an environment dictionary to use for invoking or importing
324 327 mercurial from the local repository."""
325 328 # Execute hg out of this directory with a custom environment which takes
326 329 # care to not use any hgrc files and do no localization.
327 330 env = {
328 331 'HGMODULEPOLICY': 'py',
329 332 'HGRCPATH': '',
330 333 'LANGUAGE': 'C',
331 334 'PATH': '',
332 335 } # make pypi modules that use os.environ['PATH'] happy
333 336 if 'LD_LIBRARY_PATH' in os.environ:
334 337 env['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH']
335 338 if 'SystemRoot' in os.environ:
336 339 # SystemRoot is required by Windows to load various DLLs. See:
337 340 # https://bugs.python.org/issue13524#msg148850
338 341 env['SystemRoot'] = os.environ['SystemRoot']
339 342 return env
340 343
341 344
342 345 version = ''
343 346
344 347
345 348 def _try_get_version():
346 349 hg = findhg()
347 350 if hg is None:
348 351 return ''
349 352 hgid = None
350 353 numerictags = []
351 354 cmd = ['log', '-r', '.', '--template', '{tags}\n']
352 355 pieces = sysstr(hg.run(cmd)).split()
353 356 numerictags = [t for t in pieces if t[0:1].isdigit()]
354 357 hgid = sysstr(hg.run(['id', '-i'])).strip()
355 358 if hgid.count('+') == 2:
356 359 hgid = hgid.replace("+", ".", 1)
357 360 if not hgid:
358 361 eprint("/!\\")
359 362 eprint(r"/!\ Unable to determine hg version from local repository")
360 363 eprint(r"/!\ Failed to retrieve current revision tags")
361 364 return ''
362 365 if numerictags: # tag(s) found
363 366 version = numerictags[-1]
364 367 if hgid.endswith('+'): # propagate the dirty status to the tag
365 368 version += '+'
366 369 else: # no tag found on the checked out revision
367 370 ltagcmd = ['log', '--rev', 'wdir()', '--template', '{latesttag}']
368 371 ltag = sysstr(hg.run(ltagcmd))
369 372 if not ltag:
370 373 eprint("/!\\")
371 374 eprint(r"/!\ Unable to determine hg version from local repository")
372 375 eprint(
373 376 r"/!\ Failed to retrieve current revision distance to lated tag"
374 377 )
375 378 return ''
376 379 changessincecmd = [
377 380 'log',
378 381 '-T',
379 382 'x\n',
380 383 '-r',
381 384 "only(parents(),'%s')" % ltag,
382 385 ]
383 386 changessince = len(hg.run(changessincecmd).splitlines())
384 387 version = '%s+hg%s.%s' % (ltag, changessince, hgid)
385 388 if version.endswith('+'):
386 389 version = version[:-1] + 'local' + time.strftime('%Y%m%d')
387 390 return version
388 391
389 392
390 393 if os.path.isdir('.hg'):
391 394 version = _try_get_version()
392 395 elif os.path.exists('.hg_archival.txt'):
393 396 kw = dict(
394 397 [[t.strip() for t in l.split(':', 1)] for l in open('.hg_archival.txt')]
395 398 )
396 399 if 'tag' in kw:
397 400 version = kw['tag']
398 401 elif 'latesttag' in kw:
399 402 if 'changessincelatesttag' in kw:
400 403 version = (
401 404 '%(latesttag)s+hg%(changessincelatesttag)s.%(node).12s' % kw
402 405 )
403 406 else:
404 407 version = '%(latesttag)s+hg%(latesttagdistance)s.%(node).12s' % kw
405 408 else:
406 409 version = '0+hg' + kw.get('node', '')[:12]
407 410 elif os.path.exists('mercurial/__version__.py'):
408 411 with open('mercurial/__version__.py') as f:
409 412 data = f.read()
410 413 version = re.search('version = b"(.*)"', data).group(1)
411 414 if not version:
412 415 if os.environ.get("MERCURIAL_SETUP_MAKE_LOCAL") == "1":
413 416 version = "0.0+0"
414 417 eprint("/!\\")
415 418 eprint(r"/!\ Using '0.0+0' as the default version")
416 419 eprint(r"/!\ Re-run make local once that first version is built")
417 420 eprint("/!\\")
418 421 else:
419 422 eprint("/!\\")
420 423 eprint(r"/!\ Could not determine the Mercurial version")
421 424 eprint(r"/!\ You need to build a local version first")
422 425 eprint(r"/!\ Run `make local` and try again")
423 426 eprint("/!\\")
424 427 msg = "Run `make local` first to get a working local version"
425 428 raise SystemExit(msg)
426 429
427 430 versionb = version
428 431 if not isinstance(versionb, bytes):
429 432 versionb = versionb.encode('ascii')
430 433
431 434 write_if_changed(
432 435 'mercurial/__version__.py',
433 436 b''.join(
434 437 [
435 438 b'# this file is autogenerated by setup.py\n'
436 439 b'version = b"%s"\n' % versionb,
437 440 ]
438 441 ),
439 442 )
440 443
441 444
442 445 class hgbuild(build):
443 446 # Insert hgbuildmo first so that files in mercurial/locale/ are found
444 447 # when build_py is run next.
445 448 sub_commands = [('build_mo', None)] + build.sub_commands
446 449
447 450
448 451 class hgbuildmo(build):
449 452
450 453 description = "build translations (.mo files)"
451 454
452 455 def run(self):
453 456 if not find_executable('msgfmt'):
454 457 self.warn(
455 458 "could not find msgfmt executable, no translations "
456 459 "will be built"
457 460 )
458 461 return
459 462
460 463 podir = 'i18n'
461 464 if not os.path.isdir(podir):
462 465 self.warn("could not find %s/ directory" % podir)
463 466 return
464 467
465 468 join = os.path.join
466 469 for po in os.listdir(podir):
467 470 if not po.endswith('.po'):
468 471 continue
469 472 pofile = join(podir, po)
470 473 modir = join('locale', po[:-3], 'LC_MESSAGES')
471 474 mofile = join(modir, 'hg.mo')
472 475 mobuildfile = join('mercurial', mofile)
473 476 cmd = ['msgfmt', '-v', '-o', mobuildfile, pofile]
474 477 if sys.platform != 'sunos5':
475 478 # msgfmt on Solaris does not know about -c
476 479 cmd.append('-c')
477 480 self.mkpath(join('mercurial', modir))
478 481 self.make_file([pofile], mobuildfile, spawn, (cmd,))
479 482
480 483
481 484 class hgdist(Distribution):
482 485 pure = False
483 486 rust = False
484 487 no_rust = False
485 488 cffi = ispypy
486 489
487 490 global_options = Distribution.global_options + [
488 491 ('pure', None, "use pure (slow) Python code instead of C extensions"),
489 492 ('rust', None, "use Rust extensions additionally to C extensions"),
490 493 (
491 494 'no-rust',
492 495 None,
493 496 "do not use Rust extensions additionally to C extensions",
494 497 ),
495 498 ]
496 499
497 500 negative_opt = Distribution.negative_opt.copy()
498 501 boolean_options = ['pure', 'rust', 'no-rust']
499 502 negative_opt['no-rust'] = 'rust'
500 503
501 504 def _set_command_options(self, command_obj, option_dict=None):
502 505 # Not all distutils versions in the wild have boolean_options.
503 506 # This should be cleaned up when we're Python 3 only.
504 507 command_obj.boolean_options = (
505 508 getattr(command_obj, 'boolean_options', []) + self.boolean_options
506 509 )
507 510 return Distribution._set_command_options(
508 511 self, command_obj, option_dict=option_dict
509 512 )
510 513
511 514 def parse_command_line(self):
512 515 ret = Distribution.parse_command_line(self)
513 516 if not (self.rust or self.no_rust):
514 517 hgrustext = os.environ.get('HGWITHRUSTEXT')
515 518 # TODO record it for proper rebuild upon changes
516 519 # (see mercurial/__modulepolicy__.py)
517 520 if hgrustext != 'cpython' and hgrustext is not None:
518 521 if hgrustext:
519 522 msg = 'unknown HGWITHRUSTEXT value: %s' % hgrustext
520 523 print(msg, file=sys.stderr)
521 524 hgrustext = None
522 525 self.rust = hgrustext is not None
523 526 self.no_rust = not self.rust
524 527 return ret
525 528
526 529 def has_ext_modules(self):
527 530 # self.ext_modules is emptied in hgbuildpy.finalize_options which is
528 531 # too late for some cases
529 532 return not self.pure and Distribution.has_ext_modules(self)
530 533
531 534
532 535 # This is ugly as a one-liner. So use a variable.
533 536 buildextnegops = dict(getattr(build_ext, 'negative_options', {}))
534 537 buildextnegops['no-zstd'] = 'zstd'
535 538 buildextnegops['no-rust'] = 'rust'
536 539
537 540
538 541 class hgbuildext(build_ext):
539 542 user_options = build_ext.user_options + [
540 543 ('zstd', None, 'compile zstd bindings [default]'),
541 544 ('no-zstd', None, 'do not compile zstd bindings'),
542 545 (
543 546 'rust',
544 547 None,
545 548 'compile Rust extensions if they are in use '
546 549 '(requires Cargo) [default]',
547 550 ),
548 551 ('no-rust', None, 'do not compile Rust extensions'),
549 552 ]
550 553
551 554 boolean_options = build_ext.boolean_options + ['zstd', 'rust']
552 555 negative_opt = buildextnegops
553 556
554 557 def initialize_options(self):
555 558 self.zstd = True
556 559 self.rust = True
557 560
558 561 return build_ext.initialize_options(self)
559 562
560 563 def finalize_options(self):
561 564 # Unless overridden by the end user, build extensions in parallel.
562 565 # Only influences behavior on Python 3.5+.
563 566 if getattr(self, 'parallel', None) is None:
564 567 self.parallel = True
565 568
566 569 return build_ext.finalize_options(self)
567 570
568 571 def build_extensions(self):
569 572 ruststandalones = [
570 573 e for e in self.extensions if isinstance(e, RustStandaloneExtension)
571 574 ]
572 575 self.extensions = [
573 576 e for e in self.extensions if e not in ruststandalones
574 577 ]
575 578 # Filter out zstd if disabled via argument.
576 579 if not self.zstd:
577 580 self.extensions = [
578 581 e for e in self.extensions if e.name != 'mercurial.zstd'
579 582 ]
580 583
581 584 # Build Rust standalone extensions if it'll be used
582 585 # and its build is not explicitly disabled (for external build
583 586 # as Linux distributions would do)
584 587 if self.distribution.rust and self.rust:
585 588 if not sys.platform.startswith('linux'):
586 589 self.warn(
587 590 "rust extensions have only been tested on Linux "
588 591 "and may not behave correctly on other platforms"
589 592 )
590 593
591 594 for rustext in ruststandalones:
592 595 rustext.build('' if self.inplace else self.build_lib)
593 596
594 597 return build_ext.build_extensions(self)
595 598
596 599 def build_extension(self, ext):
597 600 if (
598 601 self.distribution.rust
599 602 and self.rust
600 603 and isinstance(ext, RustExtension)
601 604 ):
602 605 ext.rustbuild()
603 606 try:
604 607 build_ext.build_extension(self, ext)
605 608 except CCompilerError:
606 609 if not getattr(ext, 'optional', False):
607 610 raise
608 611 log.warn(
609 612 "Failed to build optional extension '%s' (skipping)", ext.name
610 613 )
611 614
612 615
613 616 class hgbuildscripts(build_scripts):
614 617 def run(self):
615 618 if os.name != 'nt' or self.distribution.pure:
616 619 return build_scripts.run(self)
617 620
618 621 exebuilt = False
619 622 try:
620 623 self.run_command('build_hgexe')
621 624 exebuilt = True
622 625 except (DistutilsError, CCompilerError):
623 626 log.warn('failed to build optional hg.exe')
624 627
625 628 if exebuilt:
626 629 # Copying hg.exe to the scripts build directory ensures it is
627 630 # installed by the install_scripts command.
628 631 hgexecommand = self.get_finalized_command('build_hgexe')
629 632 dest = os.path.join(self.build_dir, 'hg.exe')
630 633 self.mkpath(self.build_dir)
631 634 self.copy_file(hgexecommand.hgexepath, dest)
632 635
633 636 # Remove hg.bat because it is redundant with hg.exe.
634 637 self.scripts.remove('contrib/win32/hg.bat')
635 638
636 639 return build_scripts.run(self)
637 640
638 641
639 642 class hgbuildpy(build_py):
640 643 def finalize_options(self):
641 644 build_py.finalize_options(self)
642 645
643 646 if self.distribution.pure:
644 647 self.distribution.ext_modules = []
645 648 elif self.distribution.cffi:
646 649 from mercurial.cffi import (
647 650 bdiffbuild,
648 651 mpatchbuild,
649 652 )
650 653
651 654 exts = [
652 655 mpatchbuild.ffi.distutils_extension(),
653 656 bdiffbuild.ffi.distutils_extension(),
654 657 ]
655 658 # cffi modules go here
656 659 if sys.platform == 'darwin':
657 660 from mercurial.cffi import osutilbuild
658 661
659 662 exts.append(osutilbuild.ffi.distutils_extension())
660 663 self.distribution.ext_modules = exts
661 664 else:
662 665 h = os.path.join(get_python_inc(), 'Python.h')
663 666 if not os.path.exists(h):
664 667 raise SystemExit(
665 668 'Python headers are required to build '
666 669 'Mercurial but weren\'t found in %s' % h
667 670 )
668 671
669 672 def run(self):
670 673 basepath = os.path.join(self.build_lib, 'mercurial')
671 674 self.mkpath(basepath)
672 675
673 676 rust = self.distribution.rust
674 677 if self.distribution.pure:
675 678 modulepolicy = 'py'
676 679 elif self.build_lib == '.':
677 680 # in-place build should run without rebuilding and Rust extensions
678 681 modulepolicy = 'rust+c-allow' if rust else 'allow'
679 682 else:
680 683 modulepolicy = 'rust+c' if rust else 'c'
681 684
682 685 content = b''.join(
683 686 [
684 687 b'# this file is autogenerated by setup.py\n',
685 688 b'modulepolicy = b"%s"\n' % modulepolicy.encode('ascii'),
686 689 ]
687 690 )
688 691 write_if_changed(os.path.join(basepath, '__modulepolicy__.py'), content)
689 692
690 693 build_py.run(self)
691 694
692 695
693 696 class buildhgextindex(Command):
694 697 description = 'generate prebuilt index of hgext (for frozen package)'
695 698 user_options = []
696 699 _indexfilename = 'hgext/__index__.py'
697 700
698 701 def initialize_options(self):
699 702 pass
700 703
701 704 def finalize_options(self):
702 705 pass
703 706
704 707 def run(self):
705 708 if os.path.exists(self._indexfilename):
706 709 with open(self._indexfilename, 'w') as f:
707 710 f.write('# empty\n')
708 711
709 712 # here no extension enabled, disabled() lists up everything
710 713 code = (
711 714 'import pprint; from mercurial import extensions; '
712 715 'ext = extensions.disabled();'
713 716 'ext.pop("__index__", None);'
714 717 'pprint.pprint(ext)'
715 718 )
716 719 returncode, out, err = runcmd(
717 720 [sys.executable, '-c', code], localhgenv()
718 721 )
719 722 if err or returncode != 0:
720 723 raise DistutilsExecError(err)
721 724
722 725 with open(self._indexfilename, 'wb') as f:
723 726 f.write(b'# this file is autogenerated by setup.py\n')
724 727 f.write(b'docs = ')
725 728 f.write(out)
726 729
727 730
728 731 class buildhgexe(build_ext):
729 732 description = 'compile hg.exe from mercurial/exewrapper.c'
730 733
731 734 LONG_PATHS_MANIFEST = """\
732 735 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
733 736 <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
734 737 <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
735 738 <security>
736 739 <requestedPrivileges>
737 740 <requestedExecutionLevel
738 741 level="asInvoker"
739 742 uiAccess="false"
740 743 />
741 744 </requestedPrivileges>
742 745 </security>
743 746 </trustInfo>
744 747 <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
745 748 <application>
746 749 <!-- Windows Vista -->
747 750 <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
748 751 <!-- Windows 7 -->
749 752 <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
750 753 <!-- Windows 8 -->
751 754 <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
752 755 <!-- Windows 8.1 -->
753 756 <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
754 757 <!-- Windows 10 and Windows 11 -->
755 758 <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
756 759 </application>
757 760 </compatibility>
758 761 <application xmlns="urn:schemas-microsoft-com:asm.v3">
759 762 <windowsSettings
760 763 xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
761 764 <ws2:longPathAware>true</ws2:longPathAware>
762 765 </windowsSettings>
763 766 </application>
764 767 <dependency>
765 768 <dependentAssembly>
766 769 <assemblyIdentity type="win32"
767 770 name="Microsoft.Windows.Common-Controls"
768 771 version="6.0.0.0"
769 772 processorArchitecture="*"
770 773 publicKeyToken="6595b64144ccf1df"
771 774 language="*" />
772 775 </dependentAssembly>
773 776 </dependency>
774 777 </assembly>
775 778 """
776 779
777 780 def initialize_options(self):
778 781 build_ext.initialize_options(self)
779 782
780 783 def build_extensions(self):
781 784 if os.name != 'nt':
782 785 return
783 786 if isinstance(self.compiler, HackedMingw32CCompiler):
784 787 self.compiler.compiler_so = self.compiler.compiler # no -mdll
785 788 self.compiler.dll_libraries = [] # no -lmsrvc90
786 789
787 790 pythonlib = None
788 791
789 792 dirname = os.path.dirname(self.get_ext_fullpath('dummy'))
790 793 self.hgtarget = os.path.join(dirname, 'hg')
791 794
792 795 if getattr(sys, 'dllhandle', None):
793 796 # Different Python installs can have different Python library
794 797 # names. e.g. the official CPython distribution uses pythonXY.dll
795 798 # and MinGW uses libpythonX.Y.dll.
796 799 _kernel32 = ctypes.windll.kernel32
797 800 _kernel32.GetModuleFileNameA.argtypes = [
798 801 ctypes.c_void_p,
799 802 ctypes.c_void_p,
800 803 ctypes.c_ulong,
801 804 ]
802 805 _kernel32.GetModuleFileNameA.restype = ctypes.c_ulong
803 806 size = 1000
804 807 buf = ctypes.create_string_buffer(size + 1)
805 808 filelen = _kernel32.GetModuleFileNameA(
806 809 sys.dllhandle, ctypes.byref(buf), size
807 810 )
808 811
809 812 if filelen > 0 and filelen != size:
810 813 dllbasename = os.path.basename(buf.value)
811 814 if not dllbasename.lower().endswith(b'.dll'):
812 815 raise SystemExit(
813 816 'Python DLL does not end with .dll: %s' % dllbasename
814 817 )
815 818 pythonlib = dllbasename[:-4]
816 819
817 820 # Copy the pythonXY.dll next to the binary so that it runs
818 821 # without tampering with PATH.
819 822 dest = os.path.join(
820 823 os.path.dirname(self.hgtarget),
821 824 os.fsdecode(dllbasename),
822 825 )
823 826
824 827 if not os.path.exists(dest):
825 828 shutil.copy(buf.value, dest)
826 829
827 830 # Also overwrite python3.dll so that hgext.git is usable.
828 831 # TODO: also handle the MSYS flavor
829 832 python_x = os.path.join(
830 833 os.path.dirname(os.fsdecode(buf.value)),
831 834 "python3.dll",
832 835 )
833 836
834 837 if os.path.exists(python_x):
835 838 dest = os.path.join(
836 839 os.path.dirname(self.hgtarget),
837 840 os.path.basename(python_x),
838 841 )
839 842
840 843 shutil.copy(python_x, dest)
841 844
842 845 if not pythonlib:
843 846 log.warn(
844 847 'could not determine Python DLL filename; assuming pythonXY'
845 848 )
846 849
847 850 hv = sys.hexversion
848 851 pythonlib = b'python%d%d' % (hv >> 24, (hv >> 16) & 0xFF)
849 852
850 853 log.info('using %s as Python library name' % pythonlib)
851 854 with open('mercurial/hgpythonlib.h', 'wb') as f:
852 855 f.write(b'/* this file is autogenerated by setup.py */\n')
853 856 f.write(b'#define HGPYTHONLIB "%s"\n' % pythonlib)
854 857
855 858 objects = self.compiler.compile(
856 859 ['mercurial/exewrapper.c'],
857 860 output_dir=self.build_temp,
858 861 macros=[('_UNICODE', None), ('UNICODE', None)],
859 862 )
860 863 self.compiler.link_executable(
861 864 objects, self.hgtarget, libraries=[], output_dir=self.build_temp
862 865 )
863 866
864 867 self.addlongpathsmanifest()
865 868
866 869 def addlongpathsmanifest(self):
867 870 """Add manifest pieces so that hg.exe understands long paths
868 871
869 872 Why resource #1 should be used for .exe manifests? I don't know and
870 873 wasn't able to find an explanation for mortals. But it seems to work.
871 874 """
872 875 exefname = self.compiler.executable_filename(self.hgtarget)
873 876 fdauto, manfname = tempfile.mkstemp(suffix='.hg.exe.manifest')
874 877 os.close(fdauto)
875 878 with open(manfname, 'w', encoding="UTF-8") as f:
876 879 f.write(self.LONG_PATHS_MANIFEST)
877 880 log.info("long paths manifest is written to '%s'" % manfname)
878 881 outputresource = '-outputresource:%s;#1' % exefname
879 882 log.info("running mt.exe to update hg.exe's manifest in-place")
880 883
881 884 self.spawn(
882 885 [
883 886 self.compiler.mt,
884 887 '-nologo',
885 888 '-manifest',
886 889 manfname,
887 890 outputresource,
888 891 ]
889 892 )
890 893 log.info("done updating hg.exe's manifest")
891 894 os.remove(manfname)
892 895
893 896 @property
894 897 def hgexepath(self):
895 898 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
896 899 return os.path.join(self.build_temp, dir, 'hg.exe')
897 900
898 901
899 902 class hgbuilddoc(Command):
900 903 description = 'build documentation'
901 904 user_options = [
902 905 ('man', None, 'generate man pages'),
903 906 ('html', None, 'generate html pages'),
904 907 ]
905 908
906 909 def initialize_options(self):
907 910 self.man = None
908 911 self.html = None
909 912
910 913 def finalize_options(self):
911 914 # If --man or --html are set, only generate what we're told to.
912 915 # Otherwise generate everything.
913 916 have_subset = self.man is not None or self.html is not None
914 917
915 918 if have_subset:
916 919 self.man = True if self.man else False
917 920 self.html = True if self.html else False
918 921 else:
919 922 self.man = True
920 923 self.html = True
921 924
922 925 def run(self):
923 926 def normalizecrlf(p):
924 927 with open(p, 'rb') as fh:
925 928 orig = fh.read()
926 929
927 930 if b'\r\n' not in orig:
928 931 return
929 932
930 933 log.info('normalizing %s to LF line endings' % p)
931 934 with open(p, 'wb') as fh:
932 935 fh.write(orig.replace(b'\r\n', b'\n'))
933 936
934 937 def gentxt(root):
935 938 txt = 'doc/%s.txt' % root
936 939 log.info('generating %s' % txt)
937 940 res, out, err = runcmd(
938 941 [sys.executable, 'gendoc.py', root], os.environ, cwd='doc'
939 942 )
940 943 if res:
941 944 raise SystemExit(
942 945 'error running gendoc.py: %s'
943 946 % '\n'.join([sysstr(out), sysstr(err)])
944 947 )
945 948
946 949 with open(txt, 'wb') as fh:
947 950 fh.write(out)
948 951
949 952 def gengendoc(root):
950 953 gendoc = 'doc/%s.gendoc.txt' % root
951 954
952 955 log.info('generating %s' % gendoc)
953 956 res, out, err = runcmd(
954 957 [sys.executable, 'gendoc.py', '%s.gendoc' % root],
955 958 os.environ,
956 959 cwd='doc',
957 960 )
958 961 if res:
959 962 raise SystemExit(
960 963 'error running gendoc: %s'
961 964 % '\n'.join([sysstr(out), sysstr(err)])
962 965 )
963 966
964 967 with open(gendoc, 'wb') as fh:
965 968 fh.write(out)
966 969
967 970 def genman(root):
968 971 log.info('generating doc/%s' % root)
969 972 res, out, err = runcmd(
970 973 [
971 974 sys.executable,
972 975 'runrst',
973 976 'hgmanpage',
974 977 '--halt',
975 978 'warning',
976 979 '--strip-elements-with-class',
977 980 'htmlonly',
978 981 '%s.txt' % root,
979 982 root,
980 983 ],
981 984 os.environ,
982 985 cwd='doc',
983 986 )
984 987 if res:
985 988 raise SystemExit(
986 989 'error running runrst: %s'
987 990 % '\n'.join([sysstr(out), sysstr(err)])
988 991 )
989 992
990 993 normalizecrlf('doc/%s' % root)
991 994
992 995 def genhtml(root):
993 996 log.info('generating doc/%s.html' % root)
994 997 res, out, err = runcmd(
995 998 [
996 999 sys.executable,
997 1000 'runrst',
998 1001 'html',
999 1002 '--halt',
1000 1003 'warning',
1001 1004 '--link-stylesheet',
1002 1005 '--stylesheet-path',
1003 1006 'style.css',
1004 1007 '%s.txt' % root,
1005 1008 '%s.html' % root,
1006 1009 ],
1007 1010 os.environ,
1008 1011 cwd='doc',
1009 1012 )
1010 1013 if res:
1011 1014 raise SystemExit(
1012 1015 'error running runrst: %s'
1013 1016 % '\n'.join([sysstr(out), sysstr(err)])
1014 1017 )
1015 1018
1016 1019 normalizecrlf('doc/%s.html' % root)
1017 1020
1018 1021 # This logic is duplicated in doc/Makefile.
1019 1022 sources = {
1020 1023 f
1021 1024 for f in os.listdir('mercurial/helptext')
1022 1025 if re.search(r'[0-9]\.txt$', f)
1023 1026 }
1024 1027
1025 1028 # common.txt is a one-off.
1026 1029 gentxt('common')
1027 1030
1028 1031 for source in sorted(sources):
1029 1032 assert source[-4:] == '.txt'
1030 1033 root = source[:-4]
1031 1034
1032 1035 gentxt(root)
1033 1036 gengendoc(root)
1034 1037
1035 1038 if self.man:
1036 1039 genman(root)
1037 1040 if self.html:
1038 1041 genhtml(root)
1039 1042
1040 1043
1041 1044 class hginstall(install):
1042 1045
1043 1046 user_options = install.user_options + [
1044 1047 (
1045 1048 'old-and-unmanageable',
1046 1049 None,
1047 1050 'noop, present for eggless setuptools compat',
1048 1051 ),
1049 1052 (
1050 1053 'single-version-externally-managed',
1051 1054 None,
1052 1055 'noop, present for eggless setuptools compat',
1053 1056 ),
1054 1057 ]
1055 1058
1056 1059 sub_commands = install.sub_commands + [
1057 1060 ('install_completion', lambda self: True)
1058 1061 ]
1059 1062
1060 1063 # Also helps setuptools not be sad while we refuse to create eggs.
1061 1064 single_version_externally_managed = True
1062 1065
1063 1066 def get_sub_commands(self):
1064 1067 # Screen out egg related commands to prevent egg generation. But allow
1065 1068 # mercurial.egg-info generation, since that is part of modern
1066 1069 # packaging.
1067 1070 excl = {'bdist_egg'}
1068 1071 return filter(lambda x: x not in excl, install.get_sub_commands(self))
1069 1072
1070 1073
1071 1074 class hginstalllib(install_lib):
1072 1075 """
1073 1076 This is a specialization of install_lib that replaces the copy_file used
1074 1077 there so that it supports setting the mode of files after copying them,
1075 1078 instead of just preserving the mode that the files originally had. If your
1076 1079 system has a umask of something like 027, preserving the permissions when
1077 1080 copying will lead to a broken install.
1078 1081
1079 1082 Note that just passing keep_permissions=False to copy_file would be
1080 1083 insufficient, as it might still be applying a umask.
1081 1084 """
1082 1085
1083 1086 def run(self):
1084 1087 realcopyfile = file_util.copy_file
1085 1088
1086 1089 def copyfileandsetmode(*args, **kwargs):
1087 1090 src, dst = args[0], args[1]
1088 1091 dst, copied = realcopyfile(*args, **kwargs)
1089 1092 if copied:
1090 1093 st = os.stat(src)
1091 1094 # Persist executable bit (apply it to group and other if user
1092 1095 # has it)
1093 1096 if st[stat.ST_MODE] & stat.S_IXUSR:
1094 1097 setmode = int('0755', 8)
1095 1098 else:
1096 1099 setmode = int('0644', 8)
1097 1100 m = stat.S_IMODE(st[stat.ST_MODE])
1098 1101 m = (m & ~int('0777', 8)) | setmode
1099 1102 os.chmod(dst, m)
1100 1103
1101 1104 file_util.copy_file = copyfileandsetmode
1102 1105 try:
1103 1106 install_lib.run(self)
1104 1107 finally:
1105 1108 file_util.copy_file = realcopyfile
1106 1109
1107 1110
1108 1111 class hginstallscripts(install_scripts):
1109 1112 """
1110 1113 This is a specialization of install_scripts that replaces the @LIBDIR@ with
1111 1114 the configured directory for modules. If possible, the path is made relative
1112 1115 to the directory for scripts.
1113 1116 """
1114 1117
1115 1118 def initialize_options(self):
1116 1119 install_scripts.initialize_options(self)
1117 1120
1118 1121 self.install_lib = None
1119 1122
1120 1123 def finalize_options(self):
1121 1124 install_scripts.finalize_options(self)
1122 1125 self.set_undefined_options('install', ('install_lib', 'install_lib'))
1123 1126
1124 1127 def run(self):
1125 1128 install_scripts.run(self)
1126 1129
1127 1130 # It only makes sense to replace @LIBDIR@ with the install path if
1128 1131 # the install path is known. For wheels, the logic below calculates
1129 1132 # the libdir to be "../..". This is because the internal layout of a
1130 1133 # wheel archive looks like:
1131 1134 #
1132 1135 # mercurial-3.6.1.data/scripts/hg
1133 1136 # mercurial/__init__.py
1134 1137 #
1135 1138 # When installing wheels, the subdirectories of the "<pkg>.data"
1136 1139 # directory are translated to system local paths and files therein
1137 1140 # are copied in place. The mercurial/* files are installed into the
1138 1141 # site-packages directory. However, the site-packages directory
1139 1142 # isn't known until wheel install time. This means we have no clue
1140 1143 # at wheel generation time what the installed site-packages directory
1141 1144 # will be. And, wheels don't appear to provide the ability to register
1142 1145 # custom code to run during wheel installation. This all means that
1143 1146 # we can't reliably set the libdir in wheels: the default behavior
1144 1147 # of looking in sys.path must do.
1145 1148
1146 1149 if (
1147 1150 os.path.splitdrive(self.install_dir)[0]
1148 1151 != os.path.splitdrive(self.install_lib)[0]
1149 1152 ):
1150 1153 # can't make relative paths from one drive to another, so use an
1151 1154 # absolute path instead
1152 1155 libdir = self.install_lib
1153 1156 else:
1154 1157 libdir = os.path.relpath(self.install_lib, self.install_dir)
1155 1158
1156 1159 for outfile in self.outfiles:
1157 1160 with open(outfile, 'rb') as fp:
1158 1161 data = fp.read()
1159 1162
1160 1163 # skip binary files
1161 1164 if b'\0' in data:
1162 1165 continue
1163 1166
1164 1167 # During local installs, the shebang will be rewritten to the final
1165 1168 # install path. During wheel packaging, the shebang has a special
1166 1169 # value.
1167 1170 if data.startswith(b'#!python'):
1168 1171 log.info(
1169 1172 'not rewriting @LIBDIR@ in %s because install path '
1170 1173 'not known' % outfile
1171 1174 )
1172 1175 continue
1173 1176
1174 1177 data = data.replace(b'@LIBDIR@', libdir.encode('unicode_escape'))
1175 1178 with open(outfile, 'wb') as fp:
1176 1179 fp.write(data)
1177 1180
1178 1181
1179 1182 class hginstallcompletion(Command):
1180 1183 description = 'Install shell completion'
1181 1184
1182 1185 def initialize_options(self):
1183 1186 self.install_dir = None
1184 1187 self.outputs = []
1185 1188
1186 1189 def finalize_options(self):
1187 1190 self.set_undefined_options(
1188 1191 'install_data', ('install_dir', 'install_dir')
1189 1192 )
1190 1193
1191 1194 def get_outputs(self):
1192 1195 return self.outputs
1193 1196
1194 1197 def run(self):
1195 1198 for src, dir_path, dest in (
1196 1199 (
1197 1200 'bash_completion',
1198 1201 ('share', 'bash-completion', 'completions'),
1199 1202 'hg',
1200 1203 ),
1201 1204 ('zsh_completion', ('share', 'zsh', 'site-functions'), '_hg'),
1202 1205 ):
1203 1206 dir = os.path.join(self.install_dir, *dir_path)
1204 1207 self.mkpath(dir)
1205 1208
1206 1209 dest = os.path.join(dir, dest)
1207 1210 self.outputs.append(dest)
1208 1211 self.copy_file(os.path.join('contrib', src), dest)
1209 1212
1210 1213
1211 1214 # virtualenv installs custom distutils/__init__.py and
1212 1215 # distutils/distutils.cfg files which essentially proxy back to the
1213 1216 # "real" distutils in the main Python install. The presence of this
1214 1217 # directory causes py2exe to pick up the "hacked" distutils package
1215 1218 # from the virtualenv and "import distutils" will fail from the py2exe
1216 1219 # build because the "real" distutils files can't be located.
1217 1220 #
1218 1221 # We work around this by monkeypatching the py2exe code finding Python
1219 1222 # modules to replace the found virtualenv distutils modules with the
1220 1223 # original versions via filesystem scanning. This is a bit hacky. But
1221 1224 # it allows us to use virtualenvs for py2exe packaging, which is more
1222 1225 # deterministic and reproducible.
1223 1226 #
1224 1227 # It's worth noting that the common StackOverflow suggestions for this
1225 1228 # problem involve copying the original distutils files into the
1226 1229 # virtualenv or into the staging directory after setup() is invoked.
1227 1230 # The former is very brittle and can easily break setup(). Our hacking
1228 1231 # of the found modules routine has a similar result as copying the files
1229 1232 # manually. But it makes fewer assumptions about how py2exe works and
1230 1233 # is less brittle.
1231 1234
1232 1235 # This only catches virtualenvs made with virtualenv (as opposed to
1233 1236 # venv, which is likely what Python 3 uses).
1234 1237 py2exehacked = py2exeloaded and getattr(sys, 'real_prefix', None) is not None
1235 1238
1236 1239 if py2exehacked:
1237 1240 from distutils.command.py2exe import py2exe as buildpy2exe
1238 1241 from py2exe.mf import Module as py2exemodule
1239 1242
1240 1243 class hgbuildpy2exe(buildpy2exe):
1241 1244 def find_needed_modules(self, mf, files, modules):
1242 1245 res = buildpy2exe.find_needed_modules(self, mf, files, modules)
1243 1246
1244 1247 # Replace virtualenv's distutils modules with the real ones.
1245 1248 modules = {}
1246 1249 for k, v in res.modules.items():
1247 1250 if k != 'distutils' and not k.startswith('distutils.'):
1248 1251 modules[k] = v
1249 1252
1250 1253 res.modules = modules
1251 1254
1252 1255 import opcode
1253 1256
1254 1257 distutilsreal = os.path.join(
1255 1258 os.path.dirname(opcode.__file__), 'distutils'
1256 1259 )
1257 1260
1258 1261 for root, dirs, files in os.walk(distutilsreal):
1259 1262 for f in sorted(files):
1260 1263 if not f.endswith('.py'):
1261 1264 continue
1262 1265
1263 1266 full = os.path.join(root, f)
1264 1267
1265 1268 parents = ['distutils']
1266 1269
1267 1270 if root != distutilsreal:
1268 1271 rel = os.path.relpath(root, distutilsreal)
1269 1272 parents.extend(p for p in rel.split(os.sep))
1270 1273
1271 1274 modname = '%s.%s' % ('.'.join(parents), f[:-3])
1272 1275
1273 1276 if modname.startswith('distutils.tests.'):
1274 1277 continue
1275 1278
1276 1279 if modname.endswith('.__init__'):
1277 1280 modname = modname[: -len('.__init__')]
1278 1281 path = os.path.dirname(full)
1279 1282 else:
1280 1283 path = None
1281 1284
1282 1285 res.modules[modname] = py2exemodule(
1283 1286 modname, full, path=path
1284 1287 )
1285 1288
1286 1289 if 'distutils' not in res.modules:
1287 1290 raise SystemExit('could not find distutils modules')
1288 1291
1289 1292 return res
1290 1293
1291 1294
1292 1295 cmdclass = {
1293 1296 'build': hgbuild,
1294 1297 'build_doc': hgbuilddoc,
1295 1298 'build_mo': hgbuildmo,
1296 1299 'build_ext': hgbuildext,
1297 1300 'build_py': hgbuildpy,
1298 1301 'build_scripts': hgbuildscripts,
1299 1302 'build_hgextindex': buildhgextindex,
1300 1303 'install': hginstall,
1301 1304 'install_completion': hginstallcompletion,
1302 1305 'install_lib': hginstalllib,
1303 1306 'install_scripts': hginstallscripts,
1304 1307 'build_hgexe': buildhgexe,
1305 1308 }
1306 1309
1307 1310 if py2exehacked:
1308 1311 cmdclass['py2exe'] = hgbuildpy2exe
1309 1312
1310 1313 packages = [
1311 1314 'mercurial',
1312 1315 'mercurial.admin',
1313 1316 'mercurial.cext',
1314 1317 'mercurial.cffi',
1315 1318 'mercurial.defaultrc',
1316 1319 'mercurial.dirstateutils',
1317 1320 'mercurial.helptext',
1318 1321 'mercurial.helptext.internals',
1319 1322 'mercurial.hgweb',
1320 1323 'mercurial.interfaces',
1321 1324 'mercurial.pure',
1322 1325 'mercurial.stabletailgraph',
1323 1326 'mercurial.templates',
1324 1327 'mercurial.thirdparty',
1325 1328 'mercurial.thirdparty.attr',
1326 1329 'mercurial.thirdparty.tomli',
1327 1330 'mercurial.thirdparty.zope',
1328 1331 'mercurial.thirdparty.zope.interface',
1329 1332 'mercurial.upgrade_utils',
1330 1333 'mercurial.utils',
1331 1334 'mercurial.revlogutils',
1332 1335 'mercurial.testing',
1333 1336 'hgext',
1334 1337 'hgext.convert',
1335 1338 'hgext.fsmonitor',
1336 1339 'hgext.fastannotate',
1337 1340 'hgext.fsmonitor.pywatchman',
1338 1341 'hgext.git',
1339 1342 'hgext.highlight',
1340 1343 'hgext.hooklib',
1341 1344 'hgext.largefiles',
1342 1345 'hgext.lfs',
1343 1346 'hgext.narrow',
1344 1347 'hgext.remotefilelog',
1345 1348 'hgext.zeroconf',
1346 1349 'hgext3rd',
1347 1350 'hgdemandimport',
1348 1351 ]
1349 1352
1350 1353 for name in os.listdir(os.path.join('mercurial', 'templates')):
1351 1354 if name != '__pycache__' and os.path.isdir(
1352 1355 os.path.join('mercurial', 'templates', name)
1353 1356 ):
1354 1357 packages.append('mercurial.templates.%s' % name)
1355 1358
1356 1359 if 'HG_PY2EXE_EXTRA_INSTALL_PACKAGES' in os.environ:
1357 1360 # py2exe can't cope with namespace packages very well, so we have to
1358 1361 # install any hgext3rd.* extensions that we want in the final py2exe
1359 1362 # image here. This is gross, but you gotta do what you gotta do.
1360 1363 packages.extend(os.environ['HG_PY2EXE_EXTRA_INSTALL_PACKAGES'].split(' '))
1361 1364
1362 1365 common_depends = [
1363 1366 'mercurial/bitmanipulation.h',
1364 1367 'mercurial/compat.h',
1365 1368 'mercurial/cext/util.h',
1366 1369 ]
1367 1370 common_include_dirs = ['mercurial']
1368 1371
1369 1372 common_cflags = []
1370 1373
1371 1374 # MSVC 2008 still needs declarations at the top of the scope, but Python 3.9
1372 1375 # makes declarations not at the top of a scope in the headers.
1373 1376 if os.name != 'nt' and sys.version_info[1] < 9:
1374 1377 common_cflags = ['-Werror=declaration-after-statement']
1375 1378
1376 1379 osutil_cflags = []
1377 1380 osutil_ldflags = []
1378 1381
1379 1382 # platform specific macros
1380 1383 for plat, func in [('bsd', 'setproctitle')]:
1381 1384 if re.search(plat, sys.platform) and hasfunction(new_compiler(), func):
1382 1385 osutil_cflags.append('-DHAVE_%s' % func.upper())
1383 1386
1384 1387 for plat, macro, code in [
1385 1388 (
1386 1389 'bsd|darwin',
1387 1390 'BSD_STATFS',
1388 1391 '''
1389 1392 #include <sys/param.h>
1390 1393 #include <sys/mount.h>
1391 1394 int main() { struct statfs s; return sizeof(s.f_fstypename); }
1392 1395 ''',
1393 1396 ),
1394 1397 (
1395 1398 'linux',
1396 1399 'LINUX_STATFS',
1397 1400 '''
1398 1401 #include <linux/magic.h>
1399 1402 #include <sys/vfs.h>
1400 1403 int main() { struct statfs s; return sizeof(s.f_type); }
1401 1404 ''',
1402 1405 ),
1403 1406 ]:
1404 1407 if re.search(plat, sys.platform) and cancompile(new_compiler(), code):
1405 1408 osutil_cflags.append('-DHAVE_%s' % macro)
1406 1409
1407 1410 if sys.platform == 'darwin':
1408 1411 osutil_ldflags += ['-framework', 'ApplicationServices']
1409 1412
1410 1413 if sys.platform == 'sunos5':
1411 1414 osutil_ldflags += ['-lsocket']
1412 1415
1413 1416 xdiff_srcs = [
1414 1417 'mercurial/thirdparty/xdiff/xdiffi.c',
1415 1418 'mercurial/thirdparty/xdiff/xprepare.c',
1416 1419 'mercurial/thirdparty/xdiff/xutils.c',
1417 1420 ]
1418 1421
1419 1422 xdiff_headers = [
1420 1423 'mercurial/thirdparty/xdiff/xdiff.h',
1421 1424 'mercurial/thirdparty/xdiff/xdiffi.h',
1422 1425 'mercurial/thirdparty/xdiff/xinclude.h',
1423 1426 'mercurial/thirdparty/xdiff/xmacros.h',
1424 1427 'mercurial/thirdparty/xdiff/xprepare.h',
1425 1428 'mercurial/thirdparty/xdiff/xtypes.h',
1426 1429 'mercurial/thirdparty/xdiff/xutils.h',
1427 1430 ]
1428 1431
1429 1432
1430 1433 class RustCompilationError(CCompilerError):
1431 1434 """Exception class for Rust compilation errors."""
1432 1435
1433 1436
1434 1437 class RustExtension(Extension):
1435 1438 """Base classes for concrete Rust Extension classes."""
1436 1439
1437 1440 rusttargetdir = os.path.join('rust', 'target', 'release')
1438 1441
1439 1442 def __init__(self, mpath, sources, rustlibname, subcrate, **kw):
1440 1443 Extension.__init__(self, mpath, sources, **kw)
1441 1444 srcdir = self.rustsrcdir = os.path.join('rust', subcrate)
1442 1445
1443 1446 # adding Rust source and control files to depends so that the extension
1444 1447 # gets rebuilt if they've changed
1445 1448 self.depends.append(os.path.join(srcdir, 'Cargo.toml'))
1446 1449 cargo_lock = os.path.join(srcdir, 'Cargo.lock')
1447 1450 if os.path.exists(cargo_lock):
1448 1451 self.depends.append(cargo_lock)
1449 1452 for dirpath, subdir, fnames in os.walk(os.path.join(srcdir, 'src')):
1450 1453 self.depends.extend(
1451 1454 os.path.join(dirpath, fname)
1452 1455 for fname in fnames
1453 1456 if os.path.splitext(fname)[1] == '.rs'
1454 1457 )
1455 1458
1456 1459 @staticmethod
1457 1460 def rustdylibsuffix():
1458 1461 """Return the suffix for shared libraries produced by rustc.
1459 1462
1460 1463 See also: https://doc.rust-lang.org/reference/linkage.html
1461 1464 """
1462 1465 if sys.platform == 'darwin':
1463 1466 return '.dylib'
1464 1467 elif os.name == 'nt':
1465 1468 return '.dll'
1466 1469 else:
1467 1470 return '.so'
1468 1471
1469 1472 def rustbuild(self):
1470 1473 env = os.environ.copy()
1471 1474 if 'HGTEST_RESTOREENV' in env:
1472 1475 # Mercurial tests change HOME to a temporary directory,
1473 1476 # but, if installed with rustup, the Rust toolchain needs
1474 1477 # HOME to be correct (otherwise the 'no default toolchain'
1475 1478 # error message is issued and the build fails).
1476 1479 # This happens currently with test-hghave.t, which does
1477 1480 # invoke this build.
1478 1481
1479 1482 # Unix only fix (os.path.expanduser not really reliable if
1480 1483 # HOME is shadowed like this)
1481 1484 import pwd
1482 1485
1483 1486 env['HOME'] = pwd.getpwuid(os.getuid()).pw_dir
1484 1487
1485 1488 cargocmd = ['cargo', 'rustc', '--release']
1486 1489
1487 1490 rust_features = env.get("HG_RUST_FEATURES")
1488 1491 if rust_features:
1489 1492 cargocmd.extend(('--features', rust_features))
1490 1493
1491 1494 cargocmd.append('--')
1492 1495 if sys.platform == 'darwin':
1493 1496 cargocmd.extend(
1494 1497 ("-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup")
1495 1498 )
1496 1499 try:
1497 1500 subprocess.check_call(cargocmd, env=env, cwd=self.rustsrcdir)
1498 1501 except FileNotFoundError:
1499 1502 raise RustCompilationError("Cargo not found")
1500 1503 except PermissionError:
1501 1504 raise RustCompilationError(
1502 1505 "Cargo found, but permission to execute it is denied"
1503 1506 )
1504 1507 except subprocess.CalledProcessError:
1505 1508 raise RustCompilationError(
1506 1509 "Cargo failed. Working directory: %r, "
1507 1510 "command: %r, environment: %r"
1508 1511 % (self.rustsrcdir, cargocmd, env)
1509 1512 )
1510 1513
1511 1514
1512 1515 class RustStandaloneExtension(RustExtension):
1513 1516 def __init__(self, pydottedname, rustcrate, dylibname, **kw):
1514 1517 RustExtension.__init__(
1515 1518 self, pydottedname, [], dylibname, rustcrate, **kw
1516 1519 )
1517 1520 self.dylibname = dylibname
1518 1521
1519 1522 def build(self, target_dir):
1520 1523 self.rustbuild()
1521 1524 target = [target_dir]
1522 1525 target.extend(self.name.split('.'))
1523 1526 target[-1] += DYLIB_SUFFIX
1524 1527 target = os.path.join(*target)
1525 1528 os.makedirs(os.path.dirname(target), exist_ok=True)
1526 1529 shutil.copy2(
1527 1530 os.path.join(
1528 1531 self.rusttargetdir, self.dylibname + self.rustdylibsuffix()
1529 1532 ),
1530 1533 target,
1531 1534 )
1532 1535
1533 1536
1534 1537 extmodules = [
1535 1538 Extension(
1536 1539 'mercurial.cext.base85',
1537 1540 ['mercurial/cext/base85.c'],
1538 1541 include_dirs=common_include_dirs,
1539 1542 extra_compile_args=common_cflags,
1540 1543 depends=common_depends,
1541 1544 ),
1542 1545 Extension(
1543 1546 'mercurial.cext.bdiff',
1544 1547 ['mercurial/bdiff.c', 'mercurial/cext/bdiff.c'] + xdiff_srcs,
1545 1548 include_dirs=common_include_dirs,
1546 1549 extra_compile_args=common_cflags,
1547 1550 depends=common_depends + ['mercurial/bdiff.h'] + xdiff_headers,
1548 1551 ),
1549 1552 Extension(
1550 1553 'mercurial.cext.mpatch',
1551 1554 ['mercurial/mpatch.c', 'mercurial/cext/mpatch.c'],
1552 1555 include_dirs=common_include_dirs,
1553 1556 extra_compile_args=common_cflags,
1554 1557 depends=common_depends,
1555 1558 ),
1556 1559 Extension(
1557 1560 'mercurial.cext.parsers',
1558 1561 [
1559 1562 'mercurial/cext/charencode.c',
1560 1563 'mercurial/cext/dirs.c',
1561 1564 'mercurial/cext/manifest.c',
1562 1565 'mercurial/cext/parsers.c',
1563 1566 'mercurial/cext/pathencode.c',
1564 1567 'mercurial/cext/revlog.c',
1565 1568 ],
1566 1569 include_dirs=common_include_dirs,
1567 1570 extra_compile_args=common_cflags,
1568 1571 depends=common_depends
1569 1572 + [
1570 1573 'mercurial/cext/charencode.h',
1571 1574 'mercurial/cext/revlog.h',
1572 1575 ],
1573 1576 ),
1574 1577 Extension(
1575 1578 'mercurial.cext.osutil',
1576 1579 ['mercurial/cext/osutil.c'],
1577 1580 include_dirs=common_include_dirs,
1578 1581 extra_compile_args=common_cflags + osutil_cflags,
1579 1582 extra_link_args=osutil_ldflags,
1580 1583 depends=common_depends,
1581 1584 ),
1582 1585 Extension(
1583 1586 'mercurial.thirdparty.zope.interface._zope_interface_coptimizations',
1584 1587 [
1585 1588 'mercurial/thirdparty/zope/interface/_zope_interface_coptimizations.c',
1586 1589 ],
1587 1590 extra_compile_args=common_cflags,
1588 1591 ),
1589 1592 Extension(
1590 1593 'mercurial.thirdparty.sha1dc',
1591 1594 [
1592 1595 'mercurial/thirdparty/sha1dc/cext.c',
1593 1596 'mercurial/thirdparty/sha1dc/lib/sha1.c',
1594 1597 'mercurial/thirdparty/sha1dc/lib/ubc_check.c',
1595 1598 ],
1596 1599 extra_compile_args=common_cflags,
1597 1600 ),
1598 1601 Extension(
1599 1602 'hgext.fsmonitor.pywatchman.bser',
1600 1603 ['hgext/fsmonitor/pywatchman/bser.c'],
1601 1604 extra_compile_args=common_cflags,
1602 1605 ),
1603 1606 RustStandaloneExtension(
1604 1607 'mercurial.rustext',
1605 1608 'hg-cpython',
1606 1609 'librusthg',
1607 1610 ),
1608 1611 ]
1609 1612
1610 1613
1611 1614 sys.path.insert(0, 'contrib/python-zstandard')
1612 1615 import setup_zstd
1613 1616
1614 1617 zstd = setup_zstd.get_c_extension(
1615 1618 name='mercurial.zstd', root=os.path.abspath(os.path.dirname(__file__))
1616 1619 )
1617 1620 zstd.extra_compile_args += common_cflags
1618 1621 extmodules.append(zstd)
1619 1622
1620 1623 try:
1621 1624 from distutils import cygwinccompiler
1622 1625
1623 1626 # the -mno-cygwin option has been deprecated for years
1624 1627 mingw32compilerclass = cygwinccompiler.Mingw32CCompiler
1625 1628
1626 1629 class HackedMingw32CCompiler(cygwinccompiler.Mingw32CCompiler):
1627 1630 def __init__(self, *args, **kwargs):
1628 1631 mingw32compilerclass.__init__(self, *args, **kwargs)
1629 1632 for i in 'compiler compiler_so linker_exe linker_so'.split():
1630 1633 try:
1631 1634 getattr(self, i).remove('-mno-cygwin')
1632 1635 except ValueError:
1633 1636 pass
1634 1637
1635 1638 cygwinccompiler.Mingw32CCompiler = HackedMingw32CCompiler
1636 1639 except ImportError:
1637 1640 # the cygwinccompiler package is not available on some Python
1638 1641 # distributions like the ones from the optware project for Synology
1639 1642 # DiskStation boxes
1640 1643 class HackedMingw32CCompiler:
1641 1644 pass
1642 1645
1643 1646
1644 1647 if os.name == 'nt':
1645 1648 # Allow compiler/linker flags to be added to Visual Studio builds. Passing
1646 1649 # extra_link_args to distutils.extensions.Extension() doesn't have any
1647 1650 # effect.
1648 1651 from distutils import msvccompiler
1649 1652
1650 1653 msvccompilerclass = msvccompiler.MSVCCompiler
1651 1654
1652 1655 class HackedMSVCCompiler(msvccompiler.MSVCCompiler):
1653 1656 def initialize(self):
1654 1657 msvccompilerclass.initialize(self)
1655 1658 # "warning LNK4197: export 'func' specified multiple times"
1656 1659 self.ldflags_shared.append('/ignore:4197')
1657 1660 self.ldflags_shared_debug.append('/ignore:4197')
1658 1661
1659 1662 msvccompiler.MSVCCompiler = HackedMSVCCompiler
1660 1663
1661 1664 packagedata = {
1662 1665 'mercurial': [
1663 1666 'configitems.toml',
1664 1667 'locale/*/LC_MESSAGES/hg.mo',
1665 1668 'dummycert.pem',
1666 1669 ],
1667 1670 'mercurial.defaultrc': [
1668 1671 '*.rc',
1669 1672 ],
1670 1673 'mercurial.helptext': [
1671 1674 '*.txt',
1672 1675 ],
1673 1676 'mercurial.helptext.internals': [
1674 1677 '*.txt',
1675 1678 ],
1676 1679 'mercurial.thirdparty.attr': [
1677 1680 '*.pyi',
1678 1681 'py.typed',
1679 1682 ],
1680 1683 }
1681 1684
1682 1685
1683 1686 def ordinarypath(p):
1684 1687 return p and p[0] != '.' and p[-1] != '~'
1685 1688
1686 1689
1687 1690 for root in ('templates',):
1688 1691 for curdir, dirs, files in os.walk(os.path.join('mercurial', root)):
1689 1692 packagename = curdir.replace(os.sep, '.')
1690 1693 packagedata[packagename] = list(filter(ordinarypath, files))
1691 1694
1692 1695 datafiles = []
1693 1696
1694 1697 # distutils expects version to be str/unicode. Converting it to
1695 1698 # unicode on Python 2 still works because it won't contain any
1696 1699 # non-ascii bytes and will be implicitly converted back to bytes
1697 1700 # when operated on.
1698 1701 assert isinstance(version, str)
1699 1702 setupversion = version
1700 1703
1701 1704 extra = {}
1702 1705
1703 1706 py2exepackages = [
1704 1707 'hgdemandimport',
1705 1708 'hgext3rd',
1706 1709 'hgext',
1707 1710 'email',
1708 1711 # implicitly imported per module policy
1709 1712 # (cffi wouldn't be used as a frozen exe)
1710 1713 'mercurial.cext',
1711 1714 #'mercurial.cffi',
1712 1715 'mercurial.pure',
1713 1716 ]
1714 1717
1715 1718 py2exe_includes = []
1716 1719
1717 1720 py2exeexcludes = []
1718 1721 py2exedllexcludes = ['crypt32.dll']
1719 1722
1720 1723 if issetuptools:
1721 1724 extra['python_requires'] = supportedpy
1722 1725
1723 1726 if py2exeloaded:
1724 1727 extra['console'] = [
1725 1728 {
1726 1729 'script': 'hg',
1727 1730 'copyright': 'Copyright (C) 2005-2023 Olivia Mackall and others',
1728 1731 'product_version': version,
1729 1732 }
1730 1733 ]
1731 1734 # Sub command of 'build' because 'py2exe' does not handle sub_commands.
1732 1735 # Need to override hgbuild because it has a private copy of
1733 1736 # build.sub_commands.
1734 1737 hgbuild.sub_commands.insert(0, ('build_hgextindex', None))
1735 1738 # put dlls in sub directory so that they won't pollute PATH
1736 1739 extra['zipfile'] = 'lib/library.zip'
1737 1740
1738 1741 # We allow some configuration to be supplemented via environment
1739 1742 # variables. This is better than setup.cfg files because it allows
1740 1743 # supplementing configs instead of replacing them.
1741 1744 extrapackages = os.environ.get('HG_PY2EXE_EXTRA_PACKAGES')
1742 1745 if extrapackages:
1743 1746 py2exepackages.extend(extrapackages.split(' '))
1744 1747
1745 1748 extra_includes = os.environ.get('HG_PY2EXE_EXTRA_INCLUDES')
1746 1749 if extra_includes:
1747 1750 py2exe_includes.extend(extra_includes.split(' '))
1748 1751
1749 1752 excludes = os.environ.get('HG_PY2EXE_EXTRA_EXCLUDES')
1750 1753 if excludes:
1751 1754 py2exeexcludes.extend(excludes.split(' '))
1752 1755
1753 1756 dllexcludes = os.environ.get('HG_PY2EXE_EXTRA_DLL_EXCLUDES')
1754 1757 if dllexcludes:
1755 1758 py2exedllexcludes.extend(dllexcludes.split(' '))
1756 1759
1757 1760 if os.environ.get('PYOXIDIZER'):
1758 1761 hgbuild.sub_commands.insert(0, ('build_hgextindex', None))
1759 1762
1760 1763 if os.name == 'nt':
1761 1764 # Windows binary file versions for exe/dll files must have the
1762 1765 # form W.X.Y.Z, where W,X,Y,Z are numbers in the range 0..65535
1763 1766 setupversion = setupversion.split(r'+', 1)[0]
1764 1767
1765 1768 setup(
1766 1769 name='mercurial',
1767 1770 version=setupversion,
1768 1771 author='Olivia Mackall and many others',
1769 1772 author_email='mercurial@mercurial-scm.org',
1770 1773 url='https://mercurial-scm.org/',
1771 1774 download_url='https://mercurial-scm.org/release/',
1772 1775 description=(
1773 1776 'Fast scalable distributed SCM (revision control, version '
1774 1777 'control) system'
1775 1778 ),
1776 1779 long_description=(
1777 1780 'Mercurial is a distributed SCM tool written in Python.'
1778 1781 ' It is used by a number of large projects that require'
1779 1782 ' fast, reliable distributed revision control, such as '
1780 1783 'Mozilla.'
1781 1784 ),
1782 1785 license='GNU GPLv2 or any later version',
1783 1786 classifiers=[
1784 1787 'Development Status :: 6 - Mature',
1785 1788 'Environment :: Console',
1786 1789 'Intended Audience :: Developers',
1787 1790 'Intended Audience :: System Administrators',
1788 1791 'License :: OSI Approved :: GNU General Public License (GPL)',
1789 1792 'Natural Language :: Danish',
1790 1793 'Natural Language :: English',
1791 1794 'Natural Language :: German',
1792 1795 'Natural Language :: Italian',
1793 1796 'Natural Language :: Japanese',
1794 1797 'Natural Language :: Portuguese (Brazilian)',
1795 1798 'Operating System :: Microsoft :: Windows',
1796 1799 'Operating System :: OS Independent',
1797 1800 'Operating System :: POSIX',
1798 1801 'Programming Language :: C',
1799 1802 'Programming Language :: Python',
1800 1803 'Topic :: Software Development :: Version Control',
1801 1804 ],
1802 1805 scripts=scripts,
1803 1806 packages=packages,
1804 1807 ext_modules=extmodules,
1805 1808 data_files=datafiles,
1806 1809 package_data=packagedata,
1807 1810 cmdclass=cmdclass,
1808 1811 distclass=hgdist,
1809 1812 options={
1810 1813 'py2exe': {
1811 1814 'bundle_files': 3,
1812 1815 'dll_excludes': py2exedllexcludes,
1813 1816 'includes': py2exe_includes,
1814 1817 'excludes': py2exeexcludes,
1815 1818 'packages': py2exepackages,
1816 1819 },
1817 1820 'bdist_mpkg': {
1818 1821 'zipdist': False,
1819 1822 'license': 'COPYING',
1820 1823 'readme': 'contrib/packaging/macosx/Readme.html',
1821 1824 'welcome': 'contrib/packaging/macosx/Welcome.html',
1822 1825 },
1823 1826 },
1824 **extra
1827 **extra,
1825 1828 )
General Comments 0
You need to be logged in to leave comments. Login now