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