##// END OF EJS Templates
help: create packages for the help text...
Matt Harbison -
r44031:2e017696 default
parent child Browse files
Show More
1 NO CONTENT: new file 100644
1 NO CONTENT: new file 100644
@@ -1,1119 +1,1119 b''
1 1 # help.py - help data for mercurial
2 2 #
3 3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import itertools
11 11 import os
12 12 import re
13 13 import textwrap
14 14
15 15 from .i18n import (
16 16 _,
17 17 gettext,
18 18 )
19 19 from .pycompat import getattr
20 20 from . import (
21 21 cmdutil,
22 22 encoding,
23 23 error,
24 24 extensions,
25 25 fancyopts,
26 26 filemerge,
27 27 fileset,
28 28 minirst,
29 29 pycompat,
30 30 registrar,
31 31 revset,
32 32 templatefilters,
33 33 templatefuncs,
34 34 templatekw,
35 35 ui as uimod,
36 36 util,
37 37 )
38 38 from .hgweb import webcommands
39 39 from .utils import compression
40 40
41 41 _exclkeywords = {
42 42 b"(ADVANCED)",
43 43 b"(DEPRECATED)",
44 44 b"(EXPERIMENTAL)",
45 45 # i18n: "(ADVANCED)" is a keyword, must be translated consistently
46 46 _(b"(ADVANCED)"),
47 47 # i18n: "(DEPRECATED)" is a keyword, must be translated consistently
48 48 _(b"(DEPRECATED)"),
49 49 # i18n: "(EXPERIMENTAL)" is a keyword, must be translated consistently
50 50 _(b"(EXPERIMENTAL)"),
51 51 }
52 52
53 53 # The order in which command categories will be displayed.
54 54 # Extensions with custom categories should insert them into this list
55 55 # after/before the appropriate item, rather than replacing the list or
56 56 # assuming absolute positions.
57 57 CATEGORY_ORDER = [
58 58 registrar.command.CATEGORY_REPO_CREATION,
59 59 registrar.command.CATEGORY_REMOTE_REPO_MANAGEMENT,
60 60 registrar.command.CATEGORY_COMMITTING,
61 61 registrar.command.CATEGORY_CHANGE_MANAGEMENT,
62 62 registrar.command.CATEGORY_CHANGE_ORGANIZATION,
63 63 registrar.command.CATEGORY_FILE_CONTENTS,
64 64 registrar.command.CATEGORY_CHANGE_NAVIGATION,
65 65 registrar.command.CATEGORY_WORKING_DIRECTORY,
66 66 registrar.command.CATEGORY_IMPORT_EXPORT,
67 67 registrar.command.CATEGORY_MAINTENANCE,
68 68 registrar.command.CATEGORY_HELP,
69 69 registrar.command.CATEGORY_MISC,
70 70 registrar.command.CATEGORY_NONE,
71 71 ]
72 72
73 73 # Human-readable category names. These are translated.
74 74 # Extensions with custom categories should add their names here.
75 75 CATEGORY_NAMES = {
76 76 registrar.command.CATEGORY_REPO_CREATION: b'Repository creation',
77 77 registrar.command.CATEGORY_REMOTE_REPO_MANAGEMENT: b'Remote repository management',
78 78 registrar.command.CATEGORY_COMMITTING: b'Change creation',
79 79 registrar.command.CATEGORY_CHANGE_NAVIGATION: b'Change navigation',
80 80 registrar.command.CATEGORY_CHANGE_MANAGEMENT: b'Change manipulation',
81 81 registrar.command.CATEGORY_CHANGE_ORGANIZATION: b'Change organization',
82 82 registrar.command.CATEGORY_WORKING_DIRECTORY: b'Working directory management',
83 83 registrar.command.CATEGORY_FILE_CONTENTS: b'File content management',
84 84 registrar.command.CATEGORY_IMPORT_EXPORT: b'Change import/export',
85 85 registrar.command.CATEGORY_MAINTENANCE: b'Repository maintenance',
86 86 registrar.command.CATEGORY_HELP: b'Help',
87 87 registrar.command.CATEGORY_MISC: b'Miscellaneous commands',
88 88 registrar.command.CATEGORY_NONE: b'Uncategorized commands',
89 89 }
90 90
91 91 # Topic categories.
92 92 TOPIC_CATEGORY_IDS = b'ids'
93 93 TOPIC_CATEGORY_OUTPUT = b'output'
94 94 TOPIC_CATEGORY_CONFIG = b'config'
95 95 TOPIC_CATEGORY_CONCEPTS = b'concepts'
96 96 TOPIC_CATEGORY_MISC = b'misc'
97 97 TOPIC_CATEGORY_NONE = b'none'
98 98
99 99 # The order in which topic categories will be displayed.
100 100 # Extensions with custom categories should insert them into this list
101 101 # after/before the appropriate item, rather than replacing the list or
102 102 # assuming absolute positions.
103 103 TOPIC_CATEGORY_ORDER = [
104 104 TOPIC_CATEGORY_IDS,
105 105 TOPIC_CATEGORY_OUTPUT,
106 106 TOPIC_CATEGORY_CONFIG,
107 107 TOPIC_CATEGORY_CONCEPTS,
108 108 TOPIC_CATEGORY_MISC,
109 109 TOPIC_CATEGORY_NONE,
110 110 ]
111 111
112 112 # Human-readable topic category names. These are translated.
113 113 TOPIC_CATEGORY_NAMES = {
114 114 TOPIC_CATEGORY_IDS: b'Mercurial identifiers',
115 115 TOPIC_CATEGORY_OUTPUT: b'Mercurial output',
116 116 TOPIC_CATEGORY_CONFIG: b'Mercurial configuration',
117 117 TOPIC_CATEGORY_CONCEPTS: b'Concepts',
118 118 TOPIC_CATEGORY_MISC: b'Miscellaneous',
119 119 TOPIC_CATEGORY_NONE: b'Uncategorized topics',
120 120 }
121 121
122 122
123 123 def listexts(header, exts, indent=1, showdeprecated=False):
124 124 '''return a text listing of the given extensions'''
125 125 rst = []
126 126 if exts:
127 127 for name, desc in sorted(pycompat.iteritems(exts)):
128 128 if not showdeprecated and any(w in desc for w in _exclkeywords):
129 129 continue
130 130 rst.append(b'%s:%s: %s\n' % (b' ' * indent, name, desc))
131 131 if rst:
132 132 rst.insert(0, b'\n%s\n\n' % header)
133 133 return rst
134 134
135 135
136 136 def extshelp(ui):
137 137 rst = loaddoc(b'extensions')(ui).splitlines(True)
138 138 rst.extend(
139 139 listexts(
140 140 _(b'enabled extensions:'), extensions.enabled(), showdeprecated=True
141 141 )
142 142 )
143 143 rst.extend(
144 144 listexts(
145 145 _(b'disabled extensions:'),
146 146 extensions.disabled(),
147 147 showdeprecated=ui.verbose,
148 148 )
149 149 )
150 150 doc = b''.join(rst)
151 151 return doc
152 152
153 153
154 154 def optrst(header, options, verbose):
155 155 data = []
156 156 multioccur = False
157 157 for option in options:
158 158 if len(option) == 5:
159 159 shortopt, longopt, default, desc, optlabel = option
160 160 else:
161 161 shortopt, longopt, default, desc = option
162 162 optlabel = _(b"VALUE") # default label
163 163
164 164 if not verbose and any(w in desc for w in _exclkeywords):
165 165 continue
166 166
167 167 so = b''
168 168 if shortopt:
169 169 so = b'-' + shortopt
170 170 lo = b'--' + longopt
171 171 if default is True:
172 172 lo = b'--[no-]' + longopt
173 173
174 174 if isinstance(default, fancyopts.customopt):
175 175 default = default.getdefaultvalue()
176 176 if default and not callable(default):
177 177 # default is of unknown type, and in Python 2 we abused
178 178 # the %s-shows-repr property to handle integers etc. To
179 179 # match that behavior on Python 3, we do str(default) and
180 180 # then convert it to bytes.
181 181 defaultstr = pycompat.bytestr(default)
182 182 if default is True:
183 183 defaultstr = _(b"on")
184 184 desc += _(b" (default: %s)") % defaultstr
185 185
186 186 if isinstance(default, list):
187 187 lo += b" %s [+]" % optlabel
188 188 multioccur = True
189 189 elif (default is not None) and not isinstance(default, bool):
190 190 lo += b" %s" % optlabel
191 191
192 192 data.append((so, lo, desc))
193 193
194 194 if multioccur:
195 195 header += _(b" ([+] can be repeated)")
196 196
197 197 rst = [b'\n%s:\n\n' % header]
198 198 rst.extend(minirst.maketable(data, 1))
199 199
200 200 return b''.join(rst)
201 201
202 202
203 203 def indicateomitted(rst, omitted, notomitted=None):
204 204 rst.append(b'\n\n.. container:: omitted\n\n %s\n\n' % omitted)
205 205 if notomitted:
206 206 rst.append(b'\n\n.. container:: notomitted\n\n %s\n\n' % notomitted)
207 207
208 208
209 209 def filtercmd(ui, cmd, func, kw, doc):
210 210 if not ui.debugflag and cmd.startswith(b"debug") and kw != b"debug":
211 211 # Debug command, and user is not looking for those.
212 212 return True
213 213 if not ui.verbose:
214 214 if not kw and not doc:
215 215 # Command had no documentation, no point in showing it by default.
216 216 return True
217 217 if getattr(func, 'alias', False) and not getattr(func, 'owndoc', False):
218 218 # Alias didn't have its own documentation.
219 219 return True
220 220 if doc and any(w in doc for w in _exclkeywords):
221 221 # Documentation has excluded keywords.
222 222 return True
223 223 if kw == b"shortlist" and not getattr(func, 'helpbasic', False):
224 224 # We're presenting the short list but the command is not basic.
225 225 return True
226 226 if ui.configbool(b'help', b'hidden-command.%s' % cmd):
227 227 # Configuration explicitly hides the command.
228 228 return True
229 229 return False
230 230
231 231
232 232 def filtertopic(ui, topic):
233 233 return ui.configbool(b'help', b'hidden-topic.%s' % topic, False)
234 234
235 235
236 236 def topicmatch(ui, commands, kw):
237 237 """Return help topics matching kw.
238 238
239 239 Returns {'section': [(name, summary), ...], ...} where section is
240 240 one of topics, commands, extensions, or extensioncommands.
241 241 """
242 242 kw = encoding.lower(kw)
243 243
244 244 def lowercontains(container):
245 245 return kw in encoding.lower(container) # translated in helptable
246 246
247 247 results = {
248 248 b'topics': [],
249 249 b'commands': [],
250 250 b'extensions': [],
251 251 b'extensioncommands': [],
252 252 }
253 253 for topic in helptable:
254 254 names, header, doc = topic[0:3]
255 255 # Old extensions may use a str as doc.
256 256 if (
257 257 sum(map(lowercontains, names))
258 258 or lowercontains(header)
259 259 or (callable(doc) and lowercontains(doc(ui)))
260 260 ):
261 261 name = names[0]
262 262 if not filtertopic(ui, name):
263 263 results[b'topics'].append((names[0], header))
264 264 for cmd, entry in pycompat.iteritems(commands.table):
265 265 if len(entry) == 3:
266 266 summary = entry[2]
267 267 else:
268 268 summary = b''
269 269 # translate docs *before* searching there
270 270 func = entry[0]
271 271 docs = _(pycompat.getdoc(func)) or b''
272 272 if kw in cmd or lowercontains(summary) or lowercontains(docs):
273 273 doclines = docs.splitlines()
274 274 if doclines:
275 275 summary = doclines[0]
276 276 cmdname = cmdutil.parsealiases(cmd)[0]
277 277 if filtercmd(ui, cmdname, func, kw, docs):
278 278 continue
279 279 results[b'commands'].append((cmdname, summary))
280 280 for name, docs in itertools.chain(
281 281 pycompat.iteritems(extensions.enabled(False)),
282 282 pycompat.iteritems(extensions.disabled()),
283 283 ):
284 284 if not docs:
285 285 continue
286 286 name = name.rpartition(b'.')[-1]
287 287 if lowercontains(name) or lowercontains(docs):
288 288 # extension docs are already translated
289 289 results[b'extensions'].append((name, docs.splitlines()[0]))
290 290 try:
291 291 mod = extensions.load(ui, name, b'')
292 292 except ImportError:
293 293 # debug message would be printed in extensions.load()
294 294 continue
295 295 for cmd, entry in pycompat.iteritems(getattr(mod, 'cmdtable', {})):
296 296 if kw in cmd or (len(entry) > 2 and lowercontains(entry[2])):
297 297 cmdname = cmdutil.parsealiases(cmd)[0]
298 298 func = entry[0]
299 299 cmddoc = pycompat.getdoc(func)
300 300 if cmddoc:
301 301 cmddoc = gettext(cmddoc).splitlines()[0]
302 302 else:
303 303 cmddoc = _(b'(no help text available)')
304 304 if filtercmd(ui, cmdname, func, kw, cmddoc):
305 305 continue
306 306 results[b'extensioncommands'].append((cmdname, cmddoc))
307 307 return results
308 308
309 309
310 310 def loaddoc(topic, subdir=None):
311 311 """Return a delayed loader for help/topic.txt."""
312 312
313 313 def loader(ui):
314 docdir = os.path.join(util.datapath, b'help')
314 docdir = os.path.join(util.datapath, b'helptext')
315 315 if subdir:
316 316 docdir = os.path.join(docdir, subdir)
317 317 path = os.path.join(docdir, topic + b".txt")
318 318 doc = gettext(util.readfile(path))
319 319 for rewriter in helphooks.get(topic, []):
320 320 doc = rewriter(ui, topic, doc)
321 321 return doc
322 322
323 323 return loader
324 324
325 325
326 326 internalstable = sorted(
327 327 [
328 328 ([b'bundle2'], _(b'Bundle2'), loaddoc(b'bundle2', subdir=b'internals')),
329 329 ([b'bundles'], _(b'Bundles'), loaddoc(b'bundles', subdir=b'internals')),
330 330 ([b'cbor'], _(b'CBOR'), loaddoc(b'cbor', subdir=b'internals')),
331 331 ([b'censor'], _(b'Censor'), loaddoc(b'censor', subdir=b'internals')),
332 332 (
333 333 [b'changegroups'],
334 334 _(b'Changegroups'),
335 335 loaddoc(b'changegroups', subdir=b'internals'),
336 336 ),
337 337 (
338 338 [b'config'],
339 339 _(b'Config Registrar'),
340 340 loaddoc(b'config', subdir=b'internals'),
341 341 ),
342 342 (
343 343 [b'extensions', b'extension'],
344 344 _(b'Extension API'),
345 345 loaddoc(b'extensions', subdir=b'internals'),
346 346 ),
347 347 (
348 348 [b'mergestate'],
349 349 _(b'Mergestate'),
350 350 loaddoc(b'mergestate', subdir=b'internals'),
351 351 ),
352 352 (
353 353 [b'requirements'],
354 354 _(b'Repository Requirements'),
355 355 loaddoc(b'requirements', subdir=b'internals'),
356 356 ),
357 357 (
358 358 [b'revlogs'],
359 359 _(b'Revision Logs'),
360 360 loaddoc(b'revlogs', subdir=b'internals'),
361 361 ),
362 362 (
363 363 [b'wireprotocol'],
364 364 _(b'Wire Protocol'),
365 365 loaddoc(b'wireprotocol', subdir=b'internals'),
366 366 ),
367 367 (
368 368 [b'wireprotocolrpc'],
369 369 _(b'Wire Protocol RPC'),
370 370 loaddoc(b'wireprotocolrpc', subdir=b'internals'),
371 371 ),
372 372 (
373 373 [b'wireprotocolv2'],
374 374 _(b'Wire Protocol Version 2'),
375 375 loaddoc(b'wireprotocolv2', subdir=b'internals'),
376 376 ),
377 377 ]
378 378 )
379 379
380 380
381 381 def internalshelp(ui):
382 382 """Generate the index for the "internals" topic."""
383 383 lines = [
384 384 b'To access a subtopic, use "hg help internals.{subtopic-name}"\n',
385 385 b'\n',
386 386 ]
387 387 for names, header, doc in internalstable:
388 388 lines.append(b' :%s: %s\n' % (names[0], header))
389 389
390 390 return b''.join(lines)
391 391
392 392
393 393 helptable = sorted(
394 394 [
395 395 (
396 396 [b'bundlespec'],
397 397 _(b"Bundle File Formats"),
398 398 loaddoc(b'bundlespec'),
399 399 TOPIC_CATEGORY_CONCEPTS,
400 400 ),
401 401 (
402 402 [b'color'],
403 403 _(b"Colorizing Outputs"),
404 404 loaddoc(b'color'),
405 405 TOPIC_CATEGORY_OUTPUT,
406 406 ),
407 407 (
408 408 [b"config", b"hgrc"],
409 409 _(b"Configuration Files"),
410 410 loaddoc(b'config'),
411 411 TOPIC_CATEGORY_CONFIG,
412 412 ),
413 413 (
414 414 [b'deprecated'],
415 415 _(b"Deprecated Features"),
416 416 loaddoc(b'deprecated'),
417 417 TOPIC_CATEGORY_MISC,
418 418 ),
419 419 (
420 420 [b"dates"],
421 421 _(b"Date Formats"),
422 422 loaddoc(b'dates'),
423 423 TOPIC_CATEGORY_OUTPUT,
424 424 ),
425 425 (
426 426 [b"flags"],
427 427 _(b"Command-line flags"),
428 428 loaddoc(b'flags'),
429 429 TOPIC_CATEGORY_CONFIG,
430 430 ),
431 431 (
432 432 [b"patterns"],
433 433 _(b"File Name Patterns"),
434 434 loaddoc(b'patterns'),
435 435 TOPIC_CATEGORY_IDS,
436 436 ),
437 437 (
438 438 [b'environment', b'env'],
439 439 _(b'Environment Variables'),
440 440 loaddoc(b'environment'),
441 441 TOPIC_CATEGORY_CONFIG,
442 442 ),
443 443 (
444 444 [
445 445 b'revisions',
446 446 b'revs',
447 447 b'revsets',
448 448 b'revset',
449 449 b'multirevs',
450 450 b'mrevs',
451 451 ],
452 452 _(b'Specifying Revisions'),
453 453 loaddoc(b'revisions'),
454 454 TOPIC_CATEGORY_IDS,
455 455 ),
456 456 (
457 457 [b'filesets', b'fileset'],
458 458 _(b"Specifying File Sets"),
459 459 loaddoc(b'filesets'),
460 460 TOPIC_CATEGORY_IDS,
461 461 ),
462 462 (
463 463 [b'diffs'],
464 464 _(b'Diff Formats'),
465 465 loaddoc(b'diffs'),
466 466 TOPIC_CATEGORY_OUTPUT,
467 467 ),
468 468 (
469 469 [b'merge-tools', b'mergetools', b'mergetool'],
470 470 _(b'Merge Tools'),
471 471 loaddoc(b'merge-tools'),
472 472 TOPIC_CATEGORY_CONFIG,
473 473 ),
474 474 (
475 475 [b'templating', b'templates', b'template', b'style'],
476 476 _(b'Template Usage'),
477 477 loaddoc(b'templates'),
478 478 TOPIC_CATEGORY_OUTPUT,
479 479 ),
480 480 ([b'urls'], _(b'URL Paths'), loaddoc(b'urls'), TOPIC_CATEGORY_IDS),
481 481 (
482 482 [b"extensions"],
483 483 _(b"Using Additional Features"),
484 484 extshelp,
485 485 TOPIC_CATEGORY_CONFIG,
486 486 ),
487 487 (
488 488 [b"subrepos", b"subrepo"],
489 489 _(b"Subrepositories"),
490 490 loaddoc(b'subrepos'),
491 491 TOPIC_CATEGORY_CONCEPTS,
492 492 ),
493 493 (
494 494 [b"hgweb"],
495 495 _(b"Configuring hgweb"),
496 496 loaddoc(b'hgweb'),
497 497 TOPIC_CATEGORY_CONFIG,
498 498 ),
499 499 (
500 500 [b"glossary"],
501 501 _(b"Glossary"),
502 502 loaddoc(b'glossary'),
503 503 TOPIC_CATEGORY_CONCEPTS,
504 504 ),
505 505 (
506 506 [b"hgignore", b"ignore"],
507 507 _(b"Syntax for Mercurial Ignore Files"),
508 508 loaddoc(b'hgignore'),
509 509 TOPIC_CATEGORY_IDS,
510 510 ),
511 511 (
512 512 [b"phases"],
513 513 _(b"Working with Phases"),
514 514 loaddoc(b'phases'),
515 515 TOPIC_CATEGORY_CONCEPTS,
516 516 ),
517 517 (
518 518 [b'scripting'],
519 519 _(b'Using Mercurial from scripts and automation'),
520 520 loaddoc(b'scripting'),
521 521 TOPIC_CATEGORY_MISC,
522 522 ),
523 523 (
524 524 [b'internals'],
525 525 _(b"Technical implementation topics"),
526 526 internalshelp,
527 527 TOPIC_CATEGORY_MISC,
528 528 ),
529 529 (
530 530 [b'pager'],
531 531 _(b"Pager Support"),
532 532 loaddoc(b'pager'),
533 533 TOPIC_CATEGORY_CONFIG,
534 534 ),
535 535 ]
536 536 )
537 537
538 538 # Maps topics with sub-topics to a list of their sub-topics.
539 539 subtopics = {
540 540 b'internals': internalstable,
541 541 }
542 542
543 543 # Map topics to lists of callable taking the current topic help and
544 544 # returning the updated version
545 545 helphooks = {}
546 546
547 547
548 548 def addtopichook(topic, rewriter):
549 549 helphooks.setdefault(topic, []).append(rewriter)
550 550
551 551
552 552 def makeitemsdoc(ui, topic, doc, marker, items, dedent=False):
553 553 """Extract docstring from the items key to function mapping, build a
554 554 single documentation block and use it to overwrite the marker in doc.
555 555 """
556 556 entries = []
557 557 for name in sorted(items):
558 558 text = (pycompat.getdoc(items[name]) or b'').rstrip()
559 559 if not text or not ui.verbose and any(w in text for w in _exclkeywords):
560 560 continue
561 561 text = gettext(text)
562 562 if dedent:
563 563 # Abuse latin1 to use textwrap.dedent() on bytes.
564 564 text = textwrap.dedent(text.decode('latin1')).encode('latin1')
565 565 lines = text.splitlines()
566 566 doclines = [(lines[0])]
567 567 for l in lines[1:]:
568 568 # Stop once we find some Python doctest
569 569 if l.strip().startswith(b'>>>'):
570 570 break
571 571 if dedent:
572 572 doclines.append(l.rstrip())
573 573 else:
574 574 doclines.append(b' ' + l.strip())
575 575 entries.append(b'\n'.join(doclines))
576 576 entries = b'\n\n'.join(entries)
577 577 return doc.replace(marker, entries)
578 578
579 579
580 580 def addtopicsymbols(topic, marker, symbols, dedent=False):
581 581 def add(ui, topic, doc):
582 582 return makeitemsdoc(ui, topic, doc, marker, symbols, dedent=dedent)
583 583
584 584 addtopichook(topic, add)
585 585
586 586
587 587 addtopicsymbols(
588 588 b'bundlespec',
589 589 b'.. bundlecompressionmarker',
590 590 compression.bundlecompressiontopics(),
591 591 )
592 592 addtopicsymbols(b'filesets', b'.. predicatesmarker', fileset.symbols)
593 593 addtopicsymbols(
594 594 b'merge-tools', b'.. internaltoolsmarker', filemerge.internalsdoc
595 595 )
596 596 addtopicsymbols(b'revisions', b'.. predicatesmarker', revset.symbols)
597 597 addtopicsymbols(b'templates', b'.. keywordsmarker', templatekw.keywords)
598 598 addtopicsymbols(b'templates', b'.. filtersmarker', templatefilters.filters)
599 599 addtopicsymbols(b'templates', b'.. functionsmarker', templatefuncs.funcs)
600 600 addtopicsymbols(
601 601 b'hgweb', b'.. webcommandsmarker', webcommands.commands, dedent=True
602 602 )
603 603
604 604
605 605 def inserttweakrc(ui, topic, doc):
606 606 marker = b'.. tweakdefaultsmarker'
607 607 repl = uimod.tweakrc
608 608
609 609 def sub(m):
610 610 lines = [m.group(1) + s for s in repl.splitlines()]
611 611 return b'\n'.join(lines)
612 612
613 613 return re.sub(br'( *)%s' % re.escape(marker), sub, doc)
614 614
615 615
616 616 addtopichook(b'config', inserttweakrc)
617 617
618 618
619 619 def help_(
620 620 ui,
621 621 commands,
622 622 name,
623 623 unknowncmd=False,
624 624 full=True,
625 625 subtopic=None,
626 626 fullname=None,
627 627 **opts
628 628 ):
629 629 '''
630 630 Generate the help for 'name' as unformatted restructured text. If
631 631 'name' is None, describe the commands available.
632 632 '''
633 633
634 634 opts = pycompat.byteskwargs(opts)
635 635
636 636 def helpcmd(name, subtopic=None):
637 637 try:
638 638 aliases, entry = cmdutil.findcmd(
639 639 name, commands.table, strict=unknowncmd
640 640 )
641 641 except error.AmbiguousCommand as inst:
642 642 # py3 fix: except vars can't be used outside the scope of the
643 643 # except block, nor can be used inside a lambda. python issue4617
644 644 prefix = inst.args[0]
645 645 select = lambda c: cmdutil.parsealiases(c)[0].startswith(prefix)
646 646 rst = helplist(select)
647 647 return rst
648 648
649 649 rst = []
650 650
651 651 # check if it's an invalid alias and display its error if it is
652 652 if getattr(entry[0], 'badalias', None):
653 653 rst.append(entry[0].badalias + b'\n')
654 654 if entry[0].unknowncmd:
655 655 try:
656 656 rst.extend(helpextcmd(entry[0].cmdname))
657 657 except error.UnknownCommand:
658 658 pass
659 659 return rst
660 660
661 661 # synopsis
662 662 if len(entry) > 2:
663 663 if entry[2].startswith(b'hg'):
664 664 rst.append(b"%s\n" % entry[2])
665 665 else:
666 666 rst.append(b'hg %s %s\n' % (aliases[0], entry[2]))
667 667 else:
668 668 rst.append(b'hg %s\n' % aliases[0])
669 669 # aliases
670 670 if full and not ui.quiet and len(aliases) > 1:
671 671 rst.append(_(b"\naliases: %s\n") % b', '.join(aliases[1:]))
672 672 rst.append(b'\n')
673 673
674 674 # description
675 675 doc = gettext(pycompat.getdoc(entry[0]))
676 676 if not doc:
677 677 doc = _(b"(no help text available)")
678 678 if util.safehasattr(entry[0], b'definition'): # aliased command
679 679 source = entry[0].source
680 680 if entry[0].definition.startswith(b'!'): # shell alias
681 681 doc = _(b'shell alias for: %s\n\n%s\n\ndefined by: %s\n') % (
682 682 entry[0].definition[1:],
683 683 doc,
684 684 source,
685 685 )
686 686 else:
687 687 doc = _(b'alias for: hg %s\n\n%s\n\ndefined by: %s\n') % (
688 688 entry[0].definition,
689 689 doc,
690 690 source,
691 691 )
692 692 doc = doc.splitlines(True)
693 693 if ui.quiet or not full:
694 694 rst.append(doc[0])
695 695 else:
696 696 rst.extend(doc)
697 697 rst.append(b'\n')
698 698
699 699 # check if this command shadows a non-trivial (multi-line)
700 700 # extension help text
701 701 try:
702 702 mod = extensions.find(name)
703 703 doc = gettext(pycompat.getdoc(mod)) or b''
704 704 if b'\n' in doc.strip():
705 705 msg = _(
706 706 b"(use 'hg help -e %s' to show help for "
707 707 b"the %s extension)"
708 708 ) % (name, name)
709 709 rst.append(b'\n%s\n' % msg)
710 710 except KeyError:
711 711 pass
712 712
713 713 # options
714 714 if not ui.quiet and entry[1]:
715 715 rst.append(optrst(_(b"options"), entry[1], ui.verbose))
716 716
717 717 if ui.verbose:
718 718 rst.append(
719 719 optrst(_(b"global options"), commands.globalopts, ui.verbose)
720 720 )
721 721
722 722 if not ui.verbose:
723 723 if not full:
724 724 rst.append(_(b"\n(use 'hg %s -h' to show more help)\n") % name)
725 725 elif not ui.quiet:
726 726 rst.append(
727 727 _(
728 728 b'\n(some details hidden, use --verbose '
729 729 b'to show complete help)'
730 730 )
731 731 )
732 732
733 733 return rst
734 734
735 735 def helplist(select=None, **opts):
736 736 # Category -> list of commands
737 737 cats = {}
738 738 # Command -> short description
739 739 h = {}
740 740 # Command -> string showing synonyms
741 741 syns = {}
742 742 for c, e in pycompat.iteritems(commands.table):
743 743 fs = cmdutil.parsealiases(c)
744 744 f = fs[0]
745 745 syns[f] = b', '.join(fs)
746 746 func = e[0]
747 747 if select and not select(f):
748 748 continue
749 749 doc = pycompat.getdoc(func)
750 750 if filtercmd(ui, f, func, name, doc):
751 751 continue
752 752 doc = gettext(doc)
753 753 if not doc:
754 754 doc = _(b"(no help text available)")
755 755 h[f] = doc.splitlines()[0].rstrip()
756 756
757 757 cat = getattr(func, 'helpcategory', None) or (
758 758 registrar.command.CATEGORY_NONE
759 759 )
760 760 cats.setdefault(cat, []).append(f)
761 761
762 762 rst = []
763 763 if not h:
764 764 if not ui.quiet:
765 765 rst.append(_(b'no commands defined\n'))
766 766 return rst
767 767
768 768 # Output top header.
769 769 if not ui.quiet:
770 770 if name == b"shortlist":
771 771 rst.append(_(b'basic commands:\n\n'))
772 772 elif name == b"debug":
773 773 rst.append(_(b'debug commands (internal and unsupported):\n\n'))
774 774 else:
775 775 rst.append(_(b'list of commands:\n'))
776 776
777 777 def appendcmds(cmds):
778 778 cmds = sorted(cmds)
779 779 for c in cmds:
780 780 if ui.verbose:
781 781 rst.append(b" :%s: %s\n" % (syns[c], h[c]))
782 782 else:
783 783 rst.append(b' :%s: %s\n' % (c, h[c]))
784 784
785 785 if name in (b'shortlist', b'debug'):
786 786 # List without categories.
787 787 appendcmds(h)
788 788 else:
789 789 # Check that all categories have an order.
790 790 missing_order = set(cats.keys()) - set(CATEGORY_ORDER)
791 791 if missing_order:
792 792 ui.develwarn(
793 793 b'help categories missing from CATEGORY_ORDER: %s'
794 794 % missing_order
795 795 )
796 796
797 797 # List per category.
798 798 for cat in CATEGORY_ORDER:
799 799 catfns = cats.get(cat, [])
800 800 if catfns:
801 801 if len(cats) > 1:
802 802 catname = gettext(CATEGORY_NAMES[cat])
803 803 rst.append(b"\n%s:\n" % catname)
804 804 rst.append(b"\n")
805 805 appendcmds(catfns)
806 806
807 807 ex = opts.get
808 808 anyopts = ex('keyword') or not (ex('command') or ex('extension'))
809 809 if not name and anyopts:
810 810 exts = listexts(
811 811 _(b'enabled extensions:'),
812 812 extensions.enabled(),
813 813 showdeprecated=ui.verbose,
814 814 )
815 815 if exts:
816 816 rst.append(b'\n')
817 817 rst.extend(exts)
818 818
819 819 rst.append(_(b"\nadditional help topics:\n"))
820 820 # Group commands by category.
821 821 topiccats = {}
822 822 for topic in helptable:
823 823 names, header, doc = topic[0:3]
824 824 if len(topic) > 3 and topic[3]:
825 825 category = topic[3]
826 826 else:
827 827 category = TOPIC_CATEGORY_NONE
828 828
829 829 topicname = names[0]
830 830 if not filtertopic(ui, topicname):
831 831 topiccats.setdefault(category, []).append(
832 832 (topicname, header)
833 833 )
834 834
835 835 # Check that all categories have an order.
836 836 missing_order = set(topiccats.keys()) - set(TOPIC_CATEGORY_ORDER)
837 837 if missing_order:
838 838 ui.develwarn(
839 839 b'help categories missing from TOPIC_CATEGORY_ORDER: %s'
840 840 % missing_order
841 841 )
842 842
843 843 # Output topics per category.
844 844 for cat in TOPIC_CATEGORY_ORDER:
845 845 topics = topiccats.get(cat, [])
846 846 if topics:
847 847 if len(topiccats) > 1:
848 848 catname = gettext(TOPIC_CATEGORY_NAMES[cat])
849 849 rst.append(b"\n%s:\n" % catname)
850 850 rst.append(b"\n")
851 851 for t, desc in topics:
852 852 rst.append(b" :%s: %s\n" % (t, desc))
853 853
854 854 if ui.quiet:
855 855 pass
856 856 elif ui.verbose:
857 857 rst.append(
858 858 b'\n%s\n'
859 859 % optrst(_(b"global options"), commands.globalopts, ui.verbose)
860 860 )
861 861 if name == b'shortlist':
862 862 rst.append(
863 863 _(b"\n(use 'hg help' for the full list of commands)\n")
864 864 )
865 865 else:
866 866 if name == b'shortlist':
867 867 rst.append(
868 868 _(
869 869 b"\n(use 'hg help' for the full list of commands "
870 870 b"or 'hg -v' for details)\n"
871 871 )
872 872 )
873 873 elif name and not full:
874 874 rst.append(
875 875 _(b"\n(use 'hg help %s' to show the full help text)\n")
876 876 % name
877 877 )
878 878 elif name and syns and name in syns.keys():
879 879 rst.append(
880 880 _(
881 881 b"\n(use 'hg help -v -e %s' to show built-in "
882 882 b"aliases and global options)\n"
883 883 )
884 884 % name
885 885 )
886 886 else:
887 887 rst.append(
888 888 _(
889 889 b"\n(use 'hg help -v%s' to show built-in aliases "
890 890 b"and global options)\n"
891 891 )
892 892 % (name and b" " + name or b"")
893 893 )
894 894 return rst
895 895
896 896 def helptopic(name, subtopic=None):
897 897 # Look for sub-topic entry first.
898 898 header, doc = None, None
899 899 if subtopic and name in subtopics:
900 900 for names, header, doc in subtopics[name]:
901 901 if subtopic in names:
902 902 break
903 903 if not any(subtopic in s[0] for s in subtopics[name]):
904 904 raise error.UnknownCommand(name)
905 905
906 906 if not header:
907 907 for topic in helptable:
908 908 names, header, doc = topic[0:3]
909 909 if name in names:
910 910 break
911 911 else:
912 912 raise error.UnknownCommand(name)
913 913
914 914 rst = [minirst.section(header)]
915 915
916 916 # description
917 917 if not doc:
918 918 rst.append(b" %s\n" % _(b"(no help text available)"))
919 919 if callable(doc):
920 920 rst += [b" %s\n" % l for l in doc(ui).splitlines()]
921 921
922 922 if not ui.verbose:
923 923 omitted = _(
924 924 b'(some details hidden, use --verbose'
925 925 b' to show complete help)'
926 926 )
927 927 indicateomitted(rst, omitted)
928 928
929 929 try:
930 930 cmdutil.findcmd(name, commands.table)
931 931 rst.append(
932 932 _(b"\nuse 'hg help -c %s' to see help for the %s command\n")
933 933 % (name, name)
934 934 )
935 935 except error.UnknownCommand:
936 936 pass
937 937 return rst
938 938
939 939 def helpext(name, subtopic=None):
940 940 try:
941 941 mod = extensions.find(name)
942 942 doc = gettext(pycompat.getdoc(mod)) or _(b'no help text available')
943 943 except KeyError:
944 944 mod = None
945 945 doc = extensions.disabledext(name)
946 946 if not doc:
947 947 raise error.UnknownCommand(name)
948 948
949 949 if b'\n' not in doc:
950 950 head, tail = doc, b""
951 951 else:
952 952 head, tail = doc.split(b'\n', 1)
953 953 rst = [_(b'%s extension - %s\n\n') % (name.rpartition(b'.')[-1], head)]
954 954 if tail:
955 955 rst.extend(tail.splitlines(True))
956 956 rst.append(b'\n')
957 957
958 958 if not ui.verbose:
959 959 omitted = _(
960 960 b'(some details hidden, use --verbose'
961 961 b' to show complete help)'
962 962 )
963 963 indicateomitted(rst, omitted)
964 964
965 965 if mod:
966 966 try:
967 967 ct = mod.cmdtable
968 968 except AttributeError:
969 969 ct = {}
970 970 modcmds = {c.partition(b'|')[0] for c in ct}
971 971 rst.extend(helplist(modcmds.__contains__))
972 972 else:
973 973 rst.append(
974 974 _(
975 975 b"(use 'hg help extensions' for information on enabling"
976 976 b" extensions)\n"
977 977 )
978 978 )
979 979 return rst
980 980
981 981 def helpextcmd(name, subtopic=None):
982 982 cmd, ext, doc = extensions.disabledcmd(
983 983 ui, name, ui.configbool(b'ui', b'strict')
984 984 )
985 985 doc = doc.splitlines()[0]
986 986
987 987 rst = listexts(
988 988 _(b"'%s' is provided by the following extension:") % cmd,
989 989 {ext: doc},
990 990 indent=4,
991 991 showdeprecated=True,
992 992 )
993 993 rst.append(b'\n')
994 994 rst.append(
995 995 _(
996 996 b"(use 'hg help extensions' for information on enabling "
997 997 b"extensions)\n"
998 998 )
999 999 )
1000 1000 return rst
1001 1001
1002 1002 rst = []
1003 1003 kw = opts.get(b'keyword')
1004 1004 if kw or name is None and any(opts[o] for o in opts):
1005 1005 matches = topicmatch(ui, commands, name or b'')
1006 1006 helpareas = []
1007 1007 if opts.get(b'extension'):
1008 1008 helpareas += [(b'extensions', _(b'Extensions'))]
1009 1009 if opts.get(b'command'):
1010 1010 helpareas += [(b'commands', _(b'Commands'))]
1011 1011 if not helpareas:
1012 1012 helpareas = [
1013 1013 (b'topics', _(b'Topics')),
1014 1014 (b'commands', _(b'Commands')),
1015 1015 (b'extensions', _(b'Extensions')),
1016 1016 (b'extensioncommands', _(b'Extension Commands')),
1017 1017 ]
1018 1018 for t, title in helpareas:
1019 1019 if matches[t]:
1020 1020 rst.append(b'%s:\n\n' % title)
1021 1021 rst.extend(minirst.maketable(sorted(matches[t]), 1))
1022 1022 rst.append(b'\n')
1023 1023 if not rst:
1024 1024 msg = _(b'no matches')
1025 1025 hint = _(b"try 'hg help' for a list of topics")
1026 1026 raise error.Abort(msg, hint=hint)
1027 1027 elif name and name != b'shortlist':
1028 1028 queries = []
1029 1029 if unknowncmd:
1030 1030 queries += [helpextcmd]
1031 1031 if opts.get(b'extension'):
1032 1032 queries += [helpext]
1033 1033 if opts.get(b'command'):
1034 1034 queries += [helpcmd]
1035 1035 if not queries:
1036 1036 queries = (helptopic, helpcmd, helpext, helpextcmd)
1037 1037 for f in queries:
1038 1038 try:
1039 1039 rst = f(name, subtopic)
1040 1040 break
1041 1041 except error.UnknownCommand:
1042 1042 pass
1043 1043 else:
1044 1044 if unknowncmd:
1045 1045 raise error.UnknownCommand(name)
1046 1046 else:
1047 1047 if fullname:
1048 1048 formatname = fullname
1049 1049 else:
1050 1050 formatname = name
1051 1051 if subtopic:
1052 1052 hintname = subtopic
1053 1053 else:
1054 1054 hintname = name
1055 1055 msg = _(b'no such help topic: %s') % formatname
1056 1056 hint = _(b"try 'hg help --keyword %s'") % hintname
1057 1057 raise error.Abort(msg, hint=hint)
1058 1058 else:
1059 1059 # program name
1060 1060 if not ui.quiet:
1061 1061 rst = [_(b"Mercurial Distributed SCM\n"), b'\n']
1062 1062 rst.extend(helplist(None, **pycompat.strkwargs(opts)))
1063 1063
1064 1064 return b''.join(rst)
1065 1065
1066 1066
1067 1067 def formattedhelp(
1068 1068 ui, commands, fullname, keep=None, unknowncmd=False, full=True, **opts
1069 1069 ):
1070 1070 """get help for a given topic (as a dotted name) as rendered rst
1071 1071
1072 1072 Either returns the rendered help text or raises an exception.
1073 1073 """
1074 1074 if keep is None:
1075 1075 keep = []
1076 1076 else:
1077 1077 keep = list(keep) # make a copy so we can mutate this later
1078 1078
1079 1079 # <fullname> := <name>[.<subtopic][.<section>]
1080 1080 name = subtopic = section = None
1081 1081 if fullname is not None:
1082 1082 nameparts = fullname.split(b'.')
1083 1083 name = nameparts.pop(0)
1084 1084 if nameparts and name in subtopics:
1085 1085 subtopic = nameparts.pop(0)
1086 1086 if nameparts:
1087 1087 section = encoding.lower(b'.'.join(nameparts))
1088 1088
1089 1089 textwidth = ui.configint(b'ui', b'textwidth')
1090 1090 termwidth = ui.termwidth() - 2
1091 1091 if textwidth <= 0 or termwidth < textwidth:
1092 1092 textwidth = termwidth
1093 1093 text = help_(
1094 1094 ui,
1095 1095 commands,
1096 1096 name,
1097 1097 fullname=fullname,
1098 1098 subtopic=subtopic,
1099 1099 unknowncmd=unknowncmd,
1100 1100 full=full,
1101 1101 **opts
1102 1102 )
1103 1103
1104 1104 blocks, pruned = minirst.parse(text, keep=keep)
1105 1105 if b'verbose' in pruned:
1106 1106 keep.append(b'omitted')
1107 1107 else:
1108 1108 keep.append(b'notomitted')
1109 1109 blocks, pruned = minirst.parse(text, keep=keep)
1110 1110 if section:
1111 1111 blocks = minirst.filtersections(blocks, section)
1112 1112
1113 1113 # We could have been given a weird ".foo" section without a name
1114 1114 # to look for, or we could have simply failed to found "foo.bar"
1115 1115 # because bar isn't a section of foo
1116 1116 if section and not (blocks and name):
1117 1117 raise error.Abort(_(b"help section not found: %s") % fullname)
1118 1118
1119 1119 return minirst.formatplain(blocks, textwidth)
1 NO CONTENT: file renamed from mercurial/help/bundlespec.txt to mercurial/helptext/bundlespec.txt
1 NO CONTENT: file renamed from mercurial/help/color.txt to mercurial/helptext/color.txt
1 NO CONTENT: file renamed from mercurial/help/common.txt to mercurial/helptext/common.txt
1 NO CONTENT: file renamed from mercurial/help/config.txt to mercurial/helptext/config.txt
1 NO CONTENT: file renamed from mercurial/help/dates.txt to mercurial/helptext/dates.txt
1 NO CONTENT: file renamed from mercurial/help/deprecated.txt to mercurial/helptext/deprecated.txt
1 NO CONTENT: file renamed from mercurial/help/diffs.txt to mercurial/helptext/diffs.txt
1 NO CONTENT: file renamed from mercurial/help/environment.txt to mercurial/helptext/environment.txt
1 NO CONTENT: file renamed from mercurial/help/extensions.txt to mercurial/helptext/extensions.txt
1 NO CONTENT: file renamed from mercurial/help/filesets.txt to mercurial/helptext/filesets.txt
1 NO CONTENT: file renamed from mercurial/help/flags.txt to mercurial/helptext/flags.txt
1 NO CONTENT: file renamed from mercurial/help/glossary.txt to mercurial/helptext/glossary.txt
1 NO CONTENT: file renamed from mercurial/help/hg-ssh.8.txt to mercurial/helptext/hg-ssh.8.txt
1 NO CONTENT: file renamed from mercurial/help/hg.1.txt to mercurial/helptext/hg.1.txt
1 NO CONTENT: file renamed from mercurial/help/hgignore.5.txt to mercurial/helptext/hgignore.5.txt
1 NO CONTENT: file renamed from mercurial/help/hgignore.txt to mercurial/helptext/hgignore.txt
1 NO CONTENT: file renamed from mercurial/help/hgrc.5.txt to mercurial/helptext/hgrc.5.txt
1 NO CONTENT: file renamed from mercurial/help/hgweb.txt to mercurial/helptext/hgweb.txt
1 NO CONTENT: file renamed from mercurial/help/internals/bundle2.txt to mercurial/helptext/internals/bundle2.txt
1 NO CONTENT: file renamed from mercurial/help/internals/bundles.txt to mercurial/helptext/internals/bundles.txt
1 NO CONTENT: file renamed from mercurial/help/internals/cbor.txt to mercurial/helptext/internals/cbor.txt
1 NO CONTENT: file renamed from mercurial/help/internals/censor.txt to mercurial/helptext/internals/censor.txt
1 NO CONTENT: file renamed from mercurial/help/internals/changegroups.txt to mercurial/helptext/internals/changegroups.txt
1 NO CONTENT: file renamed from mercurial/help/internals/config.txt to mercurial/helptext/internals/config.txt
1 NO CONTENT: file renamed from mercurial/help/internals/extensions.txt to mercurial/helptext/internals/extensions.txt
1 NO CONTENT: file renamed from mercurial/help/internals/linelog.txt to mercurial/helptext/internals/linelog.txt
1 NO CONTENT: file renamed from mercurial/help/internals/mergestate.txt to mercurial/helptext/internals/mergestate.txt
1 NO CONTENT: file renamed from mercurial/help/internals/requirements.txt to mercurial/helptext/internals/requirements.txt
1 NO CONTENT: file renamed from mercurial/help/internals/revlogs.txt to mercurial/helptext/internals/revlogs.txt
1 NO CONTENT: file renamed from mercurial/help/internals/wireprotocol.txt to mercurial/helptext/internals/wireprotocol.txt
1 NO CONTENT: file renamed from mercurial/help/internals/wireprotocolrpc.txt to mercurial/helptext/internals/wireprotocolrpc.txt
1 NO CONTENT: file renamed from mercurial/help/internals/wireprotocolv2.txt to mercurial/helptext/internals/wireprotocolv2.txt
1 NO CONTENT: file renamed from mercurial/help/merge-tools.txt to mercurial/helptext/merge-tools.txt
1 NO CONTENT: file renamed from mercurial/help/pager.txt to mercurial/helptext/pager.txt
1 NO CONTENT: file renamed from mercurial/help/patterns.txt to mercurial/helptext/patterns.txt
1 NO CONTENT: file renamed from mercurial/help/phases.txt to mercurial/helptext/phases.txt
1 NO CONTENT: file renamed from mercurial/help/revisions.txt to mercurial/helptext/revisions.txt
1 NO CONTENT: file renamed from mercurial/help/scripting.txt to mercurial/helptext/scripting.txt
1 NO CONTENT: file renamed from mercurial/help/subrepos.txt to mercurial/helptext/subrepos.txt
1 NO CONTENT: file renamed from mercurial/help/templates.txt to mercurial/helptext/templates.txt
1 NO CONTENT: file renamed from mercurial/help/urls.txt to mercurial/helptext/urls.txt
@@ -1,1716 +1,1718 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
7 7 import os
8 8
9 9 # Mercurial will never work on Python 3 before 3.5 due to a lack
10 10 # of % formatting on bytestrings, and can't work on 3.6.0 or 3.6.1
11 11 # due to a bug in % formatting in bytestrings.
12 12 # We cannot support Python 3.5.0, 3.5.1, 3.5.2 because of bug in
13 13 # codecs.escape_encode() where it raises SystemError on empty bytestring
14 14 # bug link: https://bugs.python.org/issue25270
15 15 supportedpy = ','.join(
16 16 [
17 17 '>=2.7',
18 18 '!=3.0.*',
19 19 '!=3.1.*',
20 20 '!=3.2.*',
21 21 '!=3.3.*',
22 22 '!=3.4.*',
23 23 '!=3.5.0',
24 24 '!=3.5.1',
25 25 '!=3.5.2',
26 26 '!=3.6.0',
27 27 '!=3.6.1',
28 28 ]
29 29 )
30 30
31 31 import sys, platform
32 32 import sysconfig
33 33
34 34 if sys.version_info[0] >= 3:
35 35 printf = eval('print')
36 36 libdir_escape = 'unicode_escape'
37 37
38 38 def sysstr(s):
39 39 return s.decode('latin-1')
40 40
41 41
42 42 else:
43 43 libdir_escape = 'string_escape'
44 44
45 45 def printf(*args, **kwargs):
46 46 f = kwargs.get('file', sys.stdout)
47 47 end = kwargs.get('end', '\n')
48 48 f.write(b' '.join(args) + end)
49 49
50 50 def sysstr(s):
51 51 return s
52 52
53 53
54 54 # Attempt to guide users to a modern pip - this means that 2.6 users
55 55 # should have a chance of getting a 4.2 release, and when we ratchet
56 56 # the version requirement forward again hopefully everyone will get
57 57 # something that works for them.
58 58 if sys.version_info < (2, 7, 0, 'final'):
59 59 pip_message = (
60 60 'This may be due to an out of date pip. '
61 61 'Make sure you have pip >= 9.0.1.'
62 62 )
63 63 try:
64 64 import pip
65 65
66 66 pip_version = tuple([int(x) for x in pip.__version__.split('.')[:3]])
67 67 if pip_version < (9, 0, 1):
68 68 pip_message = (
69 69 'Your pip version is out of date, please install '
70 70 'pip >= 9.0.1. pip {} detected.'.format(pip.__version__)
71 71 )
72 72 else:
73 73 # pip is new enough - it must be something else
74 74 pip_message = ''
75 75 except Exception:
76 76 pass
77 77 error = """
78 78 Mercurial does not support Python older than 2.7.
79 79 Python {py} detected.
80 80 {pip}
81 81 """.format(
82 82 py=sys.version_info, pip=pip_message
83 83 )
84 84 printf(error, file=sys.stderr)
85 85 sys.exit(1)
86 86
87 87 if sys.version_info[0] >= 3:
88 88 DYLIB_SUFFIX = sysconfig.get_config_vars()['EXT_SUFFIX']
89 89 else:
90 90 # deprecated in Python 3
91 91 DYLIB_SUFFIX = sysconfig.get_config_vars()['SO']
92 92
93 93 # Solaris Python packaging brain damage
94 94 try:
95 95 import hashlib
96 96
97 97 sha = hashlib.sha1()
98 98 except ImportError:
99 99 try:
100 100 import sha
101 101
102 102 sha.sha # silence unused import warning
103 103 except ImportError:
104 104 raise SystemExit(
105 105 "Couldn't import standard hashlib (incomplete Python install)."
106 106 )
107 107
108 108 try:
109 109 import zlib
110 110
111 111 zlib.compressobj # silence unused import warning
112 112 except ImportError:
113 113 raise SystemExit(
114 114 "Couldn't import standard zlib (incomplete Python install)."
115 115 )
116 116
117 117 # The base IronPython distribution (as of 2.7.1) doesn't support bz2
118 118 isironpython = False
119 119 try:
120 120 isironpython = (
121 121 platform.python_implementation().lower().find("ironpython") != -1
122 122 )
123 123 except AttributeError:
124 124 pass
125 125
126 126 if isironpython:
127 127 sys.stderr.write("warning: IronPython detected (no bz2 support)\n")
128 128 else:
129 129 try:
130 130 import bz2
131 131
132 132 bz2.BZ2Compressor # silence unused import warning
133 133 except ImportError:
134 134 raise SystemExit(
135 135 "Couldn't import standard bz2 (incomplete Python install)."
136 136 )
137 137
138 138 ispypy = "PyPy" in sys.version
139 139
140 140 hgrustext = os.environ.get('HGWITHRUSTEXT')
141 141 # TODO record it for proper rebuild upon changes
142 142 # (see mercurial/__modulepolicy__.py)
143 143 if hgrustext != 'cpython' and hgrustext is not None:
144 144 hgrustext = 'direct-ffi'
145 145
146 146 import ctypes
147 147 import errno
148 148 import stat, subprocess, time
149 149 import re
150 150 import shutil
151 151 import tempfile
152 152 from distutils import log
153 153
154 154 # We have issues with setuptools on some platforms and builders. Until
155 155 # those are resolved, setuptools is opt-in except for platforms where
156 156 # we don't have issues.
157 157 issetuptools = os.name == 'nt' or 'FORCE_SETUPTOOLS' in os.environ
158 158 if issetuptools:
159 159 from setuptools import setup
160 160 else:
161 161 from distutils.core import setup
162 162 from distutils.ccompiler import new_compiler
163 163 from distutils.core import Command, Extension
164 164 from distutils.dist import Distribution
165 165 from distutils.command.build import build
166 166 from distutils.command.build_ext import build_ext
167 167 from distutils.command.build_py import build_py
168 168 from distutils.command.build_scripts import build_scripts
169 169 from distutils.command.install import install
170 170 from distutils.command.install_lib import install_lib
171 171 from distutils.command.install_scripts import install_scripts
172 172 from distutils.spawn import spawn, find_executable
173 173 from distutils import file_util
174 174 from distutils.errors import (
175 175 CCompilerError,
176 176 DistutilsError,
177 177 DistutilsExecError,
178 178 )
179 179 from distutils.sysconfig import get_python_inc, get_config_var
180 180 from distutils.version import StrictVersion
181 181
182 182 # Explain to distutils.StrictVersion how our release candidates are versionned
183 183 StrictVersion.version_re = re.compile(r'^(\d+)\.(\d+)(\.(\d+))?-?(rc(\d+))?$')
184 184
185 185
186 186 def write_if_changed(path, content):
187 187 """Write content to a file iff the content hasn't changed."""
188 188 if os.path.exists(path):
189 189 with open(path, 'rb') as fh:
190 190 current = fh.read()
191 191 else:
192 192 current = b''
193 193
194 194 if current != content:
195 195 with open(path, 'wb') as fh:
196 196 fh.write(content)
197 197
198 198
199 199 scripts = ['hg']
200 200 if os.name == 'nt':
201 201 # We remove hg.bat if we are able to build hg.exe.
202 202 scripts.append('contrib/win32/hg.bat')
203 203
204 204
205 205 def cancompile(cc, code):
206 206 tmpdir = tempfile.mkdtemp(prefix='hg-install-')
207 207 devnull = oldstderr = None
208 208 try:
209 209 fname = os.path.join(tmpdir, 'testcomp.c')
210 210 f = open(fname, 'w')
211 211 f.write(code)
212 212 f.close()
213 213 # Redirect stderr to /dev/null to hide any error messages
214 214 # from the compiler.
215 215 # This will have to be changed if we ever have to check
216 216 # for a function on Windows.
217 217 devnull = open('/dev/null', 'w')
218 218 oldstderr = os.dup(sys.stderr.fileno())
219 219 os.dup2(devnull.fileno(), sys.stderr.fileno())
220 220 objects = cc.compile([fname], output_dir=tmpdir)
221 221 cc.link_executable(objects, os.path.join(tmpdir, "a.out"))
222 222 return True
223 223 except Exception:
224 224 return False
225 225 finally:
226 226 if oldstderr is not None:
227 227 os.dup2(oldstderr, sys.stderr.fileno())
228 228 if devnull is not None:
229 229 devnull.close()
230 230 shutil.rmtree(tmpdir)
231 231
232 232
233 233 # simplified version of distutils.ccompiler.CCompiler.has_function
234 234 # that actually removes its temporary files.
235 235 def hasfunction(cc, funcname):
236 236 code = 'int main(void) { %s(); }\n' % funcname
237 237 return cancompile(cc, code)
238 238
239 239
240 240 def hasheader(cc, headername):
241 241 code = '#include <%s>\nint main(void) { return 0; }\n' % headername
242 242 return cancompile(cc, code)
243 243
244 244
245 245 # py2exe needs to be installed to work
246 246 try:
247 247 import py2exe
248 248
249 249 py2exe.Distribution # silence unused import warning
250 250 py2exeloaded = True
251 251 # import py2exe's patched Distribution class
252 252 from distutils.core import Distribution
253 253 except ImportError:
254 254 py2exeloaded = False
255 255
256 256
257 257 def runcmd(cmd, env, cwd=None):
258 258 p = subprocess.Popen(
259 259 cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, cwd=cwd
260 260 )
261 261 out, err = p.communicate()
262 262 return p.returncode, out, err
263 263
264 264
265 265 class hgcommand(object):
266 266 def __init__(self, cmd, env):
267 267 self.cmd = cmd
268 268 self.env = env
269 269
270 270 def run(self, args):
271 271 cmd = self.cmd + args
272 272 returncode, out, err = runcmd(cmd, self.env)
273 273 err = filterhgerr(err)
274 274 if err or returncode != 0:
275 275 printf("stderr from '%s':" % (' '.join(cmd)), file=sys.stderr)
276 276 printf(err, file=sys.stderr)
277 277 return ''
278 278 return out
279 279
280 280
281 281 def filterhgerr(err):
282 282 # If root is executing setup.py, but the repository is owned by
283 283 # another user (as in "sudo python setup.py install") we will get
284 284 # trust warnings since the .hg/hgrc file is untrusted. That is
285 285 # fine, we don't want to load it anyway. Python may warn about
286 286 # a missing __init__.py in mercurial/locale, we also ignore that.
287 287 err = [
288 288 e
289 289 for e in err.splitlines()
290 290 if (
291 291 not e.startswith(b'not trusting file')
292 292 and not e.startswith(b'warning: Not importing')
293 293 and not e.startswith(b'obsolete feature not enabled')
294 294 and not e.startswith(b'*** failed to import extension')
295 295 and not e.startswith(b'devel-warn:')
296 296 and not (
297 297 e.startswith(b'(third party extension')
298 298 and e.endswith(b'or newer of Mercurial; disabling)')
299 299 )
300 300 )
301 301 ]
302 302 return b'\n'.join(b' ' + e for e in err)
303 303
304 304
305 305 def findhg():
306 306 """Try to figure out how we should invoke hg for examining the local
307 307 repository contents.
308 308
309 309 Returns an hgcommand object."""
310 310 # By default, prefer the "hg" command in the user's path. This was
311 311 # presumably the hg command that the user used to create this repository.
312 312 #
313 313 # This repository may require extensions or other settings that would not
314 314 # be enabled by running the hg script directly from this local repository.
315 315 hgenv = os.environ.copy()
316 316 # Use HGPLAIN to disable hgrc settings that would change output formatting,
317 317 # and disable localization for the same reasons.
318 318 hgenv['HGPLAIN'] = '1'
319 319 hgenv['LANGUAGE'] = 'C'
320 320 hgcmd = ['hg']
321 321 # Run a simple "hg log" command just to see if using hg from the user's
322 322 # path works and can successfully interact with this repository. Windows
323 323 # gives precedence to hg.exe in the current directory, so fall back to the
324 324 # python invocation of local hg, where pythonXY.dll can always be found.
325 325 check_cmd = ['log', '-r.', '-Ttest']
326 326 if os.name != 'nt':
327 327 try:
328 328 retcode, out, err = runcmd(hgcmd + check_cmd, hgenv)
329 329 except EnvironmentError:
330 330 retcode = -1
331 331 if retcode == 0 and not filterhgerr(err):
332 332 return hgcommand(hgcmd, hgenv)
333 333
334 334 # Fall back to trying the local hg installation.
335 335 hgenv = localhgenv()
336 336 hgcmd = [sys.executable, 'hg']
337 337 try:
338 338 retcode, out, err = runcmd(hgcmd + check_cmd, hgenv)
339 339 except EnvironmentError:
340 340 retcode = -1
341 341 if retcode == 0 and not filterhgerr(err):
342 342 return hgcommand(hgcmd, hgenv)
343 343
344 344 raise SystemExit(
345 345 'Unable to find a working hg binary to extract the '
346 346 'version from the repository tags'
347 347 )
348 348
349 349
350 350 def localhgenv():
351 351 """Get an environment dictionary to use for invoking or importing
352 352 mercurial from the local repository."""
353 353 # Execute hg out of this directory with a custom environment which takes
354 354 # care to not use any hgrc files and do no localization.
355 355 env = {
356 356 'HGMODULEPOLICY': 'py',
357 357 'HGRCPATH': '',
358 358 'LANGUAGE': 'C',
359 359 'PATH': '',
360 360 } # make pypi modules that use os.environ['PATH'] happy
361 361 if 'LD_LIBRARY_PATH' in os.environ:
362 362 env['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH']
363 363 if 'SystemRoot' in os.environ:
364 364 # SystemRoot is required by Windows to load various DLLs. See:
365 365 # https://bugs.python.org/issue13524#msg148850
366 366 env['SystemRoot'] = os.environ['SystemRoot']
367 367 return env
368 368
369 369
370 370 version = ''
371 371
372 372 if os.path.isdir('.hg'):
373 373 hg = findhg()
374 374 cmd = ['log', '-r', '.', '--template', '{tags}\n']
375 375 numerictags = [t for t in sysstr(hg.run(cmd)).split() if t[0:1].isdigit()]
376 376 hgid = sysstr(hg.run(['id', '-i'])).strip()
377 377 if not hgid:
378 378 # Bail out if hg is having problems interacting with this repository,
379 379 # rather than falling through and producing a bogus version number.
380 380 # Continuing with an invalid version number will break extensions
381 381 # that define minimumhgversion.
382 382 raise SystemExit('Unable to determine hg version from local repository')
383 383 if numerictags: # tag(s) found
384 384 version = numerictags[-1]
385 385 if hgid.endswith('+'): # propagate the dirty status to the tag
386 386 version += '+'
387 387 else: # no tag found
388 388 ltagcmd = ['parents', '--template', '{latesttag}']
389 389 ltag = sysstr(hg.run(ltagcmd))
390 390 changessincecmd = ['log', '-T', 'x\n', '-r', "only(.,'%s')" % ltag]
391 391 changessince = len(hg.run(changessincecmd).splitlines())
392 392 version = '%s+%s-%s' % (ltag, changessince, hgid)
393 393 if version.endswith('+'):
394 394 version += time.strftime('%Y%m%d')
395 395 elif os.path.exists('.hg_archival.txt'):
396 396 kw = dict(
397 397 [[t.strip() for t in l.split(':', 1)] for l in open('.hg_archival.txt')]
398 398 )
399 399 if 'tag' in kw:
400 400 version = kw['tag']
401 401 elif 'latesttag' in kw:
402 402 if 'changessincelatesttag' in kw:
403 403 version = '%(latesttag)s+%(changessincelatesttag)s-%(node).12s' % kw
404 404 else:
405 405 version = '%(latesttag)s+%(latesttagdistance)s-%(node).12s' % kw
406 406 else:
407 407 version = kw.get('node', '')[:12]
408 408
409 409 if version:
410 410 versionb = version
411 411 if not isinstance(versionb, bytes):
412 412 versionb = versionb.encode('ascii')
413 413
414 414 write_if_changed(
415 415 'mercurial/__version__.py',
416 416 b''.join(
417 417 [
418 418 b'# this file is autogenerated by setup.py\n'
419 419 b'version = b"%s"\n' % versionb,
420 420 ]
421 421 ),
422 422 )
423 423
424 424 try:
425 425 oldpolicy = os.environ.get('HGMODULEPOLICY', None)
426 426 os.environ['HGMODULEPOLICY'] = 'py'
427 427 from mercurial import __version__
428 428
429 429 version = __version__.version
430 430 except ImportError:
431 431 version = b'unknown'
432 432 finally:
433 433 if oldpolicy is None:
434 434 del os.environ['HGMODULEPOLICY']
435 435 else:
436 436 os.environ['HGMODULEPOLICY'] = oldpolicy
437 437
438 438
439 439 class hgbuild(build):
440 440 # Insert hgbuildmo first so that files in mercurial/locale/ are found
441 441 # when build_py is run next.
442 442 sub_commands = [('build_mo', None)] + build.sub_commands
443 443
444 444
445 445 class hgbuildmo(build):
446 446
447 447 description = "build translations (.mo files)"
448 448
449 449 def run(self):
450 450 if not find_executable('msgfmt'):
451 451 self.warn(
452 452 "could not find msgfmt executable, no translations "
453 453 "will be built"
454 454 )
455 455 return
456 456
457 457 podir = 'i18n'
458 458 if not os.path.isdir(podir):
459 459 self.warn("could not find %s/ directory" % podir)
460 460 return
461 461
462 462 join = os.path.join
463 463 for po in os.listdir(podir):
464 464 if not po.endswith('.po'):
465 465 continue
466 466 pofile = join(podir, po)
467 467 modir = join('locale', po[:-3], 'LC_MESSAGES')
468 468 mofile = join(modir, 'hg.mo')
469 469 mobuildfile = join('mercurial', mofile)
470 470 cmd = ['msgfmt', '-v', '-o', mobuildfile, pofile]
471 471 if sys.platform != 'sunos5':
472 472 # msgfmt on Solaris does not know about -c
473 473 cmd.append('-c')
474 474 self.mkpath(join('mercurial', modir))
475 475 self.make_file([pofile], mobuildfile, spawn, (cmd,))
476 476
477 477
478 478 class hgdist(Distribution):
479 479 pure = False
480 480 rust = hgrustext is not None
481 481 cffi = ispypy
482 482
483 483 global_options = Distribution.global_options + [
484 484 ('pure', None, "use pure (slow) Python code instead of C extensions"),
485 485 ('rust', None, "use Rust extensions additionally to C extensions"),
486 486 ]
487 487
488 488 def has_ext_modules(self):
489 489 # self.ext_modules is emptied in hgbuildpy.finalize_options which is
490 490 # too late for some cases
491 491 return not self.pure and Distribution.has_ext_modules(self)
492 492
493 493
494 494 # This is ugly as a one-liner. So use a variable.
495 495 buildextnegops = dict(getattr(build_ext, 'negative_options', {}))
496 496 buildextnegops['no-zstd'] = 'zstd'
497 497 buildextnegops['no-rust'] = 'rust'
498 498
499 499
500 500 class hgbuildext(build_ext):
501 501 user_options = build_ext.user_options + [
502 502 ('zstd', None, 'compile zstd bindings [default]'),
503 503 ('no-zstd', None, 'do not compile zstd bindings'),
504 504 (
505 505 'rust',
506 506 None,
507 507 'compile Rust extensions if they are in use '
508 508 '(requires Cargo) [default]',
509 509 ),
510 510 ('no-rust', None, 'do not compile Rust extensions'),
511 511 ]
512 512
513 513 boolean_options = build_ext.boolean_options + ['zstd', 'rust']
514 514 negative_opt = buildextnegops
515 515
516 516 def initialize_options(self):
517 517 self.zstd = True
518 518 self.rust = True
519 519
520 520 return build_ext.initialize_options(self)
521 521
522 522 def finalize_options(self):
523 523 # Unless overridden by the end user, build extensions in parallel.
524 524 # Only influences behavior on Python 3.5+.
525 525 if getattr(self, 'parallel', None) is None:
526 526 self.parallel = True
527 527
528 528 return build_ext.finalize_options(self)
529 529
530 530 def build_extensions(self):
531 531 ruststandalones = [
532 532 e for e in self.extensions if isinstance(e, RustStandaloneExtension)
533 533 ]
534 534 self.extensions = [
535 535 e for e in self.extensions if e not in ruststandalones
536 536 ]
537 537 # Filter out zstd if disabled via argument.
538 538 if not self.zstd:
539 539 self.extensions = [
540 540 e for e in self.extensions if e.name != 'mercurial.zstd'
541 541 ]
542 542
543 543 # Build Rust standalon extensions if it'll be used
544 544 # and its build is not explictely disabled (for external build
545 545 # as Linux distributions would do)
546 546 if self.distribution.rust and self.rust and hgrustext != 'direct-ffi':
547 547 for rustext in ruststandalones:
548 548 rustext.build('' if self.inplace else self.build_lib)
549 549
550 550 return build_ext.build_extensions(self)
551 551
552 552 def build_extension(self, ext):
553 553 if (
554 554 self.distribution.rust
555 555 and self.rust
556 556 and isinstance(ext, RustExtension)
557 557 ):
558 558 ext.rustbuild()
559 559 try:
560 560 build_ext.build_extension(self, ext)
561 561 except CCompilerError:
562 562 if not getattr(ext, 'optional', False):
563 563 raise
564 564 log.warn(
565 565 "Failed to build optional extension '%s' (skipping)", ext.name
566 566 )
567 567
568 568
569 569 class hgbuildscripts(build_scripts):
570 570 def run(self):
571 571 if os.name != 'nt' or self.distribution.pure:
572 572 return build_scripts.run(self)
573 573
574 574 exebuilt = False
575 575 try:
576 576 self.run_command('build_hgexe')
577 577 exebuilt = True
578 578 except (DistutilsError, CCompilerError):
579 579 log.warn('failed to build optional hg.exe')
580 580
581 581 if exebuilt:
582 582 # Copying hg.exe to the scripts build directory ensures it is
583 583 # installed by the install_scripts command.
584 584 hgexecommand = self.get_finalized_command('build_hgexe')
585 585 dest = os.path.join(self.build_dir, 'hg.exe')
586 586 self.mkpath(self.build_dir)
587 587 self.copy_file(hgexecommand.hgexepath, dest)
588 588
589 589 # Remove hg.bat because it is redundant with hg.exe.
590 590 self.scripts.remove('contrib/win32/hg.bat')
591 591
592 592 return build_scripts.run(self)
593 593
594 594
595 595 class hgbuildpy(build_py):
596 596 def finalize_options(self):
597 597 build_py.finalize_options(self)
598 598
599 599 if self.distribution.pure:
600 600 self.distribution.ext_modules = []
601 601 elif self.distribution.cffi:
602 602 from mercurial.cffi import (
603 603 bdiffbuild,
604 604 mpatchbuild,
605 605 )
606 606
607 607 exts = [
608 608 mpatchbuild.ffi.distutils_extension(),
609 609 bdiffbuild.ffi.distutils_extension(),
610 610 ]
611 611 # cffi modules go here
612 612 if sys.platform == 'darwin':
613 613 from mercurial.cffi import osutilbuild
614 614
615 615 exts.append(osutilbuild.ffi.distutils_extension())
616 616 self.distribution.ext_modules = exts
617 617 else:
618 618 h = os.path.join(get_python_inc(), 'Python.h')
619 619 if not os.path.exists(h):
620 620 raise SystemExit(
621 621 'Python headers are required to build '
622 622 'Mercurial but weren\'t found in %s' % h
623 623 )
624 624
625 625 def run(self):
626 626 basepath = os.path.join(self.build_lib, 'mercurial')
627 627 self.mkpath(basepath)
628 628
629 629 rust = self.distribution.rust
630 630 if self.distribution.pure:
631 631 modulepolicy = 'py'
632 632 elif self.build_lib == '.':
633 633 # in-place build should run without rebuilding and Rust extensions
634 634 modulepolicy = 'rust+c-allow' if rust else 'allow'
635 635 else:
636 636 modulepolicy = 'rust+c' if rust else 'c'
637 637
638 638 content = b''.join(
639 639 [
640 640 b'# this file is autogenerated by setup.py\n',
641 641 b'modulepolicy = b"%s"\n' % modulepolicy.encode('ascii'),
642 642 ]
643 643 )
644 644 write_if_changed(os.path.join(basepath, '__modulepolicy__.py'), content)
645 645
646 646 build_py.run(self)
647 647
648 648
649 649 class buildhgextindex(Command):
650 650 description = 'generate prebuilt index of hgext (for frozen package)'
651 651 user_options = []
652 652 _indexfilename = 'hgext/__index__.py'
653 653
654 654 def initialize_options(self):
655 655 pass
656 656
657 657 def finalize_options(self):
658 658 pass
659 659
660 660 def run(self):
661 661 if os.path.exists(self._indexfilename):
662 662 with open(self._indexfilename, 'w') as f:
663 663 f.write('# empty\n')
664 664
665 665 # here no extension enabled, disabled() lists up everything
666 666 code = (
667 667 'import pprint; from mercurial import extensions; '
668 668 'pprint.pprint(extensions.disabled())'
669 669 )
670 670 returncode, out, err = runcmd(
671 671 [sys.executable, '-c', code], localhgenv()
672 672 )
673 673 if err or returncode != 0:
674 674 raise DistutilsExecError(err)
675 675
676 676 with open(self._indexfilename, 'wb') as f:
677 677 f.write(b'# this file is autogenerated by setup.py\n')
678 678 f.write(b'docs = ')
679 679 f.write(out)
680 680
681 681
682 682 class buildhgexe(build_ext):
683 683 description = 'compile hg.exe from mercurial/exewrapper.c'
684 684 user_options = build_ext.user_options + [
685 685 (
686 686 'long-paths-support',
687 687 None,
688 688 'enable support for long paths on '
689 689 'Windows (off by default and '
690 690 'experimental)',
691 691 ),
692 692 ]
693 693
694 694 LONG_PATHS_MANIFEST = """
695 695 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
696 696 <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
697 697 <application>
698 698 <windowsSettings
699 699 xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
700 700 <ws2:longPathAware>true</ws2:longPathAware>
701 701 </windowsSettings>
702 702 </application>
703 703 </assembly>"""
704 704
705 705 def initialize_options(self):
706 706 build_ext.initialize_options(self)
707 707 self.long_paths_support = False
708 708
709 709 def build_extensions(self):
710 710 if os.name != 'nt':
711 711 return
712 712 if isinstance(self.compiler, HackedMingw32CCompiler):
713 713 self.compiler.compiler_so = self.compiler.compiler # no -mdll
714 714 self.compiler.dll_libraries = [] # no -lmsrvc90
715 715
716 716 # Different Python installs can have different Python library
717 717 # names. e.g. the official CPython distribution uses pythonXY.dll
718 718 # and MinGW uses libpythonX.Y.dll.
719 719 _kernel32 = ctypes.windll.kernel32
720 720 _kernel32.GetModuleFileNameA.argtypes = [
721 721 ctypes.c_void_p,
722 722 ctypes.c_void_p,
723 723 ctypes.c_ulong,
724 724 ]
725 725 _kernel32.GetModuleFileNameA.restype = ctypes.c_ulong
726 726 size = 1000
727 727 buf = ctypes.create_string_buffer(size + 1)
728 728 filelen = _kernel32.GetModuleFileNameA(
729 729 sys.dllhandle, ctypes.byref(buf), size
730 730 )
731 731
732 732 if filelen > 0 and filelen != size:
733 733 dllbasename = os.path.basename(buf.value)
734 734 if not dllbasename.lower().endswith(b'.dll'):
735 735 raise SystemExit(
736 736 'Python DLL does not end with .dll: %s' % dllbasename
737 737 )
738 738 pythonlib = dllbasename[:-4]
739 739 else:
740 740 log.warn(
741 741 'could not determine Python DLL filename; ' 'assuming pythonXY'
742 742 )
743 743
744 744 hv = sys.hexversion
745 745 pythonlib = 'python%d%d' % (hv >> 24, (hv >> 16) & 0xFF)
746 746
747 747 log.info('using %s as Python library name' % pythonlib)
748 748 with open('mercurial/hgpythonlib.h', 'wb') as f:
749 749 f.write(b'/* this file is autogenerated by setup.py */\n')
750 750 f.write(b'#define HGPYTHONLIB "%s"\n' % pythonlib)
751 751
752 752 macros = None
753 753 if sys.version_info[0] >= 3:
754 754 macros = [('_UNICODE', None), ('UNICODE', None)]
755 755
756 756 objects = self.compiler.compile(
757 757 ['mercurial/exewrapper.c'],
758 758 output_dir=self.build_temp,
759 759 macros=macros,
760 760 )
761 761 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
762 762 self.hgtarget = os.path.join(dir, 'hg')
763 763 self.compiler.link_executable(
764 764 objects, self.hgtarget, libraries=[], output_dir=self.build_temp
765 765 )
766 766 if self.long_paths_support:
767 767 self.addlongpathsmanifest()
768 768
769 769 def addlongpathsmanifest(self):
770 770 r"""Add manifest pieces so that hg.exe understands long paths
771 771
772 772 This is an EXPERIMENTAL feature, use with care.
773 773 To enable long paths support, one needs to do two things:
774 774 - build Mercurial with --long-paths-support option
775 775 - change HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\
776 776 LongPathsEnabled to have value 1.
777 777
778 778 Please ignore 'warning 81010002: Unrecognized Element "longPathAware"';
779 779 it happens because Mercurial uses mt.exe circa 2008, which is not
780 780 yet aware of long paths support in the manifest (I think so at least).
781 781 This does not stop mt.exe from embedding/merging the XML properly.
782 782
783 783 Why resource #1 should be used for .exe manifests? I don't know and
784 784 wasn't able to find an explanation for mortals. But it seems to work.
785 785 """
786 786 exefname = self.compiler.executable_filename(self.hgtarget)
787 787 fdauto, manfname = tempfile.mkstemp(suffix='.hg.exe.manifest')
788 788 os.close(fdauto)
789 789 with open(manfname, 'w') as f:
790 790 f.write(self.LONG_PATHS_MANIFEST)
791 791 log.info("long paths manifest is written to '%s'" % manfname)
792 792 inputresource = '-inputresource:%s;#1' % exefname
793 793 outputresource = '-outputresource:%s;#1' % exefname
794 794 log.info("running mt.exe to update hg.exe's manifest in-place")
795 795 # supplying both -manifest and -inputresource to mt.exe makes
796 796 # it merge the embedded and supplied manifests in the -outputresource
797 797 self.spawn(
798 798 [
799 799 'mt.exe',
800 800 '-nologo',
801 801 '-manifest',
802 802 manfname,
803 803 inputresource,
804 804 outputresource,
805 805 ]
806 806 )
807 807 log.info("done updating hg.exe's manifest")
808 808 os.remove(manfname)
809 809
810 810 @property
811 811 def hgexepath(self):
812 812 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
813 813 return os.path.join(self.build_temp, dir, 'hg.exe')
814 814
815 815
816 816 class hgbuilddoc(Command):
817 817 description = 'build documentation'
818 818 user_options = [
819 819 ('man', None, 'generate man pages'),
820 820 ('html', None, 'generate html pages'),
821 821 ]
822 822
823 823 def initialize_options(self):
824 824 self.man = None
825 825 self.html = None
826 826
827 827 def finalize_options(self):
828 828 # If --man or --html are set, only generate what we're told to.
829 829 # Otherwise generate everything.
830 830 have_subset = self.man is not None or self.html is not None
831 831
832 832 if have_subset:
833 833 self.man = True if self.man else False
834 834 self.html = True if self.html else False
835 835 else:
836 836 self.man = True
837 837 self.html = True
838 838
839 839 def run(self):
840 840 def normalizecrlf(p):
841 841 with open(p, 'rb') as fh:
842 842 orig = fh.read()
843 843
844 844 if b'\r\n' not in orig:
845 845 return
846 846
847 847 log.info('normalizing %s to LF line endings' % p)
848 848 with open(p, 'wb') as fh:
849 849 fh.write(orig.replace(b'\r\n', b'\n'))
850 850
851 851 def gentxt(root):
852 852 txt = 'doc/%s.txt' % root
853 853 log.info('generating %s' % txt)
854 854 res, out, err = runcmd(
855 855 [sys.executable, 'gendoc.py', root], os.environ, cwd='doc'
856 856 )
857 857 if res:
858 858 raise SystemExit(
859 859 'error running gendoc.py: %s' % '\n'.join([out, err])
860 860 )
861 861
862 862 with open(txt, 'wb') as fh:
863 863 fh.write(out)
864 864
865 865 def gengendoc(root):
866 866 gendoc = 'doc/%s.gendoc.txt' % root
867 867
868 868 log.info('generating %s' % gendoc)
869 869 res, out, err = runcmd(
870 870 [sys.executable, 'gendoc.py', '%s.gendoc' % root],
871 871 os.environ,
872 872 cwd='doc',
873 873 )
874 874 if res:
875 875 raise SystemExit(
876 876 'error running gendoc: %s' % '\n'.join([out, err])
877 877 )
878 878
879 879 with open(gendoc, 'wb') as fh:
880 880 fh.write(out)
881 881
882 882 def genman(root):
883 883 log.info('generating doc/%s' % root)
884 884 res, out, err = runcmd(
885 885 [
886 886 sys.executable,
887 887 'runrst',
888 888 'hgmanpage',
889 889 '--halt',
890 890 'warning',
891 891 '--strip-elements-with-class',
892 892 'htmlonly',
893 893 '%s.txt' % root,
894 894 root,
895 895 ],
896 896 os.environ,
897 897 cwd='doc',
898 898 )
899 899 if res:
900 900 raise SystemExit(
901 901 'error running runrst: %s' % '\n'.join([out, err])
902 902 )
903 903
904 904 normalizecrlf('doc/%s' % root)
905 905
906 906 def genhtml(root):
907 907 log.info('generating doc/%s.html' % root)
908 908 res, out, err = runcmd(
909 909 [
910 910 sys.executable,
911 911 'runrst',
912 912 'html',
913 913 '--halt',
914 914 'warning',
915 915 '--link-stylesheet',
916 916 '--stylesheet-path',
917 917 'style.css',
918 918 '%s.txt' % root,
919 919 '%s.html' % root,
920 920 ],
921 921 os.environ,
922 922 cwd='doc',
923 923 )
924 924 if res:
925 925 raise SystemExit(
926 926 'error running runrst: %s' % '\n'.join([out, err])
927 927 )
928 928
929 929 normalizecrlf('doc/%s.html' % root)
930 930
931 931 # This logic is duplicated in doc/Makefile.
932 932 sources = set(
933 933 f
934 934 for f in os.listdir('mercurial/help')
935 935 if re.search(r'[0-9]\.txt$', f)
936 936 )
937 937
938 938 # common.txt is a one-off.
939 939 gentxt('common')
940 940
941 941 for source in sorted(sources):
942 942 assert source[-4:] == '.txt'
943 943 root = source[:-4]
944 944
945 945 gentxt(root)
946 946 gengendoc(root)
947 947
948 948 if self.man:
949 949 genman(root)
950 950 if self.html:
951 951 genhtml(root)
952 952
953 953
954 954 class hginstall(install):
955 955
956 956 user_options = install.user_options + [
957 957 (
958 958 'old-and-unmanageable',
959 959 None,
960 960 'noop, present for eggless setuptools compat',
961 961 ),
962 962 (
963 963 'single-version-externally-managed',
964 964 None,
965 965 'noop, present for eggless setuptools compat',
966 966 ),
967 967 ]
968 968
969 969 # Also helps setuptools not be sad while we refuse to create eggs.
970 970 single_version_externally_managed = True
971 971
972 972 def get_sub_commands(self):
973 973 # Screen out egg related commands to prevent egg generation. But allow
974 974 # mercurial.egg-info generation, since that is part of modern
975 975 # packaging.
976 976 excl = set(['bdist_egg'])
977 977 return filter(lambda x: x not in excl, install.get_sub_commands(self))
978 978
979 979
980 980 class hginstalllib(install_lib):
981 981 '''
982 982 This is a specialization of install_lib that replaces the copy_file used
983 983 there so that it supports setting the mode of files after copying them,
984 984 instead of just preserving the mode that the files originally had. If your
985 985 system has a umask of something like 027, preserving the permissions when
986 986 copying will lead to a broken install.
987 987
988 988 Note that just passing keep_permissions=False to copy_file would be
989 989 insufficient, as it might still be applying a umask.
990 990 '''
991 991
992 992 def run(self):
993 993 realcopyfile = file_util.copy_file
994 994
995 995 def copyfileandsetmode(*args, **kwargs):
996 996 src, dst = args[0], args[1]
997 997 dst, copied = realcopyfile(*args, **kwargs)
998 998 if copied:
999 999 st = os.stat(src)
1000 1000 # Persist executable bit (apply it to group and other if user
1001 1001 # has it)
1002 1002 if st[stat.ST_MODE] & stat.S_IXUSR:
1003 1003 setmode = int('0755', 8)
1004 1004 else:
1005 1005 setmode = int('0644', 8)
1006 1006 m = stat.S_IMODE(st[stat.ST_MODE])
1007 1007 m = (m & ~int('0777', 8)) | setmode
1008 1008 os.chmod(dst, m)
1009 1009
1010 1010 file_util.copy_file = copyfileandsetmode
1011 1011 try:
1012 1012 install_lib.run(self)
1013 1013 finally:
1014 1014 file_util.copy_file = realcopyfile
1015 1015
1016 1016
1017 1017 class hginstallscripts(install_scripts):
1018 1018 '''
1019 1019 This is a specialization of install_scripts that replaces the @LIBDIR@ with
1020 1020 the configured directory for modules. If possible, the path is made relative
1021 1021 to the directory for scripts.
1022 1022 '''
1023 1023
1024 1024 def initialize_options(self):
1025 1025 install_scripts.initialize_options(self)
1026 1026
1027 1027 self.install_lib = None
1028 1028
1029 1029 def finalize_options(self):
1030 1030 install_scripts.finalize_options(self)
1031 1031 self.set_undefined_options('install', ('install_lib', 'install_lib'))
1032 1032
1033 1033 def run(self):
1034 1034 install_scripts.run(self)
1035 1035
1036 1036 # It only makes sense to replace @LIBDIR@ with the install path if
1037 1037 # the install path is known. For wheels, the logic below calculates
1038 1038 # the libdir to be "../..". This is because the internal layout of a
1039 1039 # wheel archive looks like:
1040 1040 #
1041 1041 # mercurial-3.6.1.data/scripts/hg
1042 1042 # mercurial/__init__.py
1043 1043 #
1044 1044 # When installing wheels, the subdirectories of the "<pkg>.data"
1045 1045 # directory are translated to system local paths and files therein
1046 1046 # are copied in place. The mercurial/* files are installed into the
1047 1047 # site-packages directory. However, the site-packages directory
1048 1048 # isn't known until wheel install time. This means we have no clue
1049 1049 # at wheel generation time what the installed site-packages directory
1050 1050 # will be. And, wheels don't appear to provide the ability to register
1051 1051 # custom code to run during wheel installation. This all means that
1052 1052 # we can't reliably set the libdir in wheels: the default behavior
1053 1053 # of looking in sys.path must do.
1054 1054
1055 1055 if (
1056 1056 os.path.splitdrive(self.install_dir)[0]
1057 1057 != os.path.splitdrive(self.install_lib)[0]
1058 1058 ):
1059 1059 # can't make relative paths from one drive to another, so use an
1060 1060 # absolute path instead
1061 1061 libdir = self.install_lib
1062 1062 else:
1063 1063 common = os.path.commonprefix((self.install_dir, self.install_lib))
1064 1064 rest = self.install_dir[len(common) :]
1065 1065 uplevel = len([n for n in os.path.split(rest) if n])
1066 1066
1067 1067 libdir = uplevel * ('..' + os.sep) + self.install_lib[len(common) :]
1068 1068
1069 1069 for outfile in self.outfiles:
1070 1070 with open(outfile, 'rb') as fp:
1071 1071 data = fp.read()
1072 1072
1073 1073 # skip binary files
1074 1074 if b'\0' in data:
1075 1075 continue
1076 1076
1077 1077 # During local installs, the shebang will be rewritten to the final
1078 1078 # install path. During wheel packaging, the shebang has a special
1079 1079 # value.
1080 1080 if data.startswith(b'#!python'):
1081 1081 log.info(
1082 1082 'not rewriting @LIBDIR@ in %s because install path '
1083 1083 'not known' % outfile
1084 1084 )
1085 1085 continue
1086 1086
1087 1087 data = data.replace(b'@LIBDIR@', libdir.encode(libdir_escape))
1088 1088 with open(outfile, 'wb') as fp:
1089 1089 fp.write(data)
1090 1090
1091 1091
1092 1092 # virtualenv installs custom distutils/__init__.py and
1093 1093 # distutils/distutils.cfg files which essentially proxy back to the
1094 1094 # "real" distutils in the main Python install. The presence of this
1095 1095 # directory causes py2exe to pick up the "hacked" distutils package
1096 1096 # from the virtualenv and "import distutils" will fail from the py2exe
1097 1097 # build because the "real" distutils files can't be located.
1098 1098 #
1099 1099 # We work around this by monkeypatching the py2exe code finding Python
1100 1100 # modules to replace the found virtualenv distutils modules with the
1101 1101 # original versions via filesystem scanning. This is a bit hacky. But
1102 1102 # it allows us to use virtualenvs for py2exe packaging, which is more
1103 1103 # deterministic and reproducible.
1104 1104 #
1105 1105 # It's worth noting that the common StackOverflow suggestions for this
1106 1106 # problem involve copying the original distutils files into the
1107 1107 # virtualenv or into the staging directory after setup() is invoked.
1108 1108 # The former is very brittle and can easily break setup(). Our hacking
1109 1109 # of the found modules routine has a similar result as copying the files
1110 1110 # manually. But it makes fewer assumptions about how py2exe works and
1111 1111 # is less brittle.
1112 1112
1113 1113 # This only catches virtualenvs made with virtualenv (as opposed to
1114 1114 # venv, which is likely what Python 3 uses).
1115 1115 py2exehacked = py2exeloaded and getattr(sys, 'real_prefix', None) is not None
1116 1116
1117 1117 if py2exehacked:
1118 1118 from distutils.command.py2exe import py2exe as buildpy2exe
1119 1119 from py2exe.mf import Module as py2exemodule
1120 1120
1121 1121 class hgbuildpy2exe(buildpy2exe):
1122 1122 def find_needed_modules(self, mf, files, modules):
1123 1123 res = buildpy2exe.find_needed_modules(self, mf, files, modules)
1124 1124
1125 1125 # Replace virtualenv's distutils modules with the real ones.
1126 1126 modules = {}
1127 1127 for k, v in res.modules.items():
1128 1128 if k != 'distutils' and not k.startswith('distutils.'):
1129 1129 modules[k] = v
1130 1130
1131 1131 res.modules = modules
1132 1132
1133 1133 import opcode
1134 1134
1135 1135 distutilsreal = os.path.join(
1136 1136 os.path.dirname(opcode.__file__), 'distutils'
1137 1137 )
1138 1138
1139 1139 for root, dirs, files in os.walk(distutilsreal):
1140 1140 for f in sorted(files):
1141 1141 if not f.endswith('.py'):
1142 1142 continue
1143 1143
1144 1144 full = os.path.join(root, f)
1145 1145
1146 1146 parents = ['distutils']
1147 1147
1148 1148 if root != distutilsreal:
1149 1149 rel = os.path.relpath(root, distutilsreal)
1150 1150 parents.extend(p for p in rel.split(os.sep))
1151 1151
1152 1152 modname = '%s.%s' % ('.'.join(parents), f[:-3])
1153 1153
1154 1154 if modname.startswith('distutils.tests.'):
1155 1155 continue
1156 1156
1157 1157 if modname.endswith('.__init__'):
1158 1158 modname = modname[: -len('.__init__')]
1159 1159 path = os.path.dirname(full)
1160 1160 else:
1161 1161 path = None
1162 1162
1163 1163 res.modules[modname] = py2exemodule(
1164 1164 modname, full, path=path
1165 1165 )
1166 1166
1167 1167 if 'distutils' not in res.modules:
1168 1168 raise SystemExit('could not find distutils modules')
1169 1169
1170 1170 return res
1171 1171
1172 1172
1173 1173 cmdclass = {
1174 1174 'build': hgbuild,
1175 1175 'build_doc': hgbuilddoc,
1176 1176 'build_mo': hgbuildmo,
1177 1177 'build_ext': hgbuildext,
1178 1178 'build_py': hgbuildpy,
1179 1179 'build_scripts': hgbuildscripts,
1180 1180 'build_hgextindex': buildhgextindex,
1181 1181 'install': hginstall,
1182 1182 'install_lib': hginstalllib,
1183 1183 'install_scripts': hginstallscripts,
1184 1184 'build_hgexe': buildhgexe,
1185 1185 }
1186 1186
1187 1187 if py2exehacked:
1188 1188 cmdclass['py2exe'] = hgbuildpy2exe
1189 1189
1190 1190 packages = [
1191 1191 'mercurial',
1192 1192 'mercurial.cext',
1193 1193 'mercurial.cffi',
1194 'mercurial.helptext',
1195 'mercurial.helptext.internals',
1194 1196 'mercurial.hgweb',
1195 1197 'mercurial.interfaces',
1196 1198 'mercurial.pure',
1197 1199 'mercurial.thirdparty',
1198 1200 'mercurial.thirdparty.attr',
1199 1201 'mercurial.thirdparty.zope',
1200 1202 'mercurial.thirdparty.zope.interface',
1201 1203 'mercurial.utils',
1202 1204 'mercurial.revlogutils',
1203 1205 'mercurial.testing',
1204 1206 'hgext',
1205 1207 'hgext.convert',
1206 1208 'hgext.fsmonitor',
1207 1209 'hgext.fastannotate',
1208 1210 'hgext.fsmonitor.pywatchman',
1209 1211 'hgext.highlight',
1210 1212 'hgext.infinitepush',
1211 1213 'hgext.largefiles',
1212 1214 'hgext.lfs',
1213 1215 'hgext.narrow',
1214 1216 'hgext.remotefilelog',
1215 1217 'hgext.zeroconf',
1216 1218 'hgext3rd',
1217 1219 'hgdemandimport',
1218 1220 ]
1219 1221 if sys.version_info[0] == 2:
1220 1222 packages.extend(
1221 1223 [
1222 1224 'mercurial.thirdparty.concurrent',
1223 1225 'mercurial.thirdparty.concurrent.futures',
1224 1226 ]
1225 1227 )
1226 1228
1227 1229 if 'HG_PY2EXE_EXTRA_INSTALL_PACKAGES' in os.environ:
1228 1230 # py2exe can't cope with namespace packages very well, so we have to
1229 1231 # install any hgext3rd.* extensions that we want in the final py2exe
1230 1232 # image here. This is gross, but you gotta do what you gotta do.
1231 1233 packages.extend(os.environ['HG_PY2EXE_EXTRA_INSTALL_PACKAGES'].split(' '))
1232 1234
1233 1235 common_depends = [
1234 1236 'mercurial/bitmanipulation.h',
1235 1237 'mercurial/compat.h',
1236 1238 'mercurial/cext/util.h',
1237 1239 ]
1238 1240 common_include_dirs = ['mercurial']
1239 1241
1240 1242 osutil_cflags = []
1241 1243 osutil_ldflags = []
1242 1244
1243 1245 # platform specific macros
1244 1246 for plat, func in [('bsd', 'setproctitle')]:
1245 1247 if re.search(plat, sys.platform) and hasfunction(new_compiler(), func):
1246 1248 osutil_cflags.append('-DHAVE_%s' % func.upper())
1247 1249
1248 1250 for plat, macro, code in [
1249 1251 (
1250 1252 'bsd|darwin',
1251 1253 'BSD_STATFS',
1252 1254 '''
1253 1255 #include <sys/param.h>
1254 1256 #include <sys/mount.h>
1255 1257 int main() { struct statfs s; return sizeof(s.f_fstypename); }
1256 1258 ''',
1257 1259 ),
1258 1260 (
1259 1261 'linux',
1260 1262 'LINUX_STATFS',
1261 1263 '''
1262 1264 #include <linux/magic.h>
1263 1265 #include <sys/vfs.h>
1264 1266 int main() { struct statfs s; return sizeof(s.f_type); }
1265 1267 ''',
1266 1268 ),
1267 1269 ]:
1268 1270 if re.search(plat, sys.platform) and cancompile(new_compiler(), code):
1269 1271 osutil_cflags.append('-DHAVE_%s' % macro)
1270 1272
1271 1273 if sys.platform == 'darwin':
1272 1274 osutil_ldflags += ['-framework', 'ApplicationServices']
1273 1275
1274 1276 xdiff_srcs = [
1275 1277 'mercurial/thirdparty/xdiff/xdiffi.c',
1276 1278 'mercurial/thirdparty/xdiff/xprepare.c',
1277 1279 'mercurial/thirdparty/xdiff/xutils.c',
1278 1280 ]
1279 1281
1280 1282 xdiff_headers = [
1281 1283 'mercurial/thirdparty/xdiff/xdiff.h',
1282 1284 'mercurial/thirdparty/xdiff/xdiffi.h',
1283 1285 'mercurial/thirdparty/xdiff/xinclude.h',
1284 1286 'mercurial/thirdparty/xdiff/xmacros.h',
1285 1287 'mercurial/thirdparty/xdiff/xprepare.h',
1286 1288 'mercurial/thirdparty/xdiff/xtypes.h',
1287 1289 'mercurial/thirdparty/xdiff/xutils.h',
1288 1290 ]
1289 1291
1290 1292
1291 1293 class RustCompilationError(CCompilerError):
1292 1294 """Exception class for Rust compilation errors."""
1293 1295
1294 1296
1295 1297 class RustExtension(Extension):
1296 1298 """Base classes for concrete Rust Extension classes.
1297 1299 """
1298 1300
1299 1301 rusttargetdir = os.path.join('rust', 'target', 'release')
1300 1302
1301 1303 def __init__(
1302 1304 self, mpath, sources, rustlibname, subcrate, py3_features=None, **kw
1303 1305 ):
1304 1306 Extension.__init__(self, mpath, sources, **kw)
1305 1307 srcdir = self.rustsrcdir = os.path.join('rust', subcrate)
1306 1308 self.py3_features = py3_features
1307 1309
1308 1310 # adding Rust source and control files to depends so that the extension
1309 1311 # gets rebuilt if they've changed
1310 1312 self.depends.append(os.path.join(srcdir, 'Cargo.toml'))
1311 1313 cargo_lock = os.path.join(srcdir, 'Cargo.lock')
1312 1314 if os.path.exists(cargo_lock):
1313 1315 self.depends.append(cargo_lock)
1314 1316 for dirpath, subdir, fnames in os.walk(os.path.join(srcdir, 'src')):
1315 1317 self.depends.extend(
1316 1318 os.path.join(dirpath, fname)
1317 1319 for fname in fnames
1318 1320 if os.path.splitext(fname)[1] == '.rs'
1319 1321 )
1320 1322
1321 1323 @staticmethod
1322 1324 def rustdylibsuffix():
1323 1325 """Return the suffix for shared libraries produced by rustc.
1324 1326
1325 1327 See also: https://doc.rust-lang.org/reference/linkage.html
1326 1328 """
1327 1329 if sys.platform == 'darwin':
1328 1330 return '.dylib'
1329 1331 elif os.name == 'nt':
1330 1332 return '.dll'
1331 1333 else:
1332 1334 return '.so'
1333 1335
1334 1336 def rustbuild(self):
1335 1337 env = os.environ.copy()
1336 1338 if 'HGTEST_RESTOREENV' in env:
1337 1339 # Mercurial tests change HOME to a temporary directory,
1338 1340 # but, if installed with rustup, the Rust toolchain needs
1339 1341 # HOME to be correct (otherwise the 'no default toolchain'
1340 1342 # error message is issued and the build fails).
1341 1343 # This happens currently with test-hghave.t, which does
1342 1344 # invoke this build.
1343 1345
1344 1346 # Unix only fix (os.path.expanduser not really reliable if
1345 1347 # HOME is shadowed like this)
1346 1348 import pwd
1347 1349
1348 1350 env['HOME'] = pwd.getpwuid(os.getuid()).pw_dir
1349 1351
1350 1352 cargocmd = ['cargo', 'rustc', '-vv', '--release']
1351 1353 if sys.version_info[0] == 3 and self.py3_features is not None:
1352 1354 cargocmd.extend(
1353 1355 ('--features', self.py3_features, '--no-default-features')
1354 1356 )
1355 1357 cargocmd.append('--')
1356 1358 if sys.platform == 'darwin':
1357 1359 cargocmd.extend(
1358 1360 ("-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup")
1359 1361 )
1360 1362 try:
1361 1363 subprocess.check_call(cargocmd, env=env, cwd=self.rustsrcdir)
1362 1364 except OSError as exc:
1363 1365 if exc.errno == errno.ENOENT:
1364 1366 raise RustCompilationError("Cargo not found")
1365 1367 elif exc.errno == errno.EACCES:
1366 1368 raise RustCompilationError(
1367 1369 "Cargo found, but permisssion to execute it is denied"
1368 1370 )
1369 1371 else:
1370 1372 raise
1371 1373 except subprocess.CalledProcessError:
1372 1374 raise RustCompilationError(
1373 1375 "Cargo failed. Working directory: %r, "
1374 1376 "command: %r, environment: %r"
1375 1377 % (self.rustsrcdir, cargocmd, env)
1376 1378 )
1377 1379
1378 1380
1379 1381 class RustEnhancedExtension(RustExtension):
1380 1382 """A C Extension, conditionally enhanced with Rust code.
1381 1383
1382 1384 If the HGRUSTEXT environment variable is set to something else
1383 1385 than 'cpython', the Rust sources get compiled and linked within the
1384 1386 C target shared library object.
1385 1387 """
1386 1388
1387 1389 def __init__(self, mpath, sources, rustlibname, subcrate, **kw):
1388 1390 RustExtension.__init__(
1389 1391 self, mpath, sources, rustlibname, subcrate, **kw
1390 1392 )
1391 1393 if hgrustext != 'direct-ffi':
1392 1394 return
1393 1395 self.extra_compile_args.append('-DWITH_RUST')
1394 1396 self.libraries.append(rustlibname)
1395 1397 self.library_dirs.append(self.rusttargetdir)
1396 1398
1397 1399 def rustbuild(self):
1398 1400 if hgrustext == 'direct-ffi':
1399 1401 RustExtension.rustbuild(self)
1400 1402
1401 1403
1402 1404 class RustStandaloneExtension(RustExtension):
1403 1405 def __init__(self, pydottedname, rustcrate, dylibname, **kw):
1404 1406 RustExtension.__init__(
1405 1407 self, pydottedname, [], dylibname, rustcrate, **kw
1406 1408 )
1407 1409 self.dylibname = dylibname
1408 1410
1409 1411 def build(self, target_dir):
1410 1412 self.rustbuild()
1411 1413 target = [target_dir]
1412 1414 target.extend(self.name.split('.'))
1413 1415 target[-1] += DYLIB_SUFFIX
1414 1416 shutil.copy2(
1415 1417 os.path.join(
1416 1418 self.rusttargetdir, self.dylibname + self.rustdylibsuffix()
1417 1419 ),
1418 1420 os.path.join(*target),
1419 1421 )
1420 1422
1421 1423
1422 1424 extmodules = [
1423 1425 Extension(
1424 1426 'mercurial.cext.base85',
1425 1427 ['mercurial/cext/base85.c'],
1426 1428 include_dirs=common_include_dirs,
1427 1429 depends=common_depends,
1428 1430 ),
1429 1431 Extension(
1430 1432 'mercurial.cext.bdiff',
1431 1433 ['mercurial/bdiff.c', 'mercurial/cext/bdiff.c'] + xdiff_srcs,
1432 1434 include_dirs=common_include_dirs,
1433 1435 depends=common_depends + ['mercurial/bdiff.h'] + xdiff_headers,
1434 1436 ),
1435 1437 Extension(
1436 1438 'mercurial.cext.mpatch',
1437 1439 ['mercurial/mpatch.c', 'mercurial/cext/mpatch.c'],
1438 1440 include_dirs=common_include_dirs,
1439 1441 depends=common_depends,
1440 1442 ),
1441 1443 RustEnhancedExtension(
1442 1444 'mercurial.cext.parsers',
1443 1445 [
1444 1446 'mercurial/cext/charencode.c',
1445 1447 'mercurial/cext/dirs.c',
1446 1448 'mercurial/cext/manifest.c',
1447 1449 'mercurial/cext/parsers.c',
1448 1450 'mercurial/cext/pathencode.c',
1449 1451 'mercurial/cext/revlog.c',
1450 1452 ],
1451 1453 'hgdirectffi',
1452 1454 'hg-direct-ffi',
1453 1455 include_dirs=common_include_dirs,
1454 1456 depends=common_depends
1455 1457 + [
1456 1458 'mercurial/cext/charencode.h',
1457 1459 'mercurial/cext/revlog.h',
1458 1460 'rust/hg-core/src/ancestors.rs',
1459 1461 'rust/hg-core/src/lib.rs',
1460 1462 ],
1461 1463 ),
1462 1464 Extension(
1463 1465 'mercurial.cext.osutil',
1464 1466 ['mercurial/cext/osutil.c'],
1465 1467 include_dirs=common_include_dirs,
1466 1468 extra_compile_args=osutil_cflags,
1467 1469 extra_link_args=osutil_ldflags,
1468 1470 depends=common_depends,
1469 1471 ),
1470 1472 Extension(
1471 1473 'mercurial.thirdparty.zope.interface._zope_interface_coptimizations',
1472 1474 [
1473 1475 'mercurial/thirdparty/zope/interface/_zope_interface_coptimizations.c',
1474 1476 ],
1475 1477 ),
1476 1478 Extension(
1477 1479 'hgext.fsmonitor.pywatchman.bser', ['hgext/fsmonitor/pywatchman/bser.c']
1478 1480 ),
1479 1481 RustStandaloneExtension(
1480 1482 'mercurial.rustext', 'hg-cpython', 'librusthg', py3_features='python3'
1481 1483 ),
1482 1484 ]
1483 1485
1484 1486
1485 1487 sys.path.insert(0, 'contrib/python-zstandard')
1486 1488 import setup_zstd
1487 1489
1488 1490 extmodules.append(
1489 1491 setup_zstd.get_c_extension(
1490 1492 name='mercurial.zstd', root=os.path.abspath(os.path.dirname(__file__))
1491 1493 )
1492 1494 )
1493 1495
1494 1496 try:
1495 1497 from distutils import cygwinccompiler
1496 1498
1497 1499 # the -mno-cygwin option has been deprecated for years
1498 1500 mingw32compilerclass = cygwinccompiler.Mingw32CCompiler
1499 1501
1500 1502 class HackedMingw32CCompiler(cygwinccompiler.Mingw32CCompiler):
1501 1503 def __init__(self, *args, **kwargs):
1502 1504 mingw32compilerclass.__init__(self, *args, **kwargs)
1503 1505 for i in 'compiler compiler_so linker_exe linker_so'.split():
1504 1506 try:
1505 1507 getattr(self, i).remove('-mno-cygwin')
1506 1508 except ValueError:
1507 1509 pass
1508 1510
1509 1511 cygwinccompiler.Mingw32CCompiler = HackedMingw32CCompiler
1510 1512 except ImportError:
1511 1513 # the cygwinccompiler package is not available on some Python
1512 1514 # distributions like the ones from the optware project for Synology
1513 1515 # DiskStation boxes
1514 1516 class HackedMingw32CCompiler(object):
1515 1517 pass
1516 1518
1517 1519
1518 1520 if os.name == 'nt':
1519 1521 # Allow compiler/linker flags to be added to Visual Studio builds. Passing
1520 1522 # extra_link_args to distutils.extensions.Extension() doesn't have any
1521 1523 # effect.
1522 1524 from distutils import msvccompiler
1523 1525
1524 1526 msvccompilerclass = msvccompiler.MSVCCompiler
1525 1527
1526 1528 class HackedMSVCCompiler(msvccompiler.MSVCCompiler):
1527 1529 def initialize(self):
1528 1530 msvccompilerclass.initialize(self)
1529 1531 # "warning LNK4197: export 'func' specified multiple times"
1530 1532 self.ldflags_shared.append('/ignore:4197')
1531 1533 self.ldflags_shared_debug.append('/ignore:4197')
1532 1534
1533 1535 msvccompiler.MSVCCompiler = HackedMSVCCompiler
1534 1536
1535 1537 packagedata = {
1536 1538 'mercurial': [
1537 1539 'locale/*/LC_MESSAGES/hg.mo',
1538 'help/*.txt',
1539 'help/internals/*.txt',
1540 1540 'default.d/*.rc',
1541 1541 'dummycert.pem',
1542 ]
1542 ],
1543 'mercurial.helptext': ['*.txt',],
1544 'mercurial.helptext.internals': ['*.txt',],
1543 1545 }
1544 1546
1545 1547
1546 1548 def ordinarypath(p):
1547 1549 return p and p[0] != '.' and p[-1] != '~'
1548 1550
1549 1551
1550 1552 for root in ('templates',):
1551 1553 for curdir, dirs, files in os.walk(os.path.join('mercurial', root)):
1552 1554 curdir = curdir.split(os.sep, 1)[1]
1553 1555 dirs[:] = filter(ordinarypath, dirs)
1554 1556 for f in filter(ordinarypath, files):
1555 1557 f = os.path.join(curdir, f)
1556 1558 packagedata['mercurial'].append(f)
1557 1559
1558 1560 datafiles = []
1559 1561
1560 1562 # distutils expects version to be str/unicode. Converting it to
1561 1563 # unicode on Python 2 still works because it won't contain any
1562 1564 # non-ascii bytes and will be implicitly converted back to bytes
1563 1565 # when operated on.
1564 1566 assert isinstance(version, bytes)
1565 1567 setupversion = version.decode('ascii')
1566 1568
1567 1569 extra = {}
1568 1570
1569 1571 py2exepackages = [
1570 1572 'hgdemandimport',
1571 1573 'hgext3rd',
1572 1574 'hgext',
1573 1575 'email',
1574 1576 # implicitly imported per module policy
1575 1577 # (cffi wouldn't be used as a frozen exe)
1576 1578 'mercurial.cext',
1577 1579 #'mercurial.cffi',
1578 1580 'mercurial.pure',
1579 1581 ]
1580 1582
1581 1583 py2exeexcludes = []
1582 1584 py2exedllexcludes = ['crypt32.dll']
1583 1585
1584 1586 if issetuptools:
1585 1587 extra['python_requires'] = supportedpy
1586 1588
1587 1589 if py2exeloaded:
1588 1590 extra['console'] = [
1589 1591 {
1590 1592 'script': 'hg',
1591 1593 'copyright': 'Copyright (C) 2005-2019 Matt Mackall and others',
1592 1594 'product_version': version,
1593 1595 }
1594 1596 ]
1595 1597 # Sub command of 'build' because 'py2exe' does not handle sub_commands.
1596 1598 # Need to override hgbuild because it has a private copy of
1597 1599 # build.sub_commands.
1598 1600 hgbuild.sub_commands.insert(0, ('build_hgextindex', None))
1599 1601 # put dlls in sub directory so that they won't pollute PATH
1600 1602 extra['zipfile'] = 'lib/library.zip'
1601 1603
1602 1604 # We allow some configuration to be supplemented via environment
1603 1605 # variables. This is better than setup.cfg files because it allows
1604 1606 # supplementing configs instead of replacing them.
1605 1607 extrapackages = os.environ.get('HG_PY2EXE_EXTRA_PACKAGES')
1606 1608 if extrapackages:
1607 1609 py2exepackages.extend(extrapackages.split(' '))
1608 1610
1609 1611 excludes = os.environ.get('HG_PY2EXE_EXTRA_EXCLUDES')
1610 1612 if excludes:
1611 1613 py2exeexcludes.extend(excludes.split(' '))
1612 1614
1613 1615 dllexcludes = os.environ.get('HG_PY2EXE_EXTRA_DLL_EXCLUDES')
1614 1616 if dllexcludes:
1615 1617 py2exedllexcludes.extend(dllexcludes.split(' '))
1616 1618
1617 1619 if os.name == 'nt':
1618 1620 # Windows binary file versions for exe/dll files must have the
1619 1621 # form W.X.Y.Z, where W,X,Y,Z are numbers in the range 0..65535
1620 1622 setupversion = setupversion.split(r'+', 1)[0]
1621 1623
1622 1624 if sys.platform == 'darwin' and os.path.exists('/usr/bin/xcodebuild'):
1623 1625 version = runcmd(['/usr/bin/xcodebuild', '-version'], {})[1].splitlines()
1624 1626 if version:
1625 1627 version = version[0]
1626 1628 if sys.version_info[0] == 3:
1627 1629 version = version.decode('utf-8')
1628 1630 xcode4 = version.startswith('Xcode') and StrictVersion(
1629 1631 version.split()[1]
1630 1632 ) >= StrictVersion('4.0')
1631 1633 xcode51 = re.match(r'^Xcode\s+5\.1', version) is not None
1632 1634 else:
1633 1635 # xcodebuild returns empty on OS X Lion with XCode 4.3 not
1634 1636 # installed, but instead with only command-line tools. Assume
1635 1637 # that only happens on >= Lion, thus no PPC support.
1636 1638 xcode4 = True
1637 1639 xcode51 = False
1638 1640
1639 1641 # XCode 4.0 dropped support for ppc architecture, which is hardcoded in
1640 1642 # distutils.sysconfig
1641 1643 if xcode4:
1642 1644 os.environ['ARCHFLAGS'] = ''
1643 1645
1644 1646 # XCode 5.1 changes clang such that it now fails to compile if the
1645 1647 # -mno-fused-madd flag is passed, but the version of Python shipped with
1646 1648 # OS X 10.9 Mavericks includes this flag. This causes problems in all
1647 1649 # C extension modules, and a bug has been filed upstream at
1648 1650 # http://bugs.python.org/issue21244. We also need to patch this here
1649 1651 # so Mercurial can continue to compile in the meantime.
1650 1652 if xcode51:
1651 1653 cflags = get_config_var('CFLAGS')
1652 1654 if cflags and re.search(r'-mno-fused-madd\b', cflags) is not None:
1653 1655 os.environ['CFLAGS'] = (
1654 1656 os.environ.get('CFLAGS', '') + ' -Qunused-arguments'
1655 1657 )
1656 1658
1657 1659 setup(
1658 1660 name='mercurial',
1659 1661 version=setupversion,
1660 1662 author='Matt Mackall and many others',
1661 1663 author_email='mercurial@mercurial-scm.org',
1662 1664 url='https://mercurial-scm.org/',
1663 1665 download_url='https://mercurial-scm.org/release/',
1664 1666 description=(
1665 1667 'Fast scalable distributed SCM (revision control, version '
1666 1668 'control) system'
1667 1669 ),
1668 1670 long_description=(
1669 1671 'Mercurial is a distributed SCM tool written in Python.'
1670 1672 ' It is used by a number of large projects that require'
1671 1673 ' fast, reliable distributed revision control, such as '
1672 1674 'Mozilla.'
1673 1675 ),
1674 1676 license='GNU GPLv2 or any later version',
1675 1677 classifiers=[
1676 1678 'Development Status :: 6 - Mature',
1677 1679 'Environment :: Console',
1678 1680 'Intended Audience :: Developers',
1679 1681 'Intended Audience :: System Administrators',
1680 1682 'License :: OSI Approved :: GNU General Public License (GPL)',
1681 1683 'Natural Language :: Danish',
1682 1684 'Natural Language :: English',
1683 1685 'Natural Language :: German',
1684 1686 'Natural Language :: Italian',
1685 1687 'Natural Language :: Japanese',
1686 1688 'Natural Language :: Portuguese (Brazilian)',
1687 1689 'Operating System :: Microsoft :: Windows',
1688 1690 'Operating System :: OS Independent',
1689 1691 'Operating System :: POSIX',
1690 1692 'Programming Language :: C',
1691 1693 'Programming Language :: Python',
1692 1694 'Topic :: Software Development :: Version Control',
1693 1695 ],
1694 1696 scripts=scripts,
1695 1697 packages=packages,
1696 1698 ext_modules=extmodules,
1697 1699 data_files=datafiles,
1698 1700 package_data=packagedata,
1699 1701 cmdclass=cmdclass,
1700 1702 distclass=hgdist,
1701 1703 options={
1702 1704 'py2exe': {
1703 1705 'bundle_files': 3,
1704 1706 'dll_excludes': py2exedllexcludes,
1705 1707 'excludes': py2exeexcludes,
1706 1708 'packages': py2exepackages,
1707 1709 },
1708 1710 'bdist_mpkg': {
1709 1711 'zipdist': False,
1710 1712 'license': 'COPYING',
1711 1713 'readme': 'contrib/packaging/macosx/Readme.html',
1712 1714 'welcome': 'contrib/packaging/macosx/Welcome.html',
1713 1715 },
1714 1716 },
1715 1717 **extra
1716 1718 )
@@ -1,47 +1,47 b''
1 1 #require test-repo
2 2
3 3 $ . "$TESTDIR/helpers-testrepo.sh"
4 4
5 5 Sanity check check-config.py
6 6
7 7 $ cat > testfile.py << EOF
8 8 > # Good
9 9 > foo = ui.config('ui', 'username')
10 10 > # Missing
11 11 > foo = ui.config('ui', 'doesnotexist')
12 12 > # Missing different type
13 13 > foo = ui.configint('ui', 'missingint')
14 14 > # Missing with default value
15 15 > foo = ui.configbool('ui', 'missingbool1', default=True)
16 16 > foo = ui.configbool('ui', 'missingbool2', False)
17 17 > # Inconsistent values for defaults.
18 18 > foo = ui.configint('ui', 'intdefault', default=1)
19 19 > foo = ui.configint('ui', 'intdefault', default=42)
20 20 > # Can suppress inconsistent value error
21 21 > foo = ui.configint('ui', 'intdefault2', default=1)
22 22 > # inconsistent config: ui.intdefault2
23 23 > foo = ui.configint('ui', 'intdefault2', default=42)
24 24 > EOF
25 25
26 26 $ cat > files << EOF
27 > mercurial/help/config.txt
27 > mercurial/helptext/config.txt
28 28 > $TESTTMP/testfile.py
29 29 > EOF
30 30
31 31 $ cd "$TESTDIR"/..
32 32
33 33 $ "$PYTHON" contrib/check-config.py < $TESTTMP/files
34 34 foo = ui.configint('ui', 'intdefault', default=42)
35 35 conflict on ui.intdefault: ('int', '42') != ('int', '1')
36 36 at $TESTTMP/testfile.py:12:
37 37 undocumented: ui.doesnotexist (str)
38 38 undocumented: ui.intdefault (int) [42]
39 39 undocumented: ui.intdefault2 (int) [42]
40 40 undocumented: ui.missingbool1 (bool) [True]
41 41 undocumented: ui.missingbool2 (bool)
42 42 undocumented: ui.missingint (int)
43 43
44 44 New errors are not allowed. Warnings are strongly discouraged.
45 45
46 46 $ testrepohg files "set:(**.py or **.txt) - tests/**" | sed 's|\\|/|g' |
47 47 > "$PYTHON" contrib/check-config.py
General Comments 0
You need to be logged in to leave comments. Login now