##// END OF EJS Templates
testing: add interface unit tests for file storage...
Gregory Szorc -
r39808:ae531f5e default
parent child Browse files
Show More
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
This diff has been collapsed as it changes many lines, (984 lines changed) Show them Hide them
@@ -0,0 +1,984
1 # storage.py - Testing of storage primitives.
2 #
3 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
4 #
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
7
8 from __future__ import absolute_import
9
10 import unittest
11
12 from ..node import (
13 hex,
14 nullid,
15 nullrev,
16 )
17 from .. import (
18 error,
19 mdiff,
20 revlog,
21 )
22
23 class basetestcase(unittest.TestCase):
24 if not getattr(unittest.TestCase, r'assertRaisesRegex', False):
25 assertRaisesRegex = (# camelcase-required
26 unittest.TestCase.assertRaisesRegexp)
27
28 class revisiondeltarequest(object):
29 def __init__(self, node, p1, p2, linknode, basenode, ellipsis):
30 self.node = node
31 self.p1node = p1
32 self.p2node = p2
33 self.linknode = linknode
34 self.basenode = basenode
35 self.ellipsis = ellipsis
36
37 class ifileindextests(basetestcase):
38 """Generic tests for the ifileindex interface.
39
40 All file storage backends for index data should conform to the tests in this
41 class.
42
43 Use ``makeifileindextests()`` to create an instance of this type.
44 """
45 def testempty(self):
46 f = self._makefilefn()
47 self.assertEqual(len(f), 0, 'new file store has 0 length by default')
48 self.assertEqual(list(f), [], 'iter yields nothing by default')
49
50 gen = iter(f)
51 with self.assertRaises(StopIteration):
52 next(gen)
53
54 # revs() should evaluate to an empty list.
55 self.assertEqual(list(f.revs()), [])
56
57 revs = iter(f.revs())
58 with self.assertRaises(StopIteration):
59 next(revs)
60
61 self.assertEqual(list(f.revs(start=20)), [])
62
63 # parents() and parentrevs() work with nullid/nullrev.
64 self.assertEqual(f.parents(nullid), (nullid, nullid))
65 self.assertEqual(f.parentrevs(nullrev), (nullrev, nullrev))
66
67 with self.assertRaises(error.LookupError):
68 f.parents(b'\x01' * 20)
69
70 for i in range(-5, 5):
71 if i == nullrev:
72 continue
73
74 with self.assertRaises(IndexError):
75 f.parentrevs(i)
76
77 # nullid/nullrev lookup always works.
78 self.assertEqual(f.rev(nullid), nullrev)
79 self.assertEqual(f.node(nullrev), nullid)
80
81 with self.assertRaises(error.LookupError):
82 f.rev(b'\x01' * 20)
83
84 for i in range(-5, 5):
85 if i == nullrev:
86 continue
87
88 with self.assertRaises(IndexError):
89 f.node(i)
90
91 self.assertEqual(f.lookup(nullid), nullid)
92 self.assertEqual(f.lookup(nullrev), nullid)
93 self.assertEqual(f.lookup(hex(nullid)), nullid)
94
95 # String converted to integer doesn't work for nullrev.
96 with self.assertRaises(error.LookupError):
97 f.lookup(b'%d' % nullrev)
98
99 self.assertEqual(f.linkrev(nullrev), nullrev)
100
101 for i in range(-5, 5):
102 if i == nullrev:
103 continue
104
105 with self.assertRaises(IndexError):
106 f.linkrev(i)
107
108 self.assertEqual(f.flags(nullrev), 0)
109
110 for i in range(-5, 5):
111 if i == nullrev:
112 continue
113
114 with self.assertRaises(IndexError):
115 f.flags(i)
116
117 self.assertFalse(f.iscensored(nullrev))
118
119 for i in range(-5, 5):
120 if i == nullrev:
121 continue
122
123 with self.assertRaises(IndexError):
124 f.iscensored(i)
125
126 self.assertEqual(list(f.commonancestorsheads(nullid, nullid)), [])
127
128 with self.assertRaises(ValueError):
129 self.assertEqual(list(f.descendants([])), [])
130
131 self.assertEqual(list(f.descendants([nullrev])), [])
132
133 self.assertEqual(f.headrevs(), [nullrev])
134 self.assertEqual(f.heads(), [nullid])
135 self.assertEqual(f.heads(nullid), [nullid])
136 self.assertEqual(f.heads(None, [nullid]), [nullid])
137 self.assertEqual(f.heads(nullid, [nullid]), [nullid])
138
139 self.assertEqual(f.children(nullid), [])
140
141 with self.assertRaises(error.LookupError):
142 f.children(b'\x01' * 20)
143
144 self.assertEqual(f.deltaparent(nullrev), nullrev)
145
146 for i in range(-5, 5):
147 if i == nullrev:
148 continue
149
150 with self.assertRaises(IndexError):
151 f.deltaparent(i)
152
153 def testsinglerevision(self):
154 f = self._makefilefn()
155 with self._maketransactionfn() as tr:
156 node = f.add(b'initial', None, tr, 0, nullid, nullid)
157
158 self.assertEqual(len(f), 1)
159 self.assertEqual(list(f), [0])
160
161 gen = iter(f)
162 self.assertEqual(next(gen), 0)
163
164 with self.assertRaises(StopIteration):
165 next(gen)
166
167 self.assertEqual(list(f.revs()), [0])
168 self.assertEqual(list(f.revs(start=1)), [])
169 self.assertEqual(list(f.revs(start=0)), [0])
170 self.assertEqual(list(f.revs(stop=0)), [0])
171 self.assertEqual(list(f.revs(stop=1)), [0])
172 self.assertEqual(list(f.revs(1, 1)), [])
173 # TODO buggy
174 self.assertEqual(list(f.revs(1, 0)), [1, 0])
175 self.assertEqual(list(f.revs(2, 0)), [2, 1, 0])
176
177 self.assertEqual(f.parents(node), (nullid, nullid))
178 self.assertEqual(f.parentrevs(0), (nullrev, nullrev))
179
180 with self.assertRaises(error.LookupError):
181 f.parents(b'\x01' * 20)
182
183 with self.assertRaises(IndexError):
184 f.parentrevs(1)
185
186 self.assertEqual(f.rev(node), 0)
187
188 with self.assertRaises(error.LookupError):
189 f.rev(b'\x01' * 20)
190
191 self.assertEqual(f.node(0), node)
192
193 with self.assertRaises(IndexError):
194 f.node(1)
195
196 self.assertEqual(f.lookup(node), node)
197 self.assertEqual(f.lookup(0), node)
198 self.assertEqual(f.lookup(b'0'), node)
199 self.assertEqual(f.lookup(hex(node)), node)
200
201 self.assertEqual(f.linkrev(0), 0)
202
203 with self.assertRaises(IndexError):
204 f.linkrev(1)
205
206 self.assertEqual(f.flags(0), 0)
207
208 with self.assertRaises(IndexError):
209 f.flags(1)
210
211 self.assertFalse(f.iscensored(0))
212
213 with self.assertRaises(IndexError):
214 f.iscensored(1)
215
216 self.assertEqual(list(f.descendants([0])), [])
217
218 self.assertEqual(f.headrevs(), [0])
219
220 self.assertEqual(f.heads(), [node])
221 self.assertEqual(f.heads(node), [node])
222 self.assertEqual(f.heads(stop=[node]), [node])
223
224 with self.assertRaises(error.LookupError):
225 f.heads(stop=[b'\x01' * 20])
226
227 self.assertEqual(f.children(node), [])
228
229 self.assertEqual(f.deltaparent(0), nullrev)
230
231 def testmultiplerevisions(self):
232 fulltext0 = b'x' * 1024
233 fulltext1 = fulltext0 + b'y'
234 fulltext2 = b'y' + fulltext0 + b'z'
235
236 f = self._makefilefn()
237 with self._maketransactionfn() as tr:
238 node0 = f.add(fulltext0, None, tr, 0, nullid, nullid)
239 node1 = f.add(fulltext1, None, tr, 1, node0, nullid)
240 node2 = f.add(fulltext2, None, tr, 3, node1, nullid)
241
242 self.assertEqual(len(f), 3)
243 self.assertEqual(list(f), [0, 1, 2])
244
245 gen = iter(f)
246 self.assertEqual(next(gen), 0)
247 self.assertEqual(next(gen), 1)
248 self.assertEqual(next(gen), 2)
249
250 with self.assertRaises(StopIteration):
251 next(gen)
252
253 self.assertEqual(list(f.revs()), [0, 1, 2])
254 self.assertEqual(list(f.revs(0)), [0, 1, 2])
255 self.assertEqual(list(f.revs(1)), [1, 2])
256 self.assertEqual(list(f.revs(2)), [2])
257 self.assertEqual(list(f.revs(3)), [])
258 self.assertEqual(list(f.revs(stop=1)), [0, 1])
259 self.assertEqual(list(f.revs(stop=2)), [0, 1, 2])
260 self.assertEqual(list(f.revs(stop=3)), [0, 1, 2])
261 self.assertEqual(list(f.revs(2, 0)), [2, 1, 0])
262 self.assertEqual(list(f.revs(2, 1)), [2, 1])
263 # TODO this is wrong
264 self.assertEqual(list(f.revs(3, 2)), [3, 2])
265
266 self.assertEqual(f.parents(node0), (nullid, nullid))
267 self.assertEqual(f.parents(node1), (node0, nullid))
268 self.assertEqual(f.parents(node2), (node1, nullid))
269
270 self.assertEqual(f.parentrevs(0), (nullrev, nullrev))
271 self.assertEqual(f.parentrevs(1), (0, nullrev))
272 self.assertEqual(f.parentrevs(2), (1, nullrev))
273
274 self.assertEqual(f.rev(node0), 0)
275 self.assertEqual(f.rev(node1), 1)
276 self.assertEqual(f.rev(node2), 2)
277
278 with self.assertRaises(error.LookupError):
279 f.rev(b'\x01' * 20)
280
281 self.assertEqual(f.node(0), node0)
282 self.assertEqual(f.node(1), node1)
283 self.assertEqual(f.node(2), node2)
284
285 with self.assertRaises(IndexError):
286 f.node(3)
287
288 self.assertEqual(f.lookup(node0), node0)
289 self.assertEqual(f.lookup(0), node0)
290 self.assertEqual(f.lookup(b'0'), node0)
291 self.assertEqual(f.lookup(hex(node0)), node0)
292
293 self.assertEqual(f.lookup(node1), node1)
294 self.assertEqual(f.lookup(1), node1)
295 self.assertEqual(f.lookup(b'1'), node1)
296 self.assertEqual(f.lookup(hex(node1)), node1)
297
298 self.assertEqual(f.linkrev(0), 0)
299 self.assertEqual(f.linkrev(1), 1)
300 self.assertEqual(f.linkrev(2), 3)
301
302 with self.assertRaises(IndexError):
303 f.linkrev(3)
304
305 self.assertEqual(f.flags(0), 0)
306 self.assertEqual(f.flags(1), 0)
307 self.assertEqual(f.flags(2), 0)
308
309 with self.assertRaises(IndexError):
310 f.flags(3)
311
312 self.assertFalse(f.iscensored(0))
313 self.assertFalse(f.iscensored(1))
314 self.assertFalse(f.iscensored(2))
315
316 with self.assertRaises(IndexError):
317 f.iscensored(3)
318
319 self.assertEqual(f.commonancestorsheads(node1, nullid), [])
320 self.assertEqual(f.commonancestorsheads(node1, node0), [node0])
321 self.assertEqual(f.commonancestorsheads(node1, node1), [node1])
322 self.assertEqual(f.commonancestorsheads(node0, node1), [node0])
323 self.assertEqual(f.commonancestorsheads(node1, node2), [node1])
324 self.assertEqual(f.commonancestorsheads(node2, node1), [node1])
325
326 self.assertEqual(list(f.descendants([0])), [1, 2])
327 self.assertEqual(list(f.descendants([1])), [2])
328 self.assertEqual(list(f.descendants([0, 1])), [1, 2])
329
330 self.assertEqual(f.headrevs(), [2])
331
332 self.assertEqual(f.heads(), [node2])
333 self.assertEqual(f.heads(node0), [node2])
334 self.assertEqual(f.heads(node1), [node2])
335 self.assertEqual(f.heads(node2), [node2])
336
337 # TODO this behavior seems wonky. Is it correct? If so, the
338 # docstring for heads() should be updated to reflect desired
339 # behavior.
340 self.assertEqual(f.heads(stop=[node1]), [node1, node2])
341 self.assertEqual(f.heads(stop=[node0]), [node0, node2])
342 self.assertEqual(f.heads(stop=[node1, node2]), [node1, node2])
343
344 with self.assertRaises(error.LookupError):
345 f.heads(stop=[b'\x01' * 20])
346
347 self.assertEqual(f.children(node0), [node1])
348 self.assertEqual(f.children(node1), [node2])
349 self.assertEqual(f.children(node2), [])
350
351 self.assertEqual(f.deltaparent(0), nullrev)
352 self.assertEqual(f.deltaparent(1), 0)
353 self.assertEqual(f.deltaparent(2), 1)
354
355 def testmultipleheads(self):
356 f = self._makefilefn()
357
358 with self._maketransactionfn() as tr:
359 node0 = f.add(b'0', None, tr, 0, nullid, nullid)
360 node1 = f.add(b'1', None, tr, 1, node0, nullid)
361 node2 = f.add(b'2', None, tr, 2, node1, nullid)
362 node3 = f.add(b'3', None, tr, 3, node0, nullid)
363 node4 = f.add(b'4', None, tr, 4, node3, nullid)
364 node5 = f.add(b'5', None, tr, 5, node0, nullid)
365
366 self.assertEqual(len(f), 6)
367
368 self.assertEqual(list(f.descendants([0])), [1, 2, 3, 4, 5])
369 self.assertEqual(list(f.descendants([1])), [2])
370 self.assertEqual(list(f.descendants([2])), [])
371 self.assertEqual(list(f.descendants([3])), [4])
372 self.assertEqual(list(f.descendants([0, 1])), [1, 2, 3, 4, 5])
373 self.assertEqual(list(f.descendants([1, 3])), [2, 4])
374
375 self.assertEqual(f.headrevs(), [2, 4, 5])
376
377 self.assertEqual(f.heads(), [node2, node4, node5])
378 self.assertEqual(f.heads(node0), [node2, node4, node5])
379 self.assertEqual(f.heads(node1), [node2])
380 self.assertEqual(f.heads(node2), [node2])
381 self.assertEqual(f.heads(node3), [node4])
382 self.assertEqual(f.heads(node4), [node4])
383 self.assertEqual(f.heads(node5), [node5])
384
385 # TODO this seems wrong.
386 self.assertEqual(f.heads(stop=[node0]), [node0, node2, node4, node5])
387 self.assertEqual(f.heads(stop=[node1]), [node1, node2, node4, node5])
388
389 self.assertEqual(f.children(node0), [node1, node3, node5])
390 self.assertEqual(f.children(node1), [node2])
391 self.assertEqual(f.children(node2), [])
392 self.assertEqual(f.children(node3), [node4])
393 self.assertEqual(f.children(node4), [])
394 self.assertEqual(f.children(node5), [])
395
396 class ifiledatatests(basetestcase):
397 """Generic tests for the ifiledata interface.
398
399 All file storage backends for data should conform to the tests in this
400 class.
401
402 Use ``makeifiledatatests()`` to create an instance of this type.
403 """
404 def testempty(self):
405 f = self._makefilefn()
406
407 self.assertEqual(f.rawsize(nullrev), 0)
408
409 for i in range(-5, 5):
410 if i == nullrev:
411 continue
412
413 with self.assertRaises(IndexError):
414 f.rawsize(i)
415
416 self.assertEqual(f.size(nullrev), 0)
417
418 for i in range(-5, 5):
419 if i == nullrev:
420 continue
421
422 with self.assertRaises(IndexError):
423 f.size(i)
424
425 with self.assertRaises(error.RevlogError):
426 f.checkhash(b'', nullid)
427
428 with self.assertRaises(error.LookupError):
429 f.checkhash(b'', b'\x01' * 20)
430
431 self.assertEqual(f.revision(nullid), b'')
432 self.assertEqual(f.revision(nullid, raw=True), b'')
433
434 with self.assertRaises(error.LookupError):
435 f.revision(b'\x01' * 20)
436
437 self.assertEqual(f.read(nullid), b'')
438
439 with self.assertRaises(error.LookupError):
440 f.read(b'\x01' * 20)
441
442 self.assertFalse(f.renamed(nullid))
443
444 with self.assertRaises(error.LookupError):
445 f.read(b'\x01' * 20)
446
447 self.assertTrue(f.cmp(nullid, b''))
448 self.assertTrue(f.cmp(nullid, b'foo'))
449
450 with self.assertRaises(error.LookupError):
451 f.cmp(b'\x01' * 20, b'irrelevant')
452
453 self.assertEqual(f.revdiff(nullrev, nullrev), b'')
454
455 with self.assertRaises(IndexError):
456 f.revdiff(0, nullrev)
457
458 with self.assertRaises(IndexError):
459 f.revdiff(nullrev, 0)
460
461 with self.assertRaises(IndexError):
462 f.revdiff(0, 0)
463
464 gen = f.emitrevisiondeltas([])
465 with self.assertRaises(StopIteration):
466 next(gen)
467
468 requests = [
469 revisiondeltarequest(nullid, nullid, nullid, nullid, nullid, False),
470 ]
471 gen = f.emitrevisiondeltas(requests)
472
473 delta = next(gen)
474
475 self.assertEqual(delta.node, nullid)
476 self.assertEqual(delta.p1node, nullid)
477 self.assertEqual(delta.p2node, nullid)
478 self.assertEqual(delta.linknode, nullid)
479 self.assertEqual(delta.basenode, nullid)
480 self.assertIsNone(delta.baserevisionsize)
481 self.assertEqual(delta.revision, b'')
482 self.assertIsNone(delta.delta)
483
484 with self.assertRaises(StopIteration):
485 next(gen)
486
487 requests = [
488 revisiondeltarequest(nullid, nullid, nullid, nullid, nullid, False),
489 revisiondeltarequest(nullid, b'\x01' * 20, b'\x02' * 20,
490 b'\x03' * 20, nullid, False)
491 ]
492
493 gen = f.emitrevisiondeltas(requests)
494
495 next(gen)
496 delta = next(gen)
497
498 self.assertEqual(delta.node, nullid)
499 self.assertEqual(delta.p1node, b'\x01' * 20)
500 self.assertEqual(delta.p2node, b'\x02' * 20)
501 self.assertEqual(delta.linknode, b'\x03' * 20)
502 self.assertEqual(delta.basenode, nullid)
503 self.assertIsNone(delta.baserevisionsize)
504 self.assertEqual(delta.revision, b'')
505 self.assertIsNone(delta.delta)
506
507 with self.assertRaises(StopIteration):
508 next(gen)
509
510 def testsinglerevision(self):
511 fulltext = b'initial'
512
513 f = self._makefilefn()
514 with self._maketransactionfn() as tr:
515 node = f.add(fulltext, None, tr, 0, nullid, nullid)
516
517 self.assertEqual(f.rawsize(0), len(fulltext))
518
519 with self.assertRaises(IndexError):
520 f.rawsize(1)
521
522 self.assertEqual(f.size(0), len(fulltext))
523
524 with self.assertRaises(IndexError):
525 f.size(1)
526
527 f.checkhash(fulltext, node)
528 f.checkhash(fulltext, node, nullid, nullid)
529
530 with self.assertRaises(error.RevlogError):
531 f.checkhash(fulltext + b'extra', node)
532
533 with self.assertRaises(error.RevlogError):
534 f.checkhash(fulltext, node, b'\x01' * 20, nullid)
535
536 with self.assertRaises(error.RevlogError):
537 f.checkhash(fulltext, node, nullid, b'\x01' * 20)
538
539 self.assertEqual(f.revision(node), fulltext)
540 self.assertEqual(f.revision(node, raw=True), fulltext)
541
542 self.assertEqual(f.read(node), fulltext)
543
544 self.assertFalse(f.renamed(node))
545
546 self.assertFalse(f.cmp(node, fulltext))
547 self.assertTrue(f.cmp(node, fulltext + b'extra'))
548
549 self.assertEqual(f.revdiff(0, 0), b'')
550 self.assertEqual(f.revdiff(nullrev, 0),
551 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07%s' %
552 fulltext)
553
554 self.assertEqual(f.revdiff(0, nullrev),
555 b'\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x00')
556
557 requests = [
558 revisiondeltarequest(node, nullid, nullid, nullid, nullid, False),
559 ]
560 gen = f.emitrevisiondeltas(requests)
561
562 delta = next(gen)
563
564 self.assertEqual(delta.node, node)
565 self.assertEqual(delta.p1node, nullid)
566 self.assertEqual(delta.p2node, nullid)
567 self.assertEqual(delta.linknode, nullid)
568 self.assertEqual(delta.basenode, nullid)
569 self.assertIsNone(delta.baserevisionsize)
570 self.assertEqual(delta.revision, fulltext)
571 self.assertIsNone(delta.delta)
572
573 with self.assertRaises(StopIteration):
574 next(gen)
575
576 def testmultiplerevisions(self):
577 fulltext0 = b'x' * 1024
578 fulltext1 = fulltext0 + b'y'
579 fulltext2 = b'y' + fulltext0 + b'z'
580
581 f = self._makefilefn()
582 with self._maketransactionfn() as tr:
583 node0 = f.add(fulltext0, None, tr, 0, nullid, nullid)
584 node1 = f.add(fulltext1, None, tr, 1, node0, nullid)
585 node2 = f.add(fulltext2, None, tr, 3, node1, nullid)
586
587 self.assertEqual(f.rawsize(0), len(fulltext0))
588 self.assertEqual(f.rawsize(1), len(fulltext1))
589 self.assertEqual(f.rawsize(2), len(fulltext2))
590
591 with self.assertRaises(IndexError):
592 f.rawsize(3)
593
594 self.assertEqual(f.size(0), len(fulltext0))
595 self.assertEqual(f.size(1), len(fulltext1))
596 self.assertEqual(f.size(2), len(fulltext2))
597
598 with self.assertRaises(IndexError):
599 f.size(3)
600
601 f.checkhash(fulltext0, node0)
602 f.checkhash(fulltext1, node1)
603 f.checkhash(fulltext1, node1, node0, nullid)
604 f.checkhash(fulltext2, node2, node1, nullid)
605
606 with self.assertRaises(error.RevlogError):
607 f.checkhash(fulltext1, b'\x01' * 20)
608
609 with self.assertRaises(error.RevlogError):
610 f.checkhash(fulltext1 + b'extra', node1, node0, nullid)
611
612 with self.assertRaises(error.RevlogError):
613 f.checkhash(fulltext1, node1, node0, node0)
614
615 self.assertEqual(f.revision(node0), fulltext0)
616 self.assertEqual(f.revision(node0, raw=True), fulltext0)
617 self.assertEqual(f.revision(node1), fulltext1)
618 self.assertEqual(f.revision(node1, raw=True), fulltext1)
619 self.assertEqual(f.revision(node2), fulltext2)
620 self.assertEqual(f.revision(node2, raw=True), fulltext2)
621
622 with self.assertRaises(error.LookupError):
623 f.revision(b'\x01' * 20)
624
625 self.assertEqual(f.read(node0), fulltext0)
626 self.assertEqual(f.read(node1), fulltext1)
627 self.assertEqual(f.read(node2), fulltext2)
628
629 with self.assertRaises(error.LookupError):
630 f.read(b'\x01' * 20)
631
632 self.assertFalse(f.renamed(node0))
633 self.assertFalse(f.renamed(node1))
634 self.assertFalse(f.renamed(node2))
635
636 with self.assertRaises(error.LookupError):
637 f.renamed(b'\x01' * 20)
638
639 self.assertFalse(f.cmp(node0, fulltext0))
640 self.assertFalse(f.cmp(node1, fulltext1))
641 self.assertFalse(f.cmp(node2, fulltext2))
642
643 self.assertTrue(f.cmp(node1, fulltext0))
644 self.assertTrue(f.cmp(node2, fulltext1))
645
646 with self.assertRaises(error.LookupError):
647 f.cmp(b'\x01' * 20, b'irrelevant')
648
649 self.assertEqual(f.revdiff(0, 1),
650 b'\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x04\x01' +
651 fulltext1)
652
653 self.assertEqual(f.revdiff(0, 2),
654 b'\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x04\x02' +
655 fulltext2)
656
657 requests = [
658 revisiondeltarequest(node0, nullid, nullid, b'\x01' * 20, nullid,
659 False),
660 revisiondeltarequest(node1, node0, nullid, b'\x02' * 20, node0,
661 False),
662 revisiondeltarequest(node2, node1, nullid, b'\x03' * 20, node1,
663 False),
664 ]
665 gen = f.emitrevisiondeltas(requests)
666
667 delta = next(gen)
668
669 self.assertEqual(delta.node, node0)
670 self.assertEqual(delta.p1node, nullid)
671 self.assertEqual(delta.p2node, nullid)
672 self.assertEqual(delta.linknode, b'\x01' * 20)
673 self.assertEqual(delta.basenode, nullid)
674 self.assertIsNone(delta.baserevisionsize)
675 self.assertEqual(delta.revision, fulltext0)
676 self.assertIsNone(delta.delta)
677
678 delta = next(gen)
679
680 self.assertEqual(delta.node, node1)
681 self.assertEqual(delta.p1node, node0)
682 self.assertEqual(delta.p2node, nullid)
683 self.assertEqual(delta.linknode, b'\x02' * 20)
684 self.assertEqual(delta.basenode, node0)
685 self.assertIsNone(delta.baserevisionsize)
686 self.assertIsNone(delta.revision)
687 self.assertEqual(delta.delta,
688 b'\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x04\x01' +
689 fulltext1)
690
691 delta = next(gen)
692
693 self.assertEqual(delta.node, node2)
694 self.assertEqual(delta.p1node, node1)
695 self.assertEqual(delta.p2node, nullid)
696 self.assertEqual(delta.linknode, b'\x03' * 20)
697 self.assertEqual(delta.basenode, node1)
698 self.assertIsNone(delta.baserevisionsize)
699 self.assertIsNone(delta.revision)
700 self.assertEqual(delta.delta,
701 b'\x00\x00\x00\x00\x00\x00\x04\x01\x00\x00\x04\x02' +
702 fulltext2)
703
704 with self.assertRaises(StopIteration):
705 next(gen)
706
707 def testrenamed(self):
708 fulltext0 = b'foo'
709 fulltext1 = b'bar'
710 fulltext2 = b'baz'
711
712 meta1 = {
713 b'copy': b'source0',
714 b'copyrev': b'a' * 40,
715 }
716
717 meta2 = {
718 b'copy': b'source1',
719 b'copyrev': b'b' * 40,
720 }
721
722 stored1 = b''.join([
723 b'\x01\ncopy: source0\n',
724 b'copyrev: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\x01\n',
725 fulltext1,
726 ])
727
728 stored2 = b''.join([
729 b'\x01\ncopy: source1\n',
730 b'copyrev: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n\x01\n',
731 fulltext2,
732 ])
733
734 f = self._makefilefn()
735 with self._maketransactionfn() as tr:
736 node0 = f.add(fulltext0, None, tr, 0, nullid, nullid)
737 node1 = f.add(fulltext1, meta1, tr, 1, node0, nullid)
738 node2 = f.add(fulltext2, meta2, tr, 2, nullid, nullid)
739
740 self.assertEqual(f.rawsize(1), len(stored1))
741 self.assertEqual(f.rawsize(2), len(stored2))
742
743 # Metadata header isn't recognized when parent isn't nullid.
744 self.assertEqual(f.size(1), len(stored1))
745 self.assertEqual(f.size(2), len(fulltext2))
746
747 self.assertEqual(f.revision(node1), stored1)
748 self.assertEqual(f.revision(node1, raw=True), stored1)
749 self.assertEqual(f.revision(node2), stored2)
750 self.assertEqual(f.revision(node2, raw=True), stored2)
751
752 self.assertEqual(f.read(node1), fulltext1)
753 self.assertEqual(f.read(node2), fulltext2)
754
755 # Returns False when first parent is set.
756 self.assertFalse(f.renamed(node1))
757 self.assertEqual(f.renamed(node2), (b'source1', b'\xbb' * 20))
758
759 self.assertTrue(f.cmp(node1, fulltext1))
760 self.assertTrue(f.cmp(node1, stored1))
761 self.assertFalse(f.cmp(node2, fulltext2))
762 self.assertTrue(f.cmp(node2, stored2))
763
764 def testmetadataprefix(self):
765 # Content with metadata prefix has extra prefix inserted in storage.
766 fulltext0 = b'\x01\nfoo'
767 stored0 = b'\x01\n\x01\n\x01\nfoo'
768
769 fulltext1 = b'\x01\nbar'
770 meta1 = {
771 b'copy': b'source0',
772 b'copyrev': b'b' * 40,
773 }
774 stored1 = b''.join([
775 b'\x01\ncopy: source0\n',
776 b'copyrev: %s\n' % (b'b' * 40),
777 b'\x01\n\x01\nbar',
778 ])
779
780 f = self._makefilefn()
781 with self._maketransactionfn() as tr:
782 node0 = f.add(fulltext0, {}, tr, 0, nullid, nullid)
783 node1 = f.add(fulltext1, meta1, tr, 1, nullid, nullid)
784
785 self.assertEqual(f.rawsize(0), len(stored0))
786 self.assertEqual(f.rawsize(1), len(stored1))
787
788 # TODO this is buggy.
789 self.assertEqual(f.size(0), len(fulltext0) + 4)
790
791 self.assertEqual(f.size(1), len(fulltext1))
792
793 self.assertEqual(f.revision(node0), stored0)
794 self.assertEqual(f.revision(node0, raw=True), stored0)
795
796 self.assertEqual(f.revision(node1), stored1)
797 self.assertEqual(f.revision(node1, raw=True), stored1)
798
799 self.assertEqual(f.read(node0), fulltext0)
800 self.assertEqual(f.read(node1), fulltext1)
801
802 self.assertFalse(f.cmp(node0, fulltext0))
803 self.assertTrue(f.cmp(node0, stored0))
804
805 self.assertFalse(f.cmp(node1, fulltext1))
806 self.assertTrue(f.cmp(node1, stored0))
807
808 def testcensored(self):
809 f = self._makefilefn()
810
811 stored1 = revlog.packmeta({
812 b'censored': b'tombstone',
813 }, b'')
814
815 # TODO tests are incomplete because we need the node to be
816 # different due to presence of censor metadata. But we can't
817 # do this with addrevision().
818 with self._maketransactionfn() as tr:
819 node0 = f.add(b'foo', None, tr, 0, nullid, nullid)
820 f.addrevision(stored1, tr, 1, node0, nullid,
821 flags=revlog.REVIDX_ISCENSORED)
822
823 self.assertEqual(f.flags(1), revlog.REVIDX_ISCENSORED)
824 self.assertTrue(f.iscensored(1))
825
826 self.assertEqual(f.revision(1), stored1)
827 self.assertEqual(f.revision(1, raw=True), stored1)
828
829 self.assertEqual(f.read(1), b'')
830
831 class ifilemutationtests(basetestcase):
832 """Generic tests for the ifilemutation interface.
833
834 All file storage backends that support writing should conform to this
835 interface.
836
837 Use ``makeifilemutationtests()`` to create an instance of this type.
838 """
839 def testaddnoop(self):
840 f = self._makefilefn()
841 with self._maketransactionfn() as tr:
842 node0 = f.add(b'foo', None, tr, 0, nullid, nullid)
843 node1 = f.add(b'foo', None, tr, 0, nullid, nullid)
844 # Varying by linkrev shouldn't impact hash.
845 node2 = f.add(b'foo', None, tr, 1, nullid, nullid)
846
847 self.assertEqual(node1, node0)
848 self.assertEqual(node2, node0)
849 self.assertEqual(len(f), 1)
850
851 def testaddrevisionbadnode(self):
852 f = self._makefilefn()
853 with self._maketransactionfn() as tr:
854 # Adding a revision with bad node value fails.
855 with self.assertRaises(error.RevlogError):
856 f.addrevision(b'foo', tr, 0, nullid, nullid, node=b'\x01' * 20)
857
858 def testaddrevisionunknownflag(self):
859 f = self._makefilefn()
860 with self._maketransactionfn() as tr:
861 for i in range(15, 0, -1):
862 if (1 << i) & ~revlog.REVIDX_KNOWN_FLAGS:
863 flags = 1 << i
864 break
865
866 with self.assertRaises(error.RevlogError):
867 f.addrevision(b'foo', tr, 0, nullid, nullid, flags=flags)
868
869 def testaddgroupsimple(self):
870 f = self._makefilefn()
871
872 callbackargs = []
873 def cb(*args, **kwargs):
874 callbackargs.append((args, kwargs))
875
876 def linkmapper(node):
877 return 0
878
879 with self._maketransactionfn() as tr:
880 nodes = f.addgroup([], None, tr, addrevisioncb=cb)
881
882 self.assertEqual(nodes, [])
883 self.assertEqual(callbackargs, [])
884 self.assertEqual(len(f), 0)
885
886 fulltext0 = b'foo'
887 delta0 = mdiff.trivialdiffheader(len(fulltext0)) + fulltext0
888
889 deltas = [
890 (b'\x01' * 20, nullid, nullid, nullid, nullid, delta0, 0),
891 ]
892
893 with self._maketransactionfn() as tr:
894 with self.assertRaises(error.RevlogError):
895 f.addgroup(deltas, linkmapper, tr, addrevisioncb=cb)
896
897 node0 = f.add(fulltext0, None, tr, 0, nullid, nullid)
898
899 f = self._makefilefn()
900
901 deltas = [
902 (node0, nullid, nullid, nullid, nullid, delta0, 0),
903 ]
904
905 with self._maketransactionfn() as tr:
906 nodes = f.addgroup(deltas, linkmapper, tr, addrevisioncb=cb)
907
908 self.assertEqual(nodes, [
909 b'\x49\xd8\xcb\xb1\x5c\xe2\x57\x92\x04\x47'
910 b'\x00\x6b\x46\x97\x8b\x7a\xf9\x80\xa9\x79'])
911
912 self.assertEqual(len(callbackargs), 1)
913 self.assertEqual(callbackargs[0][0][1], nodes[0])
914
915 self.assertEqual(list(f.revs()), [0])
916 self.assertEqual(f.rev(nodes[0]), 0)
917 self.assertEqual(f.node(0), nodes[0])
918
919 def testaddgroupmultiple(self):
920 f = self._makefilefn()
921
922 fulltexts = [
923 b'foo',
924 b'bar',
925 b'x' * 1024,
926 ]
927
928 nodes = []
929 with self._maketransactionfn() as tr:
930 for fulltext in fulltexts:
931 nodes.append(f.add(fulltext, None, tr, 0, nullid, nullid))
932
933 f = self._makefilefn()
934 deltas = []
935 for i, fulltext in enumerate(fulltexts):
936 delta = mdiff.trivialdiffheader(len(fulltext)) + fulltext
937
938 deltas.append((nodes[i], nullid, nullid, nullid, nullid, delta, 0))
939
940 with self._maketransactionfn() as tr:
941 self.assertEqual(f.addgroup(deltas, lambda x: 0, tr), nodes)
942
943 self.assertEqual(len(f), len(deltas))
944 self.assertEqual(list(f.revs()), [0, 1, 2])
945 self.assertEqual(f.rev(nodes[0]), 0)
946 self.assertEqual(f.rev(nodes[1]), 1)
947 self.assertEqual(f.rev(nodes[2]), 2)
948 self.assertEqual(f.node(0), nodes[0])
949 self.assertEqual(f.node(1), nodes[1])
950 self.assertEqual(f.node(2), nodes[2])
951
952 def makeifileindextests(makefilefn, maketransactionfn):
953 """Create a unittest.TestCase class suitable for testing file storage.
954
955 ``makefilefn`` is a callable which receives the test case as an
956 argument and returns an object implementing the ``ifilestorage`` interface.
957
958 ``maketransactionfn`` is a callable which receives the test case as an
959 argument and returns a transaction object.
960
961 Returns a type that is a ``unittest.TestCase`` that can be used for
962 testing the object implementing the file storage interface. Simply
963 assign the returned value to a module-level attribute and a test loader
964 should find and run it automatically.
965 """
966 d = {
967 r'_makefilefn': makefilefn,
968 r'_maketransactionfn': maketransactionfn,
969 }
970 return type(r'ifileindextests', (ifileindextests,), d)
971
972 def makeifiledatatests(makefilefn, maketransactionfn):
973 d = {
974 r'_makefilefn': makefilefn,
975 r'_maketransactionfn': maketransactionfn,
976 }
977 return type(r'ifiledatatests', (ifiledatatests,), d)
978
979 def makeifilemutationtests(makefilefn, maketransactionfn):
980 d = {
981 r'_makefilefn': makefilefn,
982 r'_maketransactionfn': maketransactionfn,
983 }
984 return type(r'ifilemutationtests', (ifilemutationtests,), d)
@@ -0,0 +1,46
1 # This test verifies the conformance of various classes to various
2 # storage interfaces.
3 from __future__ import absolute_import
4
5 import silenttestrunner
6
7 from mercurial import (
8 filelog,
9 transaction,
10 ui as uimod,
11 vfs as vfsmod,
12 )
13
14 from mercurial.testing import (
15 storage as storagetesting,
16 )
17
18 STATE = {
19 'lastindex': 0,
20 'ui': uimod.ui(),
21 'vfs': vfsmod.vfs(b'.', realpath=True),
22 }
23
24 def makefilefn(self):
25 """Factory for filelog instances."""
26 fl = filelog.filelog(STATE['vfs'], 'filelog-%d' % STATE['lastindex'])
27 STATE['lastindex'] += 1
28 return fl
29
30 def maketransaction(self):
31 vfsmap = {'plain': STATE['vfs']}
32
33 return transaction.transaction(STATE['ui'].warn, STATE['vfs'], vfsmap,
34 'journal', 'undo')
35
36 # Assigning module-level attributes that inherit from unittest.TestCase
37 # is all that is needed to register tests.
38 filelogindextests = storagetesting.makeifileindextests(makefilefn,
39 maketransaction)
40 filelogdatatests = storagetesting.makeifiledatatests(makefilefn,
41 maketransaction)
42 filelogmutationtests = storagetesting.makeifilemutationtests(makefilefn,
43 maketransaction)
44
45 if __name__ == '__main__':
46 silenttestrunner.main(__name__)
@@ -1,1098 +1,1099
1 #
1 #
2 # This is the mercurial setup script.
2 # This is the mercurial setup script.
3 #
3 #
4 # 'python setup.py install', or
4 # 'python setup.py install', or
5 # 'python setup.py --help' for more options
5 # 'python setup.py --help' for more options
6
6
7 import os
7 import os
8
8
9 supportedpy = '~= 2.7'
9 supportedpy = '~= 2.7'
10 if os.environ.get('HGALLOWPYTHON3', ''):
10 if os.environ.get('HGALLOWPYTHON3', ''):
11 # Mercurial will never work on Python 3 before 3.5 due to a lack
11 # Mercurial will never work on Python 3 before 3.5 due to a lack
12 # of % formatting on bytestrings, and can't work on 3.6.0 or 3.6.1
12 # of % formatting on bytestrings, and can't work on 3.6.0 or 3.6.1
13 # due to a bug in % formatting in bytestrings.
13 # due to a bug in % formatting in bytestrings.
14 # We cannot support Python 3.5.0, 3.5.1, 3.5.2 because of bug in
14 # We cannot support Python 3.5.0, 3.5.1, 3.5.2 because of bug in
15 # codecs.escape_encode() where it raises SystemError on empty bytestring
15 # codecs.escape_encode() where it raises SystemError on empty bytestring
16 # bug link: https://bugs.python.org/issue25270
16 # bug link: https://bugs.python.org/issue25270
17 #
17 #
18 # TODO: when we actually work on Python 3, use this string as the
18 # TODO: when we actually work on Python 3, use this string as the
19 # actual supportedpy string.
19 # actual supportedpy string.
20 supportedpy = ','.join([
20 supportedpy = ','.join([
21 '>=2.7',
21 '>=2.7',
22 '!=3.0.*',
22 '!=3.0.*',
23 '!=3.1.*',
23 '!=3.1.*',
24 '!=3.2.*',
24 '!=3.2.*',
25 '!=3.3.*',
25 '!=3.3.*',
26 '!=3.4.*',
26 '!=3.4.*',
27 '!=3.5.0',
27 '!=3.5.0',
28 '!=3.5.1',
28 '!=3.5.1',
29 '!=3.5.2',
29 '!=3.5.2',
30 '!=3.6.0',
30 '!=3.6.0',
31 '!=3.6.1',
31 '!=3.6.1',
32 ])
32 ])
33
33
34 import sys, platform
34 import sys, platform
35 if sys.version_info[0] >= 3:
35 if sys.version_info[0] >= 3:
36 printf = eval('print')
36 printf = eval('print')
37 libdir_escape = 'unicode_escape'
37 libdir_escape = 'unicode_escape'
38 def sysstr(s):
38 def sysstr(s):
39 return s.decode('latin-1')
39 return s.decode('latin-1')
40 else:
40 else:
41 libdir_escape = 'string_escape'
41 libdir_escape = 'string_escape'
42 def printf(*args, **kwargs):
42 def printf(*args, **kwargs):
43 f = kwargs.get('file', sys.stdout)
43 f = kwargs.get('file', sys.stdout)
44 end = kwargs.get('end', '\n')
44 end = kwargs.get('end', '\n')
45 f.write(b' '.join(args) + end)
45 f.write(b' '.join(args) + end)
46 def sysstr(s):
46 def sysstr(s):
47 return s
47 return s
48
48
49 # Attempt to guide users to a modern pip - this means that 2.6 users
49 # Attempt to guide users to a modern pip - this means that 2.6 users
50 # should have a chance of getting a 4.2 release, and when we ratchet
50 # should have a chance of getting a 4.2 release, and when we ratchet
51 # the version requirement forward again hopefully everyone will get
51 # the version requirement forward again hopefully everyone will get
52 # something that works for them.
52 # something that works for them.
53 if sys.version_info < (2, 7, 0, 'final'):
53 if sys.version_info < (2, 7, 0, 'final'):
54 pip_message = ('This may be due to an out of date pip. '
54 pip_message = ('This may be due to an out of date pip. '
55 'Make sure you have pip >= 9.0.1.')
55 'Make sure you have pip >= 9.0.1.')
56 try:
56 try:
57 import pip
57 import pip
58 pip_version = tuple([int(x) for x in pip.__version__.split('.')[:3]])
58 pip_version = tuple([int(x) for x in pip.__version__.split('.')[:3]])
59 if pip_version < (9, 0, 1) :
59 if pip_version < (9, 0, 1) :
60 pip_message = (
60 pip_message = (
61 'Your pip version is out of date, please install '
61 'Your pip version is out of date, please install '
62 'pip >= 9.0.1. pip {} detected.'.format(pip.__version__))
62 'pip >= 9.0.1. pip {} detected.'.format(pip.__version__))
63 else:
63 else:
64 # pip is new enough - it must be something else
64 # pip is new enough - it must be something else
65 pip_message = ''
65 pip_message = ''
66 except Exception:
66 except Exception:
67 pass
67 pass
68 error = """
68 error = """
69 Mercurial does not support Python older than 2.7.
69 Mercurial does not support Python older than 2.7.
70 Python {py} detected.
70 Python {py} detected.
71 {pip}
71 {pip}
72 """.format(py=sys.version_info, pip=pip_message)
72 """.format(py=sys.version_info, pip=pip_message)
73 printf(error, file=sys.stderr)
73 printf(error, file=sys.stderr)
74 sys.exit(1)
74 sys.exit(1)
75
75
76 # We don't yet officially support Python 3. But we want to allow developers to
76 # We don't yet officially support Python 3. But we want to allow developers to
77 # hack on. Detect and disallow running on Python 3 by default. But provide a
77 # hack on. Detect and disallow running on Python 3 by default. But provide a
78 # backdoor to enable working on Python 3.
78 # backdoor to enable working on Python 3.
79 if sys.version_info[0] != 2:
79 if sys.version_info[0] != 2:
80 badpython = True
80 badpython = True
81
81
82 # Allow Python 3 from source checkouts.
82 # Allow Python 3 from source checkouts.
83 if os.path.isdir('.hg') or 'HGPYTHON3' in os.environ:
83 if os.path.isdir('.hg') or 'HGPYTHON3' in os.environ:
84 badpython = False
84 badpython = False
85
85
86 if badpython:
86 if badpython:
87 error = """
87 error = """
88 Mercurial only supports Python 2.7.
88 Mercurial only supports Python 2.7.
89 Python {py} detected.
89 Python {py} detected.
90 Please re-run with Python 2.7.
90 Please re-run with Python 2.7.
91 """.format(py=sys.version_info)
91 """.format(py=sys.version_info)
92
92
93 printf(error, file=sys.stderr)
93 printf(error, file=sys.stderr)
94 sys.exit(1)
94 sys.exit(1)
95
95
96 # Solaris Python packaging brain damage
96 # Solaris Python packaging brain damage
97 try:
97 try:
98 import hashlib
98 import hashlib
99 sha = hashlib.sha1()
99 sha = hashlib.sha1()
100 except ImportError:
100 except ImportError:
101 try:
101 try:
102 import sha
102 import sha
103 sha.sha # silence unused import warning
103 sha.sha # silence unused import warning
104 except ImportError:
104 except ImportError:
105 raise SystemExit(
105 raise SystemExit(
106 "Couldn't import standard hashlib (incomplete Python install).")
106 "Couldn't import standard hashlib (incomplete Python install).")
107
107
108 try:
108 try:
109 import zlib
109 import zlib
110 zlib.compressobj # silence unused import warning
110 zlib.compressobj # silence unused import warning
111 except ImportError:
111 except ImportError:
112 raise SystemExit(
112 raise SystemExit(
113 "Couldn't import standard zlib (incomplete Python install).")
113 "Couldn't import standard zlib (incomplete Python install).")
114
114
115 # The base IronPython distribution (as of 2.7.1) doesn't support bz2
115 # The base IronPython distribution (as of 2.7.1) doesn't support bz2
116 isironpython = False
116 isironpython = False
117 try:
117 try:
118 isironpython = (platform.python_implementation()
118 isironpython = (platform.python_implementation()
119 .lower().find("ironpython") != -1)
119 .lower().find("ironpython") != -1)
120 except AttributeError:
120 except AttributeError:
121 pass
121 pass
122
122
123 if isironpython:
123 if isironpython:
124 sys.stderr.write("warning: IronPython detected (no bz2 support)\n")
124 sys.stderr.write("warning: IronPython detected (no bz2 support)\n")
125 else:
125 else:
126 try:
126 try:
127 import bz2
127 import bz2
128 bz2.BZ2Compressor # silence unused import warning
128 bz2.BZ2Compressor # silence unused import warning
129 except ImportError:
129 except ImportError:
130 raise SystemExit(
130 raise SystemExit(
131 "Couldn't import standard bz2 (incomplete Python install).")
131 "Couldn't import standard bz2 (incomplete Python install).")
132
132
133 ispypy = "PyPy" in sys.version
133 ispypy = "PyPy" in sys.version
134
134
135 import ctypes
135 import ctypes
136 import stat, subprocess, time
136 import stat, subprocess, time
137 import re
137 import re
138 import shutil
138 import shutil
139 import tempfile
139 import tempfile
140 from distutils import log
140 from distutils import log
141 # We have issues with setuptools on some platforms and builders. Until
141 # We have issues with setuptools on some platforms and builders. Until
142 # those are resolved, setuptools is opt-in except for platforms where
142 # those are resolved, setuptools is opt-in except for platforms where
143 # we don't have issues.
143 # we don't have issues.
144 issetuptools = (os.name == 'nt' or 'FORCE_SETUPTOOLS' in os.environ)
144 issetuptools = (os.name == 'nt' or 'FORCE_SETUPTOOLS' in os.environ)
145 if issetuptools:
145 if issetuptools:
146 from setuptools import setup
146 from setuptools import setup
147 else:
147 else:
148 from distutils.core import setup
148 from distutils.core import setup
149 from distutils.ccompiler import new_compiler
149 from distutils.ccompiler import new_compiler
150 from distutils.core import Command, Extension
150 from distutils.core import Command, Extension
151 from distutils.dist import Distribution
151 from distutils.dist import Distribution
152 from distutils.command.build import build
152 from distutils.command.build import build
153 from distutils.command.build_ext import build_ext
153 from distutils.command.build_ext import build_ext
154 from distutils.command.build_py import build_py
154 from distutils.command.build_py import build_py
155 from distutils.command.build_scripts import build_scripts
155 from distutils.command.build_scripts import build_scripts
156 from distutils.command.install import install
156 from distutils.command.install import install
157 from distutils.command.install_lib import install_lib
157 from distutils.command.install_lib import install_lib
158 from distutils.command.install_scripts import install_scripts
158 from distutils.command.install_scripts import install_scripts
159 from distutils.spawn import spawn, find_executable
159 from distutils.spawn import spawn, find_executable
160 from distutils import file_util
160 from distutils import file_util
161 from distutils.errors import (
161 from distutils.errors import (
162 CCompilerError,
162 CCompilerError,
163 DistutilsError,
163 DistutilsError,
164 DistutilsExecError,
164 DistutilsExecError,
165 )
165 )
166 from distutils.sysconfig import get_python_inc, get_config_var
166 from distutils.sysconfig import get_python_inc, get_config_var
167 from distutils.version import StrictVersion
167 from distutils.version import StrictVersion
168
168
169 def write_if_changed(path, content):
169 def write_if_changed(path, content):
170 """Write content to a file iff the content hasn't changed."""
170 """Write content to a file iff the content hasn't changed."""
171 if os.path.exists(path):
171 if os.path.exists(path):
172 with open(path, 'rb') as fh:
172 with open(path, 'rb') as fh:
173 current = fh.read()
173 current = fh.read()
174 else:
174 else:
175 current = b''
175 current = b''
176
176
177 if current != content:
177 if current != content:
178 with open(path, 'wb') as fh:
178 with open(path, 'wb') as fh:
179 fh.write(content)
179 fh.write(content)
180
180
181 scripts = ['hg']
181 scripts = ['hg']
182 if os.name == 'nt':
182 if os.name == 'nt':
183 # We remove hg.bat if we are able to build hg.exe.
183 # We remove hg.bat if we are able to build hg.exe.
184 scripts.append('contrib/win32/hg.bat')
184 scripts.append('contrib/win32/hg.bat')
185
185
186 def cancompile(cc, code):
186 def cancompile(cc, code):
187 tmpdir = tempfile.mkdtemp(prefix='hg-install-')
187 tmpdir = tempfile.mkdtemp(prefix='hg-install-')
188 devnull = oldstderr = None
188 devnull = oldstderr = None
189 try:
189 try:
190 fname = os.path.join(tmpdir, 'testcomp.c')
190 fname = os.path.join(tmpdir, 'testcomp.c')
191 f = open(fname, 'w')
191 f = open(fname, 'w')
192 f.write(code)
192 f.write(code)
193 f.close()
193 f.close()
194 # Redirect stderr to /dev/null to hide any error messages
194 # Redirect stderr to /dev/null to hide any error messages
195 # from the compiler.
195 # from the compiler.
196 # This will have to be changed if we ever have to check
196 # This will have to be changed if we ever have to check
197 # for a function on Windows.
197 # for a function on Windows.
198 devnull = open('/dev/null', 'w')
198 devnull = open('/dev/null', 'w')
199 oldstderr = os.dup(sys.stderr.fileno())
199 oldstderr = os.dup(sys.stderr.fileno())
200 os.dup2(devnull.fileno(), sys.stderr.fileno())
200 os.dup2(devnull.fileno(), sys.stderr.fileno())
201 objects = cc.compile([fname], output_dir=tmpdir)
201 objects = cc.compile([fname], output_dir=tmpdir)
202 cc.link_executable(objects, os.path.join(tmpdir, "a.out"))
202 cc.link_executable(objects, os.path.join(tmpdir, "a.out"))
203 return True
203 return True
204 except Exception:
204 except Exception:
205 return False
205 return False
206 finally:
206 finally:
207 if oldstderr is not None:
207 if oldstderr is not None:
208 os.dup2(oldstderr, sys.stderr.fileno())
208 os.dup2(oldstderr, sys.stderr.fileno())
209 if devnull is not None:
209 if devnull is not None:
210 devnull.close()
210 devnull.close()
211 shutil.rmtree(tmpdir)
211 shutil.rmtree(tmpdir)
212
212
213 # simplified version of distutils.ccompiler.CCompiler.has_function
213 # simplified version of distutils.ccompiler.CCompiler.has_function
214 # that actually removes its temporary files.
214 # that actually removes its temporary files.
215 def hasfunction(cc, funcname):
215 def hasfunction(cc, funcname):
216 code = 'int main(void) { %s(); }\n' % funcname
216 code = 'int main(void) { %s(); }\n' % funcname
217 return cancompile(cc, code)
217 return cancompile(cc, code)
218
218
219 def hasheader(cc, headername):
219 def hasheader(cc, headername):
220 code = '#include <%s>\nint main(void) { return 0; }\n' % headername
220 code = '#include <%s>\nint main(void) { return 0; }\n' % headername
221 return cancompile(cc, code)
221 return cancompile(cc, code)
222
222
223 # py2exe needs to be installed to work
223 # py2exe needs to be installed to work
224 try:
224 try:
225 import py2exe
225 import py2exe
226 py2exe.Distribution # silence unused import warning
226 py2exe.Distribution # silence unused import warning
227 py2exeloaded = True
227 py2exeloaded = True
228 # import py2exe's patched Distribution class
228 # import py2exe's patched Distribution class
229 from distutils.core import Distribution
229 from distutils.core import Distribution
230 except ImportError:
230 except ImportError:
231 py2exeloaded = False
231 py2exeloaded = False
232
232
233 def runcmd(cmd, env):
233 def runcmd(cmd, env):
234 p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
234 p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
235 stderr=subprocess.PIPE, env=env)
235 stderr=subprocess.PIPE, env=env)
236 out, err = p.communicate()
236 out, err = p.communicate()
237 return p.returncode, out, err
237 return p.returncode, out, err
238
238
239 class hgcommand(object):
239 class hgcommand(object):
240 def __init__(self, cmd, env):
240 def __init__(self, cmd, env):
241 self.cmd = cmd
241 self.cmd = cmd
242 self.env = env
242 self.env = env
243
243
244 def run(self, args):
244 def run(self, args):
245 cmd = self.cmd + args
245 cmd = self.cmd + args
246 returncode, out, err = runcmd(cmd, self.env)
246 returncode, out, err = runcmd(cmd, self.env)
247 err = filterhgerr(err)
247 err = filterhgerr(err)
248 if err or returncode != 0:
248 if err or returncode != 0:
249 printf("stderr from '%s':" % (' '.join(cmd)), file=sys.stderr)
249 printf("stderr from '%s':" % (' '.join(cmd)), file=sys.stderr)
250 printf(err, file=sys.stderr)
250 printf(err, file=sys.stderr)
251 return ''
251 return ''
252 return out
252 return out
253
253
254 def filterhgerr(err):
254 def filterhgerr(err):
255 # If root is executing setup.py, but the repository is owned by
255 # If root is executing setup.py, but the repository is owned by
256 # another user (as in "sudo python setup.py install") we will get
256 # another user (as in "sudo python setup.py install") we will get
257 # trust warnings since the .hg/hgrc file is untrusted. That is
257 # trust warnings since the .hg/hgrc file is untrusted. That is
258 # fine, we don't want to load it anyway. Python may warn about
258 # fine, we don't want to load it anyway. Python may warn about
259 # a missing __init__.py in mercurial/locale, we also ignore that.
259 # a missing __init__.py in mercurial/locale, we also ignore that.
260 err = [e for e in err.splitlines()
260 err = [e for e in err.splitlines()
261 if (not e.startswith(b'not trusting file')
261 if (not e.startswith(b'not trusting file')
262 and not e.startswith(b'warning: Not importing')
262 and not e.startswith(b'warning: Not importing')
263 and not e.startswith(b'obsolete feature not enabled')
263 and not e.startswith(b'obsolete feature not enabled')
264 and not e.startswith(b'*** failed to import extension')
264 and not e.startswith(b'*** failed to import extension')
265 and not e.startswith(b'devel-warn:'))]
265 and not e.startswith(b'devel-warn:'))]
266 return b'\n'.join(b' ' + e for e in err)
266 return b'\n'.join(b' ' + e for e in err)
267
267
268 def findhg():
268 def findhg():
269 """Try to figure out how we should invoke hg for examining the local
269 """Try to figure out how we should invoke hg for examining the local
270 repository contents.
270 repository contents.
271
271
272 Returns an hgcommand object."""
272 Returns an hgcommand object."""
273 # By default, prefer the "hg" command in the user's path. This was
273 # By default, prefer the "hg" command in the user's path. This was
274 # presumably the hg command that the user used to create this repository.
274 # presumably the hg command that the user used to create this repository.
275 #
275 #
276 # This repository may require extensions or other settings that would not
276 # This repository may require extensions or other settings that would not
277 # be enabled by running the hg script directly from this local repository.
277 # be enabled by running the hg script directly from this local repository.
278 hgenv = os.environ.copy()
278 hgenv = os.environ.copy()
279 # Use HGPLAIN to disable hgrc settings that would change output formatting,
279 # Use HGPLAIN to disable hgrc settings that would change output formatting,
280 # and disable localization for the same reasons.
280 # and disable localization for the same reasons.
281 hgenv['HGPLAIN'] = '1'
281 hgenv['HGPLAIN'] = '1'
282 hgenv['LANGUAGE'] = 'C'
282 hgenv['LANGUAGE'] = 'C'
283 hgcmd = ['hg']
283 hgcmd = ['hg']
284 # Run a simple "hg log" command just to see if using hg from the user's
284 # Run a simple "hg log" command just to see if using hg from the user's
285 # path works and can successfully interact with this repository.
285 # path works and can successfully interact with this repository.
286 check_cmd = ['log', '-r.', '-Ttest']
286 check_cmd = ['log', '-r.', '-Ttest']
287 try:
287 try:
288 retcode, out, err = runcmd(hgcmd + check_cmd, hgenv)
288 retcode, out, err = runcmd(hgcmd + check_cmd, hgenv)
289 except EnvironmentError:
289 except EnvironmentError:
290 retcode = -1
290 retcode = -1
291 if retcode == 0 and not filterhgerr(err):
291 if retcode == 0 and not filterhgerr(err):
292 return hgcommand(hgcmd, hgenv)
292 return hgcommand(hgcmd, hgenv)
293
293
294 # Fall back to trying the local hg installation.
294 # Fall back to trying the local hg installation.
295 hgenv = localhgenv()
295 hgenv = localhgenv()
296 hgcmd = [sys.executable, 'hg']
296 hgcmd = [sys.executable, 'hg']
297 try:
297 try:
298 retcode, out, err = runcmd(hgcmd + check_cmd, hgenv)
298 retcode, out, err = runcmd(hgcmd + check_cmd, hgenv)
299 except EnvironmentError:
299 except EnvironmentError:
300 retcode = -1
300 retcode = -1
301 if retcode == 0 and not filterhgerr(err):
301 if retcode == 0 and not filterhgerr(err):
302 return hgcommand(hgcmd, hgenv)
302 return hgcommand(hgcmd, hgenv)
303
303
304 raise SystemExit('Unable to find a working hg binary to extract the '
304 raise SystemExit('Unable to find a working hg binary to extract the '
305 'version from the repository tags')
305 'version from the repository tags')
306
306
307 def localhgenv():
307 def localhgenv():
308 """Get an environment dictionary to use for invoking or importing
308 """Get an environment dictionary to use for invoking or importing
309 mercurial from the local repository."""
309 mercurial from the local repository."""
310 # Execute hg out of this directory with a custom environment which takes
310 # Execute hg out of this directory with a custom environment which takes
311 # care to not use any hgrc files and do no localization.
311 # care to not use any hgrc files and do no localization.
312 env = {'HGMODULEPOLICY': 'py',
312 env = {'HGMODULEPOLICY': 'py',
313 'HGRCPATH': '',
313 'HGRCPATH': '',
314 'LANGUAGE': 'C',
314 'LANGUAGE': 'C',
315 'PATH': ''} # make pypi modules that use os.environ['PATH'] happy
315 'PATH': ''} # make pypi modules that use os.environ['PATH'] happy
316 if 'LD_LIBRARY_PATH' in os.environ:
316 if 'LD_LIBRARY_PATH' in os.environ:
317 env['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH']
317 env['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH']
318 if 'SystemRoot' in os.environ:
318 if 'SystemRoot' in os.environ:
319 # SystemRoot is required by Windows to load various DLLs. See:
319 # SystemRoot is required by Windows to load various DLLs. See:
320 # https://bugs.python.org/issue13524#msg148850
320 # https://bugs.python.org/issue13524#msg148850
321 env['SystemRoot'] = os.environ['SystemRoot']
321 env['SystemRoot'] = os.environ['SystemRoot']
322 return env
322 return env
323
323
324 version = ''
324 version = ''
325
325
326 if os.path.isdir('.hg'):
326 if os.path.isdir('.hg'):
327 hg = findhg()
327 hg = findhg()
328 cmd = ['log', '-r', '.', '--template', '{tags}\n']
328 cmd = ['log', '-r', '.', '--template', '{tags}\n']
329 numerictags = [t for t in sysstr(hg.run(cmd)).split() if t[0:1].isdigit()]
329 numerictags = [t for t in sysstr(hg.run(cmd)).split() if t[0:1].isdigit()]
330 hgid = sysstr(hg.run(['id', '-i'])).strip()
330 hgid = sysstr(hg.run(['id', '-i'])).strip()
331 if not hgid:
331 if not hgid:
332 # Bail out if hg is having problems interacting with this repository,
332 # Bail out if hg is having problems interacting with this repository,
333 # rather than falling through and producing a bogus version number.
333 # rather than falling through and producing a bogus version number.
334 # Continuing with an invalid version number will break extensions
334 # Continuing with an invalid version number will break extensions
335 # that define minimumhgversion.
335 # that define minimumhgversion.
336 raise SystemExit('Unable to determine hg version from local repository')
336 raise SystemExit('Unable to determine hg version from local repository')
337 if numerictags: # tag(s) found
337 if numerictags: # tag(s) found
338 version = numerictags[-1]
338 version = numerictags[-1]
339 if hgid.endswith('+'): # propagate the dirty status to the tag
339 if hgid.endswith('+'): # propagate the dirty status to the tag
340 version += '+'
340 version += '+'
341 else: # no tag found
341 else: # no tag found
342 ltagcmd = ['parents', '--template', '{latesttag}']
342 ltagcmd = ['parents', '--template', '{latesttag}']
343 ltag = sysstr(hg.run(ltagcmd))
343 ltag = sysstr(hg.run(ltagcmd))
344 changessincecmd = ['log', '-T', 'x\n', '-r', "only(.,'%s')" % ltag]
344 changessincecmd = ['log', '-T', 'x\n', '-r', "only(.,'%s')" % ltag]
345 changessince = len(hg.run(changessincecmd).splitlines())
345 changessince = len(hg.run(changessincecmd).splitlines())
346 version = '%s+%s-%s' % (ltag, changessince, hgid)
346 version = '%s+%s-%s' % (ltag, changessince, hgid)
347 if version.endswith('+'):
347 if version.endswith('+'):
348 version += time.strftime('%Y%m%d')
348 version += time.strftime('%Y%m%d')
349 elif os.path.exists('.hg_archival.txt'):
349 elif os.path.exists('.hg_archival.txt'):
350 kw = dict([[t.strip() for t in l.split(':', 1)]
350 kw = dict([[t.strip() for t in l.split(':', 1)]
351 for l in open('.hg_archival.txt')])
351 for l in open('.hg_archival.txt')])
352 if 'tag' in kw:
352 if 'tag' in kw:
353 version = kw['tag']
353 version = kw['tag']
354 elif 'latesttag' in kw:
354 elif 'latesttag' in kw:
355 if 'changessincelatesttag' in kw:
355 if 'changessincelatesttag' in kw:
356 version = '%(latesttag)s+%(changessincelatesttag)s-%(node).12s' % kw
356 version = '%(latesttag)s+%(changessincelatesttag)s-%(node).12s' % kw
357 else:
357 else:
358 version = '%(latesttag)s+%(latesttagdistance)s-%(node).12s' % kw
358 version = '%(latesttag)s+%(latesttagdistance)s-%(node).12s' % kw
359 else:
359 else:
360 version = kw.get('node', '')[:12]
360 version = kw.get('node', '')[:12]
361
361
362 if version:
362 if version:
363 versionb = version
363 versionb = version
364 if not isinstance(versionb, bytes):
364 if not isinstance(versionb, bytes):
365 versionb = versionb.encode('ascii')
365 versionb = versionb.encode('ascii')
366
366
367 write_if_changed('mercurial/__version__.py', b''.join([
367 write_if_changed('mercurial/__version__.py', b''.join([
368 b'# this file is autogenerated by setup.py\n'
368 b'# this file is autogenerated by setup.py\n'
369 b'version = b"%s"\n' % versionb,
369 b'version = b"%s"\n' % versionb,
370 ]))
370 ]))
371
371
372 try:
372 try:
373 oldpolicy = os.environ.get('HGMODULEPOLICY', None)
373 oldpolicy = os.environ.get('HGMODULEPOLICY', None)
374 os.environ['HGMODULEPOLICY'] = 'py'
374 os.environ['HGMODULEPOLICY'] = 'py'
375 from mercurial import __version__
375 from mercurial import __version__
376 version = __version__.version
376 version = __version__.version
377 except ImportError:
377 except ImportError:
378 version = b'unknown'
378 version = b'unknown'
379 finally:
379 finally:
380 if oldpolicy is None:
380 if oldpolicy is None:
381 del os.environ['HGMODULEPOLICY']
381 del os.environ['HGMODULEPOLICY']
382 else:
382 else:
383 os.environ['HGMODULEPOLICY'] = oldpolicy
383 os.environ['HGMODULEPOLICY'] = oldpolicy
384
384
385 class hgbuild(build):
385 class hgbuild(build):
386 # Insert hgbuildmo first so that files in mercurial/locale/ are found
386 # Insert hgbuildmo first so that files in mercurial/locale/ are found
387 # when build_py is run next.
387 # when build_py is run next.
388 sub_commands = [('build_mo', None)] + build.sub_commands
388 sub_commands = [('build_mo', None)] + build.sub_commands
389
389
390 class hgbuildmo(build):
390 class hgbuildmo(build):
391
391
392 description = "build translations (.mo files)"
392 description = "build translations (.mo files)"
393
393
394 def run(self):
394 def run(self):
395 if not find_executable('msgfmt'):
395 if not find_executable('msgfmt'):
396 self.warn("could not find msgfmt executable, no translations "
396 self.warn("could not find msgfmt executable, no translations "
397 "will be built")
397 "will be built")
398 return
398 return
399
399
400 podir = 'i18n'
400 podir = 'i18n'
401 if not os.path.isdir(podir):
401 if not os.path.isdir(podir):
402 self.warn("could not find %s/ directory" % podir)
402 self.warn("could not find %s/ directory" % podir)
403 return
403 return
404
404
405 join = os.path.join
405 join = os.path.join
406 for po in os.listdir(podir):
406 for po in os.listdir(podir):
407 if not po.endswith('.po'):
407 if not po.endswith('.po'):
408 continue
408 continue
409 pofile = join(podir, po)
409 pofile = join(podir, po)
410 modir = join('locale', po[:-3], 'LC_MESSAGES')
410 modir = join('locale', po[:-3], 'LC_MESSAGES')
411 mofile = join(modir, 'hg.mo')
411 mofile = join(modir, 'hg.mo')
412 mobuildfile = join('mercurial', mofile)
412 mobuildfile = join('mercurial', mofile)
413 cmd = ['msgfmt', '-v', '-o', mobuildfile, pofile]
413 cmd = ['msgfmt', '-v', '-o', mobuildfile, pofile]
414 if sys.platform != 'sunos5':
414 if sys.platform != 'sunos5':
415 # msgfmt on Solaris does not know about -c
415 # msgfmt on Solaris does not know about -c
416 cmd.append('-c')
416 cmd.append('-c')
417 self.mkpath(join('mercurial', modir))
417 self.mkpath(join('mercurial', modir))
418 self.make_file([pofile], mobuildfile, spawn, (cmd,))
418 self.make_file([pofile], mobuildfile, spawn, (cmd,))
419
419
420
420
421 class hgdist(Distribution):
421 class hgdist(Distribution):
422 pure = False
422 pure = False
423 cffi = ispypy
423 cffi = ispypy
424
424
425 global_options = Distribution.global_options + \
425 global_options = Distribution.global_options + \
426 [('pure', None, "use pure (slow) Python "
426 [('pure', None, "use pure (slow) Python "
427 "code instead of C extensions"),
427 "code instead of C extensions"),
428 ]
428 ]
429
429
430 def has_ext_modules(self):
430 def has_ext_modules(self):
431 # self.ext_modules is emptied in hgbuildpy.finalize_options which is
431 # self.ext_modules is emptied in hgbuildpy.finalize_options which is
432 # too late for some cases
432 # too late for some cases
433 return not self.pure and Distribution.has_ext_modules(self)
433 return not self.pure and Distribution.has_ext_modules(self)
434
434
435 # This is ugly as a one-liner. So use a variable.
435 # This is ugly as a one-liner. So use a variable.
436 buildextnegops = dict(getattr(build_ext, 'negative_options', {}))
436 buildextnegops = dict(getattr(build_ext, 'negative_options', {}))
437 buildextnegops['no-zstd'] = 'zstd'
437 buildextnegops['no-zstd'] = 'zstd'
438
438
439 class hgbuildext(build_ext):
439 class hgbuildext(build_ext):
440 user_options = build_ext.user_options + [
440 user_options = build_ext.user_options + [
441 ('zstd', None, 'compile zstd bindings [default]'),
441 ('zstd', None, 'compile zstd bindings [default]'),
442 ('no-zstd', None, 'do not compile zstd bindings'),
442 ('no-zstd', None, 'do not compile zstd bindings'),
443 ]
443 ]
444
444
445 boolean_options = build_ext.boolean_options + ['zstd']
445 boolean_options = build_ext.boolean_options + ['zstd']
446 negative_opt = buildextnegops
446 negative_opt = buildextnegops
447
447
448 def initialize_options(self):
448 def initialize_options(self):
449 self.zstd = True
449 self.zstd = True
450 return build_ext.initialize_options(self)
450 return build_ext.initialize_options(self)
451
451
452 def build_extensions(self):
452 def build_extensions(self):
453 # Filter out zstd if disabled via argument.
453 # Filter out zstd if disabled via argument.
454 if not self.zstd:
454 if not self.zstd:
455 self.extensions = [e for e in self.extensions
455 self.extensions = [e for e in self.extensions
456 if e.name != 'mercurial.zstd']
456 if e.name != 'mercurial.zstd']
457
457
458 return build_ext.build_extensions(self)
458 return build_ext.build_extensions(self)
459
459
460 def build_extension(self, ext):
460 def build_extension(self, ext):
461 try:
461 try:
462 build_ext.build_extension(self, ext)
462 build_ext.build_extension(self, ext)
463 except CCompilerError:
463 except CCompilerError:
464 if not getattr(ext, 'optional', False):
464 if not getattr(ext, 'optional', False):
465 raise
465 raise
466 log.warn("Failed to build optional extension '%s' (skipping)",
466 log.warn("Failed to build optional extension '%s' (skipping)",
467 ext.name)
467 ext.name)
468
468
469 class hgbuildscripts(build_scripts):
469 class hgbuildscripts(build_scripts):
470 def run(self):
470 def run(self):
471 if os.name != 'nt' or self.distribution.pure:
471 if os.name != 'nt' or self.distribution.pure:
472 return build_scripts.run(self)
472 return build_scripts.run(self)
473
473
474 exebuilt = False
474 exebuilt = False
475 try:
475 try:
476 self.run_command('build_hgexe')
476 self.run_command('build_hgexe')
477 exebuilt = True
477 exebuilt = True
478 except (DistutilsError, CCompilerError):
478 except (DistutilsError, CCompilerError):
479 log.warn('failed to build optional hg.exe')
479 log.warn('failed to build optional hg.exe')
480
480
481 if exebuilt:
481 if exebuilt:
482 # Copying hg.exe to the scripts build directory ensures it is
482 # Copying hg.exe to the scripts build directory ensures it is
483 # installed by the install_scripts command.
483 # installed by the install_scripts command.
484 hgexecommand = self.get_finalized_command('build_hgexe')
484 hgexecommand = self.get_finalized_command('build_hgexe')
485 dest = os.path.join(self.build_dir, 'hg.exe')
485 dest = os.path.join(self.build_dir, 'hg.exe')
486 self.mkpath(self.build_dir)
486 self.mkpath(self.build_dir)
487 self.copy_file(hgexecommand.hgexepath, dest)
487 self.copy_file(hgexecommand.hgexepath, dest)
488
488
489 # Remove hg.bat because it is redundant with hg.exe.
489 # Remove hg.bat because it is redundant with hg.exe.
490 self.scripts.remove('contrib/win32/hg.bat')
490 self.scripts.remove('contrib/win32/hg.bat')
491
491
492 return build_scripts.run(self)
492 return build_scripts.run(self)
493
493
494 class hgbuildpy(build_py):
494 class hgbuildpy(build_py):
495 def finalize_options(self):
495 def finalize_options(self):
496 build_py.finalize_options(self)
496 build_py.finalize_options(self)
497
497
498 if self.distribution.pure:
498 if self.distribution.pure:
499 self.distribution.ext_modules = []
499 self.distribution.ext_modules = []
500 elif self.distribution.cffi:
500 elif self.distribution.cffi:
501 from mercurial.cffi import (
501 from mercurial.cffi import (
502 bdiffbuild,
502 bdiffbuild,
503 mpatchbuild,
503 mpatchbuild,
504 )
504 )
505 exts = [mpatchbuild.ffi.distutils_extension(),
505 exts = [mpatchbuild.ffi.distutils_extension(),
506 bdiffbuild.ffi.distutils_extension()]
506 bdiffbuild.ffi.distutils_extension()]
507 # cffi modules go here
507 # cffi modules go here
508 if sys.platform == 'darwin':
508 if sys.platform == 'darwin':
509 from mercurial.cffi import osutilbuild
509 from mercurial.cffi import osutilbuild
510 exts.append(osutilbuild.ffi.distutils_extension())
510 exts.append(osutilbuild.ffi.distutils_extension())
511 self.distribution.ext_modules = exts
511 self.distribution.ext_modules = exts
512 else:
512 else:
513 h = os.path.join(get_python_inc(), 'Python.h')
513 h = os.path.join(get_python_inc(), 'Python.h')
514 if not os.path.exists(h):
514 if not os.path.exists(h):
515 raise SystemExit('Python headers are required to build '
515 raise SystemExit('Python headers are required to build '
516 'Mercurial but weren\'t found in %s' % h)
516 'Mercurial but weren\'t found in %s' % h)
517
517
518 def run(self):
518 def run(self):
519 basepath = os.path.join(self.build_lib, 'mercurial')
519 basepath = os.path.join(self.build_lib, 'mercurial')
520 self.mkpath(basepath)
520 self.mkpath(basepath)
521
521
522 if self.distribution.pure:
522 if self.distribution.pure:
523 modulepolicy = 'py'
523 modulepolicy = 'py'
524 elif self.build_lib == '.':
524 elif self.build_lib == '.':
525 # in-place build should run without rebuilding C extensions
525 # in-place build should run without rebuilding C extensions
526 modulepolicy = 'allow'
526 modulepolicy = 'allow'
527 else:
527 else:
528 modulepolicy = 'c'
528 modulepolicy = 'c'
529
529
530 content = b''.join([
530 content = b''.join([
531 b'# this file is autogenerated by setup.py\n',
531 b'# this file is autogenerated by setup.py\n',
532 b'modulepolicy = b"%s"\n' % modulepolicy.encode('ascii'),
532 b'modulepolicy = b"%s"\n' % modulepolicy.encode('ascii'),
533 ])
533 ])
534 write_if_changed(os.path.join(basepath, '__modulepolicy__.py'),
534 write_if_changed(os.path.join(basepath, '__modulepolicy__.py'),
535 content)
535 content)
536
536
537 build_py.run(self)
537 build_py.run(self)
538
538
539 class buildhgextindex(Command):
539 class buildhgextindex(Command):
540 description = 'generate prebuilt index of hgext (for frozen package)'
540 description = 'generate prebuilt index of hgext (for frozen package)'
541 user_options = []
541 user_options = []
542 _indexfilename = 'hgext/__index__.py'
542 _indexfilename = 'hgext/__index__.py'
543
543
544 def initialize_options(self):
544 def initialize_options(self):
545 pass
545 pass
546
546
547 def finalize_options(self):
547 def finalize_options(self):
548 pass
548 pass
549
549
550 def run(self):
550 def run(self):
551 if os.path.exists(self._indexfilename):
551 if os.path.exists(self._indexfilename):
552 with open(self._indexfilename, 'w') as f:
552 with open(self._indexfilename, 'w') as f:
553 f.write('# empty\n')
553 f.write('# empty\n')
554
554
555 # here no extension enabled, disabled() lists up everything
555 # here no extension enabled, disabled() lists up everything
556 code = ('import pprint; from mercurial import extensions; '
556 code = ('import pprint; from mercurial import extensions; '
557 'pprint.pprint(extensions.disabled())')
557 'pprint.pprint(extensions.disabled())')
558 returncode, out, err = runcmd([sys.executable, '-c', code],
558 returncode, out, err = runcmd([sys.executable, '-c', code],
559 localhgenv())
559 localhgenv())
560 if err or returncode != 0:
560 if err or returncode != 0:
561 raise DistutilsExecError(err)
561 raise DistutilsExecError(err)
562
562
563 with open(self._indexfilename, 'w') as f:
563 with open(self._indexfilename, 'w') as f:
564 f.write('# this file is autogenerated by setup.py\n')
564 f.write('# this file is autogenerated by setup.py\n')
565 f.write('docs = ')
565 f.write('docs = ')
566 f.write(out)
566 f.write(out)
567
567
568 class buildhgexe(build_ext):
568 class buildhgexe(build_ext):
569 description = 'compile hg.exe from mercurial/exewrapper.c'
569 description = 'compile hg.exe from mercurial/exewrapper.c'
570 user_options = build_ext.user_options + [
570 user_options = build_ext.user_options + [
571 ('long-paths-support', None, 'enable support for long paths on '
571 ('long-paths-support', None, 'enable support for long paths on '
572 'Windows (off by default and '
572 'Windows (off by default and '
573 'experimental)'),
573 'experimental)'),
574 ]
574 ]
575
575
576 LONG_PATHS_MANIFEST = """
576 LONG_PATHS_MANIFEST = """
577 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
577 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
578 <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
578 <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
579 <application>
579 <application>
580 <windowsSettings
580 <windowsSettings
581 xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
581 xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
582 <ws2:longPathAware>true</ws2:longPathAware>
582 <ws2:longPathAware>true</ws2:longPathAware>
583 </windowsSettings>
583 </windowsSettings>
584 </application>
584 </application>
585 </assembly>"""
585 </assembly>"""
586
586
587 def initialize_options(self):
587 def initialize_options(self):
588 build_ext.initialize_options(self)
588 build_ext.initialize_options(self)
589 self.long_paths_support = False
589 self.long_paths_support = False
590
590
591 def build_extensions(self):
591 def build_extensions(self):
592 if os.name != 'nt':
592 if os.name != 'nt':
593 return
593 return
594 if isinstance(self.compiler, HackedMingw32CCompiler):
594 if isinstance(self.compiler, HackedMingw32CCompiler):
595 self.compiler.compiler_so = self.compiler.compiler # no -mdll
595 self.compiler.compiler_so = self.compiler.compiler # no -mdll
596 self.compiler.dll_libraries = [] # no -lmsrvc90
596 self.compiler.dll_libraries = [] # no -lmsrvc90
597
597
598 # Different Python installs can have different Python library
598 # Different Python installs can have different Python library
599 # names. e.g. the official CPython distribution uses pythonXY.dll
599 # names. e.g. the official CPython distribution uses pythonXY.dll
600 # and MinGW uses libpythonX.Y.dll.
600 # and MinGW uses libpythonX.Y.dll.
601 _kernel32 = ctypes.windll.kernel32
601 _kernel32 = ctypes.windll.kernel32
602 _kernel32.GetModuleFileNameA.argtypes = [ctypes.c_void_p,
602 _kernel32.GetModuleFileNameA.argtypes = [ctypes.c_void_p,
603 ctypes.c_void_p,
603 ctypes.c_void_p,
604 ctypes.c_ulong]
604 ctypes.c_ulong]
605 _kernel32.GetModuleFileNameA.restype = ctypes.c_ulong
605 _kernel32.GetModuleFileNameA.restype = ctypes.c_ulong
606 size = 1000
606 size = 1000
607 buf = ctypes.create_string_buffer(size + 1)
607 buf = ctypes.create_string_buffer(size + 1)
608 filelen = _kernel32.GetModuleFileNameA(sys.dllhandle, ctypes.byref(buf),
608 filelen = _kernel32.GetModuleFileNameA(sys.dllhandle, ctypes.byref(buf),
609 size)
609 size)
610
610
611 if filelen > 0 and filelen != size:
611 if filelen > 0 and filelen != size:
612 dllbasename = os.path.basename(buf.value)
612 dllbasename = os.path.basename(buf.value)
613 if not dllbasename.lower().endswith(b'.dll'):
613 if not dllbasename.lower().endswith(b'.dll'):
614 raise SystemExit('Python DLL does not end with .dll: %s' %
614 raise SystemExit('Python DLL does not end with .dll: %s' %
615 dllbasename)
615 dllbasename)
616 pythonlib = dllbasename[:-4]
616 pythonlib = dllbasename[:-4]
617 else:
617 else:
618 log.warn('could not determine Python DLL filename; '
618 log.warn('could not determine Python DLL filename; '
619 'assuming pythonXY')
619 'assuming pythonXY')
620
620
621 hv = sys.hexversion
621 hv = sys.hexversion
622 pythonlib = 'python%d%d' % (hv >> 24, (hv >> 16) & 0xff)
622 pythonlib = 'python%d%d' % (hv >> 24, (hv >> 16) & 0xff)
623
623
624 log.info('using %s as Python library name' % pythonlib)
624 log.info('using %s as Python library name' % pythonlib)
625 with open('mercurial/hgpythonlib.h', 'wb') as f:
625 with open('mercurial/hgpythonlib.h', 'wb') as f:
626 f.write(b'/* this file is autogenerated by setup.py */\n')
626 f.write(b'/* this file is autogenerated by setup.py */\n')
627 f.write(b'#define HGPYTHONLIB "%s"\n' % pythonlib)
627 f.write(b'#define HGPYTHONLIB "%s"\n' % pythonlib)
628 objects = self.compiler.compile(['mercurial/exewrapper.c'],
628 objects = self.compiler.compile(['mercurial/exewrapper.c'],
629 output_dir=self.build_temp)
629 output_dir=self.build_temp)
630 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
630 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
631 self.hgtarget = os.path.join(dir, 'hg')
631 self.hgtarget = os.path.join(dir, 'hg')
632 self.compiler.link_executable(objects, self.hgtarget,
632 self.compiler.link_executable(objects, self.hgtarget,
633 libraries=[],
633 libraries=[],
634 output_dir=self.build_temp)
634 output_dir=self.build_temp)
635 if self.long_paths_support:
635 if self.long_paths_support:
636 self.addlongpathsmanifest()
636 self.addlongpathsmanifest()
637
637
638 def addlongpathsmanifest(self):
638 def addlongpathsmanifest(self):
639 """Add manifest pieces so that hg.exe understands long paths
639 """Add manifest pieces so that hg.exe understands long paths
640
640
641 This is an EXPERIMENTAL feature, use with care.
641 This is an EXPERIMENTAL feature, use with care.
642 To enable long paths support, one needs to do two things:
642 To enable long paths support, one needs to do two things:
643 - build Mercurial with --long-paths-support option
643 - build Mercurial with --long-paths-support option
644 - change HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\
644 - change HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\
645 LongPathsEnabled to have value 1.
645 LongPathsEnabled to have value 1.
646
646
647 Please ignore 'warning 81010002: Unrecognized Element "longPathAware"';
647 Please ignore 'warning 81010002: Unrecognized Element "longPathAware"';
648 it happens because Mercurial uses mt.exe circa 2008, which is not
648 it happens because Mercurial uses mt.exe circa 2008, which is not
649 yet aware of long paths support in the manifest (I think so at least).
649 yet aware of long paths support in the manifest (I think so at least).
650 This does not stop mt.exe from embedding/merging the XML properly.
650 This does not stop mt.exe from embedding/merging the XML properly.
651
651
652 Why resource #1 should be used for .exe manifests? I don't know and
652 Why resource #1 should be used for .exe manifests? I don't know and
653 wasn't able to find an explanation for mortals. But it seems to work.
653 wasn't able to find an explanation for mortals. But it seems to work.
654 """
654 """
655 exefname = self.compiler.executable_filename(self.hgtarget)
655 exefname = self.compiler.executable_filename(self.hgtarget)
656 fdauto, manfname = tempfile.mkstemp(suffix='.hg.exe.manifest')
656 fdauto, manfname = tempfile.mkstemp(suffix='.hg.exe.manifest')
657 os.close(fdauto)
657 os.close(fdauto)
658 with open(manfname, 'w') as f:
658 with open(manfname, 'w') as f:
659 f.write(self.LONG_PATHS_MANIFEST)
659 f.write(self.LONG_PATHS_MANIFEST)
660 log.info("long paths manifest is written to '%s'" % manfname)
660 log.info("long paths manifest is written to '%s'" % manfname)
661 inputresource = '-inputresource:%s;#1' % exefname
661 inputresource = '-inputresource:%s;#1' % exefname
662 outputresource = '-outputresource:%s;#1' % exefname
662 outputresource = '-outputresource:%s;#1' % exefname
663 log.info("running mt.exe to update hg.exe's manifest in-place")
663 log.info("running mt.exe to update hg.exe's manifest in-place")
664 # supplying both -manifest and -inputresource to mt.exe makes
664 # supplying both -manifest and -inputresource to mt.exe makes
665 # it merge the embedded and supplied manifests in the -outputresource
665 # it merge the embedded and supplied manifests in the -outputresource
666 self.spawn(['mt.exe', '-nologo', '-manifest', manfname,
666 self.spawn(['mt.exe', '-nologo', '-manifest', manfname,
667 inputresource, outputresource])
667 inputresource, outputresource])
668 log.info("done updating hg.exe's manifest")
668 log.info("done updating hg.exe's manifest")
669 os.remove(manfname)
669 os.remove(manfname)
670
670
671 @property
671 @property
672 def hgexepath(self):
672 def hgexepath(self):
673 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
673 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
674 return os.path.join(self.build_temp, dir, 'hg.exe')
674 return os.path.join(self.build_temp, dir, 'hg.exe')
675
675
676 class hginstall(install):
676 class hginstall(install):
677
677
678 user_options = install.user_options + [
678 user_options = install.user_options + [
679 ('old-and-unmanageable', None,
679 ('old-and-unmanageable', None,
680 'noop, present for eggless setuptools compat'),
680 'noop, present for eggless setuptools compat'),
681 ('single-version-externally-managed', None,
681 ('single-version-externally-managed', None,
682 'noop, present for eggless setuptools compat'),
682 'noop, present for eggless setuptools compat'),
683 ]
683 ]
684
684
685 # Also helps setuptools not be sad while we refuse to create eggs.
685 # Also helps setuptools not be sad while we refuse to create eggs.
686 single_version_externally_managed = True
686 single_version_externally_managed = True
687
687
688 def get_sub_commands(self):
688 def get_sub_commands(self):
689 # Screen out egg related commands to prevent egg generation. But allow
689 # Screen out egg related commands to prevent egg generation. But allow
690 # mercurial.egg-info generation, since that is part of modern
690 # mercurial.egg-info generation, since that is part of modern
691 # packaging.
691 # packaging.
692 excl = set(['bdist_egg'])
692 excl = set(['bdist_egg'])
693 return filter(lambda x: x not in excl, install.get_sub_commands(self))
693 return filter(lambda x: x not in excl, install.get_sub_commands(self))
694
694
695 class hginstalllib(install_lib):
695 class hginstalllib(install_lib):
696 '''
696 '''
697 This is a specialization of install_lib that replaces the copy_file used
697 This is a specialization of install_lib that replaces the copy_file used
698 there so that it supports setting the mode of files after copying them,
698 there so that it supports setting the mode of files after copying them,
699 instead of just preserving the mode that the files originally had. If your
699 instead of just preserving the mode that the files originally had. If your
700 system has a umask of something like 027, preserving the permissions when
700 system has a umask of something like 027, preserving the permissions when
701 copying will lead to a broken install.
701 copying will lead to a broken install.
702
702
703 Note that just passing keep_permissions=False to copy_file would be
703 Note that just passing keep_permissions=False to copy_file would be
704 insufficient, as it might still be applying a umask.
704 insufficient, as it might still be applying a umask.
705 '''
705 '''
706
706
707 def run(self):
707 def run(self):
708 realcopyfile = file_util.copy_file
708 realcopyfile = file_util.copy_file
709 def copyfileandsetmode(*args, **kwargs):
709 def copyfileandsetmode(*args, **kwargs):
710 src, dst = args[0], args[1]
710 src, dst = args[0], args[1]
711 dst, copied = realcopyfile(*args, **kwargs)
711 dst, copied = realcopyfile(*args, **kwargs)
712 if copied:
712 if copied:
713 st = os.stat(src)
713 st = os.stat(src)
714 # Persist executable bit (apply it to group and other if user
714 # Persist executable bit (apply it to group and other if user
715 # has it)
715 # has it)
716 if st[stat.ST_MODE] & stat.S_IXUSR:
716 if st[stat.ST_MODE] & stat.S_IXUSR:
717 setmode = int('0755', 8)
717 setmode = int('0755', 8)
718 else:
718 else:
719 setmode = int('0644', 8)
719 setmode = int('0644', 8)
720 m = stat.S_IMODE(st[stat.ST_MODE])
720 m = stat.S_IMODE(st[stat.ST_MODE])
721 m = (m & ~int('0777', 8)) | setmode
721 m = (m & ~int('0777', 8)) | setmode
722 os.chmod(dst, m)
722 os.chmod(dst, m)
723 file_util.copy_file = copyfileandsetmode
723 file_util.copy_file = copyfileandsetmode
724 try:
724 try:
725 install_lib.run(self)
725 install_lib.run(self)
726 finally:
726 finally:
727 file_util.copy_file = realcopyfile
727 file_util.copy_file = realcopyfile
728
728
729 class hginstallscripts(install_scripts):
729 class hginstallscripts(install_scripts):
730 '''
730 '''
731 This is a specialization of install_scripts that replaces the @LIBDIR@ with
731 This is a specialization of install_scripts that replaces the @LIBDIR@ with
732 the configured directory for modules. If possible, the path is made relative
732 the configured directory for modules. If possible, the path is made relative
733 to the directory for scripts.
733 to the directory for scripts.
734 '''
734 '''
735
735
736 def initialize_options(self):
736 def initialize_options(self):
737 install_scripts.initialize_options(self)
737 install_scripts.initialize_options(self)
738
738
739 self.install_lib = None
739 self.install_lib = None
740
740
741 def finalize_options(self):
741 def finalize_options(self):
742 install_scripts.finalize_options(self)
742 install_scripts.finalize_options(self)
743 self.set_undefined_options('install',
743 self.set_undefined_options('install',
744 ('install_lib', 'install_lib'))
744 ('install_lib', 'install_lib'))
745
745
746 def run(self):
746 def run(self):
747 install_scripts.run(self)
747 install_scripts.run(self)
748
748
749 # It only makes sense to replace @LIBDIR@ with the install path if
749 # It only makes sense to replace @LIBDIR@ with the install path if
750 # the install path is known. For wheels, the logic below calculates
750 # the install path is known. For wheels, the logic below calculates
751 # the libdir to be "../..". This is because the internal layout of a
751 # the libdir to be "../..". This is because the internal layout of a
752 # wheel archive looks like:
752 # wheel archive looks like:
753 #
753 #
754 # mercurial-3.6.1.data/scripts/hg
754 # mercurial-3.6.1.data/scripts/hg
755 # mercurial/__init__.py
755 # mercurial/__init__.py
756 #
756 #
757 # When installing wheels, the subdirectories of the "<pkg>.data"
757 # When installing wheels, the subdirectories of the "<pkg>.data"
758 # directory are translated to system local paths and files therein
758 # directory are translated to system local paths and files therein
759 # are copied in place. The mercurial/* files are installed into the
759 # are copied in place. The mercurial/* files are installed into the
760 # site-packages directory. However, the site-packages directory
760 # site-packages directory. However, the site-packages directory
761 # isn't known until wheel install time. This means we have no clue
761 # isn't known until wheel install time. This means we have no clue
762 # at wheel generation time what the installed site-packages directory
762 # at wheel generation time what the installed site-packages directory
763 # will be. And, wheels don't appear to provide the ability to register
763 # will be. And, wheels don't appear to provide the ability to register
764 # custom code to run during wheel installation. This all means that
764 # custom code to run during wheel installation. This all means that
765 # we can't reliably set the libdir in wheels: the default behavior
765 # we can't reliably set the libdir in wheels: the default behavior
766 # of looking in sys.path must do.
766 # of looking in sys.path must do.
767
767
768 if (os.path.splitdrive(self.install_dir)[0] !=
768 if (os.path.splitdrive(self.install_dir)[0] !=
769 os.path.splitdrive(self.install_lib)[0]):
769 os.path.splitdrive(self.install_lib)[0]):
770 # can't make relative paths from one drive to another, so use an
770 # can't make relative paths from one drive to another, so use an
771 # absolute path instead
771 # absolute path instead
772 libdir = self.install_lib
772 libdir = self.install_lib
773 else:
773 else:
774 common = os.path.commonprefix((self.install_dir, self.install_lib))
774 common = os.path.commonprefix((self.install_dir, self.install_lib))
775 rest = self.install_dir[len(common):]
775 rest = self.install_dir[len(common):]
776 uplevel = len([n for n in os.path.split(rest) if n])
776 uplevel = len([n for n in os.path.split(rest) if n])
777
777
778 libdir = uplevel * ('..' + os.sep) + self.install_lib[len(common):]
778 libdir = uplevel * ('..' + os.sep) + self.install_lib[len(common):]
779
779
780 for outfile in self.outfiles:
780 for outfile in self.outfiles:
781 with open(outfile, 'rb') as fp:
781 with open(outfile, 'rb') as fp:
782 data = fp.read()
782 data = fp.read()
783
783
784 # skip binary files
784 # skip binary files
785 if b'\0' in data:
785 if b'\0' in data:
786 continue
786 continue
787
787
788 # During local installs, the shebang will be rewritten to the final
788 # During local installs, the shebang will be rewritten to the final
789 # install path. During wheel packaging, the shebang has a special
789 # install path. During wheel packaging, the shebang has a special
790 # value.
790 # value.
791 if data.startswith(b'#!python'):
791 if data.startswith(b'#!python'):
792 log.info('not rewriting @LIBDIR@ in %s because install path '
792 log.info('not rewriting @LIBDIR@ in %s because install path '
793 'not known' % outfile)
793 'not known' % outfile)
794 continue
794 continue
795
795
796 data = data.replace(b'@LIBDIR@', libdir.encode(libdir_escape))
796 data = data.replace(b'@LIBDIR@', libdir.encode(libdir_escape))
797 with open(outfile, 'wb') as fp:
797 with open(outfile, 'wb') as fp:
798 fp.write(data)
798 fp.write(data)
799
799
800 cmdclass = {'build': hgbuild,
800 cmdclass = {'build': hgbuild,
801 'build_mo': hgbuildmo,
801 'build_mo': hgbuildmo,
802 'build_ext': hgbuildext,
802 'build_ext': hgbuildext,
803 'build_py': hgbuildpy,
803 'build_py': hgbuildpy,
804 'build_scripts': hgbuildscripts,
804 'build_scripts': hgbuildscripts,
805 'build_hgextindex': buildhgextindex,
805 'build_hgextindex': buildhgextindex,
806 'install': hginstall,
806 'install': hginstall,
807 'install_lib': hginstalllib,
807 'install_lib': hginstalllib,
808 'install_scripts': hginstallscripts,
808 'install_scripts': hginstallscripts,
809 'build_hgexe': buildhgexe,
809 'build_hgexe': buildhgexe,
810 }
810 }
811
811
812 packages = ['mercurial',
812 packages = ['mercurial',
813 'mercurial.cext',
813 'mercurial.cext',
814 'mercurial.cffi',
814 'mercurial.cffi',
815 'mercurial.hgweb',
815 'mercurial.hgweb',
816 'mercurial.pure',
816 'mercurial.pure',
817 'mercurial.thirdparty',
817 'mercurial.thirdparty',
818 'mercurial.thirdparty.attr',
818 'mercurial.thirdparty.attr',
819 'mercurial.thirdparty.cbor',
819 'mercurial.thirdparty.cbor',
820 'mercurial.thirdparty.cbor.cbor2',
820 'mercurial.thirdparty.cbor.cbor2',
821 'mercurial.thirdparty.zope',
821 'mercurial.thirdparty.zope',
822 'mercurial.thirdparty.zope.interface',
822 'mercurial.thirdparty.zope.interface',
823 'mercurial.utils',
823 'mercurial.utils',
824 'mercurial.revlogutils',
824 'mercurial.revlogutils',
825 'mercurial.testing',
825 'hgext', 'hgext.convert', 'hgext.fsmonitor',
826 'hgext', 'hgext.convert', 'hgext.fsmonitor',
826 'hgext.fastannotate',
827 'hgext.fastannotate',
827 'hgext.fsmonitor.pywatchman',
828 'hgext.fsmonitor.pywatchman',
828 'hgext.infinitepush',
829 'hgext.infinitepush',
829 'hgext.highlight',
830 'hgext.highlight',
830 'hgext.largefiles', 'hgext.lfs', 'hgext.narrow',
831 'hgext.largefiles', 'hgext.lfs', 'hgext.narrow',
831 'hgext.zeroconf', 'hgext3rd',
832 'hgext.zeroconf', 'hgext3rd',
832 'hgdemandimport']
833 'hgdemandimport']
833 if sys.version_info[0] == 2:
834 if sys.version_info[0] == 2:
834 packages.extend(['mercurial.thirdparty.concurrent',
835 packages.extend(['mercurial.thirdparty.concurrent',
835 'mercurial.thirdparty.concurrent.futures'])
836 'mercurial.thirdparty.concurrent.futures'])
836
837
837 common_depends = ['mercurial/bitmanipulation.h',
838 common_depends = ['mercurial/bitmanipulation.h',
838 'mercurial/compat.h',
839 'mercurial/compat.h',
839 'mercurial/cext/util.h']
840 'mercurial/cext/util.h']
840 common_include_dirs = ['mercurial']
841 common_include_dirs = ['mercurial']
841
842
842 osutil_cflags = []
843 osutil_cflags = []
843 osutil_ldflags = []
844 osutil_ldflags = []
844
845
845 # platform specific macros
846 # platform specific macros
846 for plat, func in [('bsd', 'setproctitle')]:
847 for plat, func in [('bsd', 'setproctitle')]:
847 if re.search(plat, sys.platform) and hasfunction(new_compiler(), func):
848 if re.search(plat, sys.platform) and hasfunction(new_compiler(), func):
848 osutil_cflags.append('-DHAVE_%s' % func.upper())
849 osutil_cflags.append('-DHAVE_%s' % func.upper())
849
850
850 for plat, macro, code in [
851 for plat, macro, code in [
851 ('bsd|darwin', 'BSD_STATFS', '''
852 ('bsd|darwin', 'BSD_STATFS', '''
852 #include <sys/param.h>
853 #include <sys/param.h>
853 #include <sys/mount.h>
854 #include <sys/mount.h>
854 int main() { struct statfs s; return sizeof(s.f_fstypename); }
855 int main() { struct statfs s; return sizeof(s.f_fstypename); }
855 '''),
856 '''),
856 ('linux', 'LINUX_STATFS', '''
857 ('linux', 'LINUX_STATFS', '''
857 #include <linux/magic.h>
858 #include <linux/magic.h>
858 #include <sys/vfs.h>
859 #include <sys/vfs.h>
859 int main() { struct statfs s; return sizeof(s.f_type); }
860 int main() { struct statfs s; return sizeof(s.f_type); }
860 '''),
861 '''),
861 ]:
862 ]:
862 if re.search(plat, sys.platform) and cancompile(new_compiler(), code):
863 if re.search(plat, sys.platform) and cancompile(new_compiler(), code):
863 osutil_cflags.append('-DHAVE_%s' % macro)
864 osutil_cflags.append('-DHAVE_%s' % macro)
864
865
865 if sys.platform == 'darwin':
866 if sys.platform == 'darwin':
866 osutil_ldflags += ['-framework', 'ApplicationServices']
867 osutil_ldflags += ['-framework', 'ApplicationServices']
867
868
868 xdiff_srcs = [
869 xdiff_srcs = [
869 'mercurial/thirdparty/xdiff/xdiffi.c',
870 'mercurial/thirdparty/xdiff/xdiffi.c',
870 'mercurial/thirdparty/xdiff/xprepare.c',
871 'mercurial/thirdparty/xdiff/xprepare.c',
871 'mercurial/thirdparty/xdiff/xutils.c',
872 'mercurial/thirdparty/xdiff/xutils.c',
872 ]
873 ]
873
874
874 xdiff_headers = [
875 xdiff_headers = [
875 'mercurial/thirdparty/xdiff/xdiff.h',
876 'mercurial/thirdparty/xdiff/xdiff.h',
876 'mercurial/thirdparty/xdiff/xdiffi.h',
877 'mercurial/thirdparty/xdiff/xdiffi.h',
877 'mercurial/thirdparty/xdiff/xinclude.h',
878 'mercurial/thirdparty/xdiff/xinclude.h',
878 'mercurial/thirdparty/xdiff/xmacros.h',
879 'mercurial/thirdparty/xdiff/xmacros.h',
879 'mercurial/thirdparty/xdiff/xprepare.h',
880 'mercurial/thirdparty/xdiff/xprepare.h',
880 'mercurial/thirdparty/xdiff/xtypes.h',
881 'mercurial/thirdparty/xdiff/xtypes.h',
881 'mercurial/thirdparty/xdiff/xutils.h',
882 'mercurial/thirdparty/xdiff/xutils.h',
882 ]
883 ]
883
884
884 extmodules = [
885 extmodules = [
885 Extension('mercurial.cext.base85', ['mercurial/cext/base85.c'],
886 Extension('mercurial.cext.base85', ['mercurial/cext/base85.c'],
886 include_dirs=common_include_dirs,
887 include_dirs=common_include_dirs,
887 depends=common_depends),
888 depends=common_depends),
888 Extension('mercurial.cext.bdiff', ['mercurial/bdiff.c',
889 Extension('mercurial.cext.bdiff', ['mercurial/bdiff.c',
889 'mercurial/cext/bdiff.c'] + xdiff_srcs,
890 'mercurial/cext/bdiff.c'] + xdiff_srcs,
890 include_dirs=common_include_dirs,
891 include_dirs=common_include_dirs,
891 depends=common_depends + ['mercurial/bdiff.h'] + xdiff_headers),
892 depends=common_depends + ['mercurial/bdiff.h'] + xdiff_headers),
892 Extension('mercurial.cext.mpatch', ['mercurial/mpatch.c',
893 Extension('mercurial.cext.mpatch', ['mercurial/mpatch.c',
893 'mercurial/cext/mpatch.c'],
894 'mercurial/cext/mpatch.c'],
894 include_dirs=common_include_dirs,
895 include_dirs=common_include_dirs,
895 depends=common_depends),
896 depends=common_depends),
896 Extension('mercurial.cext.parsers', ['mercurial/cext/charencode.c',
897 Extension('mercurial.cext.parsers', ['mercurial/cext/charencode.c',
897 'mercurial/cext/dirs.c',
898 'mercurial/cext/dirs.c',
898 'mercurial/cext/manifest.c',
899 'mercurial/cext/manifest.c',
899 'mercurial/cext/parsers.c',
900 'mercurial/cext/parsers.c',
900 'mercurial/cext/pathencode.c',
901 'mercurial/cext/pathencode.c',
901 'mercurial/cext/revlog.c'],
902 'mercurial/cext/revlog.c'],
902 include_dirs=common_include_dirs,
903 include_dirs=common_include_dirs,
903 depends=common_depends + ['mercurial/cext/charencode.h']),
904 depends=common_depends + ['mercurial/cext/charencode.h']),
904 Extension('mercurial.cext.osutil', ['mercurial/cext/osutil.c'],
905 Extension('mercurial.cext.osutil', ['mercurial/cext/osutil.c'],
905 include_dirs=common_include_dirs,
906 include_dirs=common_include_dirs,
906 extra_compile_args=osutil_cflags,
907 extra_compile_args=osutil_cflags,
907 extra_link_args=osutil_ldflags,
908 extra_link_args=osutil_ldflags,
908 depends=common_depends),
909 depends=common_depends),
909 Extension(
910 Extension(
910 'mercurial.thirdparty.zope.interface._zope_interface_coptimizations', [
911 'mercurial.thirdparty.zope.interface._zope_interface_coptimizations', [
911 'mercurial/thirdparty/zope/interface/_zope_interface_coptimizations.c',
912 'mercurial/thirdparty/zope/interface/_zope_interface_coptimizations.c',
912 ]),
913 ]),
913 Extension('hgext.fsmonitor.pywatchman.bser',
914 Extension('hgext.fsmonitor.pywatchman.bser',
914 ['hgext/fsmonitor/pywatchman/bser.c']),
915 ['hgext/fsmonitor/pywatchman/bser.c']),
915 ]
916 ]
916
917
917 sys.path.insert(0, 'contrib/python-zstandard')
918 sys.path.insert(0, 'contrib/python-zstandard')
918 import setup_zstd
919 import setup_zstd
919 extmodules.append(setup_zstd.get_c_extension(name='mercurial.zstd'))
920 extmodules.append(setup_zstd.get_c_extension(name='mercurial.zstd'))
920
921
921 try:
922 try:
922 from distutils import cygwinccompiler
923 from distutils import cygwinccompiler
923
924
924 # the -mno-cygwin option has been deprecated for years
925 # the -mno-cygwin option has been deprecated for years
925 mingw32compilerclass = cygwinccompiler.Mingw32CCompiler
926 mingw32compilerclass = cygwinccompiler.Mingw32CCompiler
926
927
927 class HackedMingw32CCompiler(cygwinccompiler.Mingw32CCompiler):
928 class HackedMingw32CCompiler(cygwinccompiler.Mingw32CCompiler):
928 def __init__(self, *args, **kwargs):
929 def __init__(self, *args, **kwargs):
929 mingw32compilerclass.__init__(self, *args, **kwargs)
930 mingw32compilerclass.__init__(self, *args, **kwargs)
930 for i in 'compiler compiler_so linker_exe linker_so'.split():
931 for i in 'compiler compiler_so linker_exe linker_so'.split():
931 try:
932 try:
932 getattr(self, i).remove('-mno-cygwin')
933 getattr(self, i).remove('-mno-cygwin')
933 except ValueError:
934 except ValueError:
934 pass
935 pass
935
936
936 cygwinccompiler.Mingw32CCompiler = HackedMingw32CCompiler
937 cygwinccompiler.Mingw32CCompiler = HackedMingw32CCompiler
937 except ImportError:
938 except ImportError:
938 # the cygwinccompiler package is not available on some Python
939 # the cygwinccompiler package is not available on some Python
939 # distributions like the ones from the optware project for Synology
940 # distributions like the ones from the optware project for Synology
940 # DiskStation boxes
941 # DiskStation boxes
941 class HackedMingw32CCompiler(object):
942 class HackedMingw32CCompiler(object):
942 pass
943 pass
943
944
944 if os.name == 'nt':
945 if os.name == 'nt':
945 # Allow compiler/linker flags to be added to Visual Studio builds. Passing
946 # Allow compiler/linker flags to be added to Visual Studio builds. Passing
946 # extra_link_args to distutils.extensions.Extension() doesn't have any
947 # extra_link_args to distutils.extensions.Extension() doesn't have any
947 # effect.
948 # effect.
948 from distutils import msvccompiler
949 from distutils import msvccompiler
949
950
950 msvccompilerclass = msvccompiler.MSVCCompiler
951 msvccompilerclass = msvccompiler.MSVCCompiler
951
952
952 class HackedMSVCCompiler(msvccompiler.MSVCCompiler):
953 class HackedMSVCCompiler(msvccompiler.MSVCCompiler):
953 def initialize(self):
954 def initialize(self):
954 msvccompilerclass.initialize(self)
955 msvccompilerclass.initialize(self)
955 # "warning LNK4197: export 'func' specified multiple times"
956 # "warning LNK4197: export 'func' specified multiple times"
956 self.ldflags_shared.append('/ignore:4197')
957 self.ldflags_shared.append('/ignore:4197')
957 self.ldflags_shared_debug.append('/ignore:4197')
958 self.ldflags_shared_debug.append('/ignore:4197')
958
959
959 msvccompiler.MSVCCompiler = HackedMSVCCompiler
960 msvccompiler.MSVCCompiler = HackedMSVCCompiler
960
961
961 packagedata = {'mercurial': ['locale/*/LC_MESSAGES/hg.mo',
962 packagedata = {'mercurial': ['locale/*/LC_MESSAGES/hg.mo',
962 'help/*.txt',
963 'help/*.txt',
963 'help/internals/*.txt',
964 'help/internals/*.txt',
964 'default.d/*.rc',
965 'default.d/*.rc',
965 'dummycert.pem']}
966 'dummycert.pem']}
966
967
967 def ordinarypath(p):
968 def ordinarypath(p):
968 return p and p[0] != '.' and p[-1] != '~'
969 return p and p[0] != '.' and p[-1] != '~'
969
970
970 for root in ('templates',):
971 for root in ('templates',):
971 for curdir, dirs, files in os.walk(os.path.join('mercurial', root)):
972 for curdir, dirs, files in os.walk(os.path.join('mercurial', root)):
972 curdir = curdir.split(os.sep, 1)[1]
973 curdir = curdir.split(os.sep, 1)[1]
973 dirs[:] = filter(ordinarypath, dirs)
974 dirs[:] = filter(ordinarypath, dirs)
974 for f in filter(ordinarypath, files):
975 for f in filter(ordinarypath, files):
975 f = os.path.join(curdir, f)
976 f = os.path.join(curdir, f)
976 packagedata['mercurial'].append(f)
977 packagedata['mercurial'].append(f)
977
978
978 datafiles = []
979 datafiles = []
979
980
980 # distutils expects version to be str/unicode. Converting it to
981 # distutils expects version to be str/unicode. Converting it to
981 # unicode on Python 2 still works because it won't contain any
982 # unicode on Python 2 still works because it won't contain any
982 # non-ascii bytes and will be implicitly converted back to bytes
983 # non-ascii bytes and will be implicitly converted back to bytes
983 # when operated on.
984 # when operated on.
984 assert isinstance(version, bytes)
985 assert isinstance(version, bytes)
985 setupversion = version.decode('ascii')
986 setupversion = version.decode('ascii')
986
987
987 extra = {}
988 extra = {}
988
989
989 if issetuptools:
990 if issetuptools:
990 extra['python_requires'] = supportedpy
991 extra['python_requires'] = supportedpy
991 if py2exeloaded:
992 if py2exeloaded:
992 extra['console'] = [
993 extra['console'] = [
993 {'script':'hg',
994 {'script':'hg',
994 'copyright':'Copyright (C) 2005-2018 Matt Mackall and others',
995 'copyright':'Copyright (C) 2005-2018 Matt Mackall and others',
995 'product_version':version}]
996 'product_version':version}]
996 # sub command of 'build' because 'py2exe' does not handle sub_commands
997 # sub command of 'build' because 'py2exe' does not handle sub_commands
997 build.sub_commands.insert(0, ('build_hgextindex', None))
998 build.sub_commands.insert(0, ('build_hgextindex', None))
998 # put dlls in sub directory so that they won't pollute PATH
999 # put dlls in sub directory so that they won't pollute PATH
999 extra['zipfile'] = 'lib/library.zip'
1000 extra['zipfile'] = 'lib/library.zip'
1000
1001
1001 if os.name == 'nt':
1002 if os.name == 'nt':
1002 # Windows binary file versions for exe/dll files must have the
1003 # Windows binary file versions for exe/dll files must have the
1003 # form W.X.Y.Z, where W,X,Y,Z are numbers in the range 0..65535
1004 # form W.X.Y.Z, where W,X,Y,Z are numbers in the range 0..65535
1004 setupversion = version.split(b'+', 1)[0]
1005 setupversion = version.split(b'+', 1)[0]
1005
1006
1006 if sys.platform == 'darwin' and os.path.exists('/usr/bin/xcodebuild'):
1007 if sys.platform == 'darwin' and os.path.exists('/usr/bin/xcodebuild'):
1007 version = runcmd(['/usr/bin/xcodebuild', '-version'], {})[1].splitlines()
1008 version = runcmd(['/usr/bin/xcodebuild', '-version'], {})[1].splitlines()
1008 if version:
1009 if version:
1009 version = version[0]
1010 version = version[0]
1010 if sys.version_info[0] == 3:
1011 if sys.version_info[0] == 3:
1011 version = version.decode('utf-8')
1012 version = version.decode('utf-8')
1012 xcode4 = (version.startswith('Xcode') and
1013 xcode4 = (version.startswith('Xcode') and
1013 StrictVersion(version.split()[1]) >= StrictVersion('4.0'))
1014 StrictVersion(version.split()[1]) >= StrictVersion('4.0'))
1014 xcode51 = re.match(r'^Xcode\s+5\.1', version) is not None
1015 xcode51 = re.match(r'^Xcode\s+5\.1', version) is not None
1015 else:
1016 else:
1016 # xcodebuild returns empty on OS X Lion with XCode 4.3 not
1017 # xcodebuild returns empty on OS X Lion with XCode 4.3 not
1017 # installed, but instead with only command-line tools. Assume
1018 # installed, but instead with only command-line tools. Assume
1018 # that only happens on >= Lion, thus no PPC support.
1019 # that only happens on >= Lion, thus no PPC support.
1019 xcode4 = True
1020 xcode4 = True
1020 xcode51 = False
1021 xcode51 = False
1021
1022
1022 # XCode 4.0 dropped support for ppc architecture, which is hardcoded in
1023 # XCode 4.0 dropped support for ppc architecture, which is hardcoded in
1023 # distutils.sysconfig
1024 # distutils.sysconfig
1024 if xcode4:
1025 if xcode4:
1025 os.environ['ARCHFLAGS'] = ''
1026 os.environ['ARCHFLAGS'] = ''
1026
1027
1027 # XCode 5.1 changes clang such that it now fails to compile if the
1028 # XCode 5.1 changes clang such that it now fails to compile if the
1028 # -mno-fused-madd flag is passed, but the version of Python shipped with
1029 # -mno-fused-madd flag is passed, but the version of Python shipped with
1029 # OS X 10.9 Mavericks includes this flag. This causes problems in all
1030 # OS X 10.9 Mavericks includes this flag. This causes problems in all
1030 # C extension modules, and a bug has been filed upstream at
1031 # C extension modules, and a bug has been filed upstream at
1031 # http://bugs.python.org/issue21244. We also need to patch this here
1032 # http://bugs.python.org/issue21244. We also need to patch this here
1032 # so Mercurial can continue to compile in the meantime.
1033 # so Mercurial can continue to compile in the meantime.
1033 if xcode51:
1034 if xcode51:
1034 cflags = get_config_var('CFLAGS')
1035 cflags = get_config_var('CFLAGS')
1035 if cflags and re.search(r'-mno-fused-madd\b', cflags) is not None:
1036 if cflags and re.search(r'-mno-fused-madd\b', cflags) is not None:
1036 os.environ['CFLAGS'] = (
1037 os.environ['CFLAGS'] = (
1037 os.environ.get('CFLAGS', '') + ' -Qunused-arguments')
1038 os.environ.get('CFLAGS', '') + ' -Qunused-arguments')
1038
1039
1039 setup(name='mercurial',
1040 setup(name='mercurial',
1040 version=setupversion,
1041 version=setupversion,
1041 author='Matt Mackall and many others',
1042 author='Matt Mackall and many others',
1042 author_email='mercurial@mercurial-scm.org',
1043 author_email='mercurial@mercurial-scm.org',
1043 url='https://mercurial-scm.org/',
1044 url='https://mercurial-scm.org/',
1044 download_url='https://mercurial-scm.org/release/',
1045 download_url='https://mercurial-scm.org/release/',
1045 description=('Fast scalable distributed SCM (revision control, version '
1046 description=('Fast scalable distributed SCM (revision control, version '
1046 'control) system'),
1047 'control) system'),
1047 long_description=('Mercurial is a distributed SCM tool written in Python.'
1048 long_description=('Mercurial is a distributed SCM tool written in Python.'
1048 ' It is used by a number of large projects that require'
1049 ' It is used by a number of large projects that require'
1049 ' fast, reliable distributed revision control, such as '
1050 ' fast, reliable distributed revision control, such as '
1050 'Mozilla.'),
1051 'Mozilla.'),
1051 license='GNU GPLv2 or any later version',
1052 license='GNU GPLv2 or any later version',
1052 classifiers=[
1053 classifiers=[
1053 'Development Status :: 6 - Mature',
1054 'Development Status :: 6 - Mature',
1054 'Environment :: Console',
1055 'Environment :: Console',
1055 'Intended Audience :: Developers',
1056 'Intended Audience :: Developers',
1056 'Intended Audience :: System Administrators',
1057 'Intended Audience :: System Administrators',
1057 'License :: OSI Approved :: GNU General Public License (GPL)',
1058 'License :: OSI Approved :: GNU General Public License (GPL)',
1058 'Natural Language :: Danish',
1059 'Natural Language :: Danish',
1059 'Natural Language :: English',
1060 'Natural Language :: English',
1060 'Natural Language :: German',
1061 'Natural Language :: German',
1061 'Natural Language :: Italian',
1062 'Natural Language :: Italian',
1062 'Natural Language :: Japanese',
1063 'Natural Language :: Japanese',
1063 'Natural Language :: Portuguese (Brazilian)',
1064 'Natural Language :: Portuguese (Brazilian)',
1064 'Operating System :: Microsoft :: Windows',
1065 'Operating System :: Microsoft :: Windows',
1065 'Operating System :: OS Independent',
1066 'Operating System :: OS Independent',
1066 'Operating System :: POSIX',
1067 'Operating System :: POSIX',
1067 'Programming Language :: C',
1068 'Programming Language :: C',
1068 'Programming Language :: Python',
1069 'Programming Language :: Python',
1069 'Topic :: Software Development :: Version Control',
1070 'Topic :: Software Development :: Version Control',
1070 ],
1071 ],
1071 scripts=scripts,
1072 scripts=scripts,
1072 packages=packages,
1073 packages=packages,
1073 ext_modules=extmodules,
1074 ext_modules=extmodules,
1074 data_files=datafiles,
1075 data_files=datafiles,
1075 package_data=packagedata,
1076 package_data=packagedata,
1076 cmdclass=cmdclass,
1077 cmdclass=cmdclass,
1077 distclass=hgdist,
1078 distclass=hgdist,
1078 options={
1079 options={
1079 'py2exe': {
1080 'py2exe': {
1080 'packages': [
1081 'packages': [
1081 'hgdemandimport',
1082 'hgdemandimport',
1082 'hgext',
1083 'hgext',
1083 'email',
1084 'email',
1084 # implicitly imported per module policy
1085 # implicitly imported per module policy
1085 # (cffi wouldn't be used as a frozen exe)
1086 # (cffi wouldn't be used as a frozen exe)
1086 'mercurial.cext',
1087 'mercurial.cext',
1087 #'mercurial.cffi',
1088 #'mercurial.cffi',
1088 'mercurial.pure',
1089 'mercurial.pure',
1089 ],
1090 ],
1090 },
1091 },
1091 'bdist_mpkg': {
1092 'bdist_mpkg': {
1092 'zipdist': False,
1093 'zipdist': False,
1093 'license': 'COPYING',
1094 'license': 'COPYING',
1094 'readme': 'contrib/packaging/macosx/Readme.html',
1095 'readme': 'contrib/packaging/macosx/Readme.html',
1095 'welcome': 'contrib/packaging/macosx/Welcome.html',
1096 'welcome': 'contrib/packaging/macosx/Welcome.html',
1096 },
1097 },
1097 },
1098 },
1098 **extra)
1099 **extra)
General Comments 0
You need to be logged in to leave comments. Login now