##// END OF EJS Templates
clang-format: do not accept version above 19.x...
marmoute -
r52955:73cf8b56 default
parent child Browse files
Show More
@@ -1,1178 +1,1179
1 import os
1 import os
2 import re
2 import re
3 import socket
3 import socket
4 import stat
4 import stat
5 import subprocess
5 import subprocess
6 import sys
6 import sys
7 import tempfile
7 import tempfile
8
8
9 try:
9 try:
10 from setuptools.extern.packaging.version import Version
10 from setuptools.extern.packaging.version import Version
11 except ImportError:
11 except ImportError:
12 from distutils.version import StrictVersion as Version
12 from distutils.version import StrictVersion as Version
13
13
14 tempprefix = 'hg-hghave-'
14 tempprefix = 'hg-hghave-'
15
15
16 checks = {
16 checks = {
17 "true": (lambda: True, "yak shaving"),
17 "true": (lambda: True, "yak shaving"),
18 "false": (lambda: False, "nail clipper"),
18 "false": (lambda: False, "nail clipper"),
19 "known-bad-output": (lambda: True, "use for currently known bad output"),
19 "known-bad-output": (lambda: True, "use for currently known bad output"),
20 "missing-correct-output": (lambda: False, "use for missing good output"),
20 "missing-correct-output": (lambda: False, "use for missing good output"),
21 }
21 }
22
22
23 try:
23 try:
24 import msvcrt
24 import msvcrt
25
25
26 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
26 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
27 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
27 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
28 except ImportError:
28 except ImportError:
29 pass
29 pass
30
30
31 stdout = getattr(sys.stdout, 'buffer', sys.stdout)
31 stdout = getattr(sys.stdout, 'buffer', sys.stdout)
32 stderr = getattr(sys.stderr, 'buffer', sys.stderr)
32 stderr = getattr(sys.stderr, 'buffer', sys.stderr)
33
33
34
34
35 def _sys2bytes(p):
35 def _sys2bytes(p):
36 if p is None:
36 if p is None:
37 return p
37 return p
38 return p.encode('utf-8')
38 return p.encode('utf-8')
39
39
40
40
41 def _bytes2sys(p):
41 def _bytes2sys(p):
42 if p is None:
42 if p is None:
43 return p
43 return p
44 return p.decode('utf-8')
44 return p.decode('utf-8')
45
45
46
46
47 def check(name, desc):
47 def check(name, desc):
48 """Registers a check function for a feature."""
48 """Registers a check function for a feature."""
49
49
50 def decorator(func):
50 def decorator(func):
51 checks[name] = (func, desc)
51 checks[name] = (func, desc)
52 return func
52 return func
53
53
54 return decorator
54 return decorator
55
55
56
56
57 def checkvers(name, desc, vers):
57 def checkvers(name, desc, vers):
58 """Registers a check function for each of a series of versions.
58 """Registers a check function for each of a series of versions.
59
59
60 vers can be a list or an iterator.
60 vers can be a list or an iterator.
61
61
62 Produces a series of feature checks that have the form <name><vers> without
62 Produces a series of feature checks that have the form <name><vers> without
63 any punctuation (even if there's punctuation in 'vers'; i.e. this produces
63 any punctuation (even if there's punctuation in 'vers'; i.e. this produces
64 'py38', not 'py3.8' or 'py-38')."""
64 'py38', not 'py3.8' or 'py-38')."""
65
65
66 def decorator(func):
66 def decorator(func):
67 def funcv(v):
67 def funcv(v):
68 def f():
68 def f():
69 return func(v)
69 return func(v)
70
70
71 return f
71 return f
72
72
73 for v in vers:
73 for v in vers:
74 assert isinstance(v, str)
74 assert isinstance(v, str)
75 f = funcv(v)
75 f = funcv(v)
76 checks['%s%s' % (name, v.replace('.', ''))] = (f, desc % v)
76 checks['%s%s' % (name, v.replace('.', ''))] = (f, desc % v)
77 return func
77 return func
78
78
79 return decorator
79 return decorator
80
80
81
81
82 def checkfeatures(features):
82 def checkfeatures(features):
83 result = {
83 result = {
84 'error': [],
84 'error': [],
85 'missing': [],
85 'missing': [],
86 'skipped': [],
86 'skipped': [],
87 }
87 }
88
88
89 for feature in features:
89 for feature in features:
90 negate = feature.startswith('no-')
90 negate = feature.startswith('no-')
91 if negate:
91 if negate:
92 feature = feature[3:]
92 feature = feature[3:]
93
93
94 if feature not in checks:
94 if feature not in checks:
95 result['missing'].append(feature)
95 result['missing'].append(feature)
96 continue
96 continue
97
97
98 check, desc = checks[feature]
98 check, desc = checks[feature]
99 try:
99 try:
100 available = check()
100 available = check()
101 except Exception as e:
101 except Exception as e:
102 result['error'].append('hghave check %s failed: %r' % (feature, e))
102 result['error'].append('hghave check %s failed: %r' % (feature, e))
103 continue
103 continue
104
104
105 if not negate and not available:
105 if not negate and not available:
106 result['skipped'].append('missing feature: %s' % desc)
106 result['skipped'].append('missing feature: %s' % desc)
107 elif negate and available:
107 elif negate and available:
108 result['skipped'].append('system supports %s' % desc)
108 result['skipped'].append('system supports %s' % desc)
109
109
110 return result
110 return result
111
111
112
112
113 def require(features):
113 def require(features):
114 """Require that features are available, exiting if not."""
114 """Require that features are available, exiting if not."""
115 result = checkfeatures(features)
115 result = checkfeatures(features)
116
116
117 for missing in result['missing']:
117 for missing in result['missing']:
118 stderr.write(
118 stderr.write(
119 ('skipped: unknown feature: %s\n' % missing).encode('utf-8')
119 ('skipped: unknown feature: %s\n' % missing).encode('utf-8')
120 )
120 )
121 for msg in result['skipped']:
121 for msg in result['skipped']:
122 stderr.write(('skipped: %s\n' % msg).encode('utf-8'))
122 stderr.write(('skipped: %s\n' % msg).encode('utf-8'))
123 for msg in result['error']:
123 for msg in result['error']:
124 stderr.write(('%s\n' % msg).encode('utf-8'))
124 stderr.write(('%s\n' % msg).encode('utf-8'))
125
125
126 if result['missing']:
126 if result['missing']:
127 sys.exit(2)
127 sys.exit(2)
128
128
129 if result['skipped'] or result['error']:
129 if result['skipped'] or result['error']:
130 sys.exit(1)
130 sys.exit(1)
131
131
132
132
133 def matchoutput(cmd, regexp, ignorestatus=False):
133 def matchoutput(cmd, regexp, ignorestatus=False):
134 """Return the match object if cmd executes successfully and its output
134 """Return the match object if cmd executes successfully and its output
135 is matched by the supplied regular expression.
135 is matched by the supplied regular expression.
136 """
136 """
137
137
138 # Tests on Windows have to fake USERPROFILE to point to the test area so
138 # Tests on Windows have to fake USERPROFILE to point to the test area so
139 # that `~` is properly expanded on py3.8+. However, some tools like black
139 # that `~` is properly expanded on py3.8+. However, some tools like black
140 # make calls that need the real USERPROFILE in order to run `foo --version`.
140 # make calls that need the real USERPROFILE in order to run `foo --version`.
141 env = os.environ
141 env = os.environ
142 if os.name == 'nt':
142 if os.name == 'nt':
143 env = os.environ.copy()
143 env = os.environ.copy()
144 env['USERPROFILE'] = env['REALUSERPROFILE']
144 env['USERPROFILE'] = env['REALUSERPROFILE']
145
145
146 r = re.compile(regexp)
146 r = re.compile(regexp)
147 p = subprocess.Popen(
147 p = subprocess.Popen(
148 cmd,
148 cmd,
149 shell=True,
149 shell=True,
150 stdout=subprocess.PIPE,
150 stdout=subprocess.PIPE,
151 stderr=subprocess.STDOUT,
151 stderr=subprocess.STDOUT,
152 env=env,
152 env=env,
153 )
153 )
154 s = p.communicate()[0]
154 s = p.communicate()[0]
155 ret = p.returncode
155 ret = p.returncode
156 return (ignorestatus or not ret) and r.search(s)
156 return (ignorestatus or not ret) and r.search(s)
157
157
158
158
159 @check("baz", "GNU Arch baz client")
159 @check("baz", "GNU Arch baz client")
160 def has_baz():
160 def has_baz():
161 return matchoutput('baz --version 2>&1', br'baz Bazaar version')
161 return matchoutput('baz --version 2>&1', br'baz Bazaar version')
162
162
163
163
164 @check("bzr", "Breezy library and executable version >= 3.1")
164 @check("bzr", "Breezy library and executable version >= 3.1")
165 def has_bzr():
165 def has_bzr():
166 try:
166 try:
167 # Test the Breezy python lib
167 # Test the Breezy python lib
168 import breezy
168 import breezy
169 import breezy.bzr.bzrdir
169 import breezy.bzr.bzrdir
170 import breezy.errors
170 import breezy.errors
171 import breezy.revision
171 import breezy.revision
172 import breezy.revisionspec
172 import breezy.revisionspec
173
173
174 breezy.revisionspec.RevisionSpec
174 breezy.revisionspec.RevisionSpec
175 if breezy.__doc__ is None or breezy.version_info[:2] < (3, 1):
175 if breezy.__doc__ is None or breezy.version_info[:2] < (3, 1):
176 return False
176 return False
177 except (AttributeError, ImportError):
177 except (AttributeError, ImportError):
178 return False
178 return False
179 # Test the executable
179 # Test the executable
180 return matchoutput('brz --version 2>&1', br'Breezy \(brz\) ')
180 return matchoutput('brz --version 2>&1', br'Breezy \(brz\) ')
181
181
182
182
183 @check("chg", "running with chg")
183 @check("chg", "running with chg")
184 def has_chg():
184 def has_chg():
185 return 'CHG_INSTALLED_AS_HG' in os.environ
185 return 'CHG_INSTALLED_AS_HG' in os.environ
186
186
187
187
188 @check("rhg", "running with rhg as 'hg'")
188 @check("rhg", "running with rhg as 'hg'")
189 def has_rhg():
189 def has_rhg():
190 return 'RHG_INSTALLED_AS_HG' in os.environ
190 return 'RHG_INSTALLED_AS_HG' in os.environ
191
191
192
192
193 @check("pyoxidizer", "running with pyoxidizer build as 'hg'")
193 @check("pyoxidizer", "running with pyoxidizer build as 'hg'")
194 def has_pyoxidizer():
194 def has_pyoxidizer():
195 return 'PYOXIDIZED_INSTALLED_AS_HG' in os.environ
195 return 'PYOXIDIZED_INSTALLED_AS_HG' in os.environ
196
196
197
197
198 @check(
198 @check(
199 "pyoxidizer-in-memory",
199 "pyoxidizer-in-memory",
200 "running with pyoxidizer build as 'hg' with embedded resources",
200 "running with pyoxidizer build as 'hg' with embedded resources",
201 )
201 )
202 def has_pyoxidizer_mem():
202 def has_pyoxidizer_mem():
203 return 'PYOXIDIZED_IN_MEMORY_RSRC' in os.environ
203 return 'PYOXIDIZED_IN_MEMORY_RSRC' in os.environ
204
204
205
205
206 @check(
206 @check(
207 "pyoxidizer-in-filesystem",
207 "pyoxidizer-in-filesystem",
208 "running with pyoxidizer build as 'hg' with external resources",
208 "running with pyoxidizer build as 'hg' with external resources",
209 )
209 )
210 def has_pyoxidizer_fs():
210 def has_pyoxidizer_fs():
211 return 'PYOXIDIZED_FILESYSTEM_RSRC' in os.environ
211 return 'PYOXIDIZED_FILESYSTEM_RSRC' in os.environ
212
212
213
213
214 @check("cvs", "cvs client/server")
214 @check("cvs", "cvs client/server")
215 def has_cvs():
215 def has_cvs():
216 re = br'Concurrent Versions System.*?server'
216 re = br'Concurrent Versions System.*?server'
217 return matchoutput('cvs --version 2>&1', re) and not has_msys()
217 return matchoutput('cvs --version 2>&1', re) and not has_msys()
218
218
219
219
220 @check("cvs112", "cvs client/server 1.12.* (not cvsnt)")
220 @check("cvs112", "cvs client/server 1.12.* (not cvsnt)")
221 def has_cvs112():
221 def has_cvs112():
222 re = br'Concurrent Versions System \(CVS\) 1.12.*?server'
222 re = br'Concurrent Versions System \(CVS\) 1.12.*?server'
223 return matchoutput('cvs --version 2>&1', re) and not has_msys()
223 return matchoutput('cvs --version 2>&1', re) and not has_msys()
224
224
225
225
226 @check("cvsnt", "cvsnt client/server")
226 @check("cvsnt", "cvsnt client/server")
227 def has_cvsnt():
227 def has_cvsnt():
228 re = br'Concurrent Versions System \(CVSNT\) (\d+).(\d+).*\(client/server\)'
228 re = br'Concurrent Versions System \(CVSNT\) (\d+).(\d+).*\(client/server\)'
229 return matchoutput('cvsnt --version 2>&1', re)
229 return matchoutput('cvsnt --version 2>&1', re)
230
230
231
231
232 @check("darcs", "darcs client")
232 @check("darcs", "darcs client")
233 def has_darcs():
233 def has_darcs():
234 return matchoutput('darcs --version', br'\b2\.([2-9]|\d{2})', True)
234 return matchoutput('darcs --version', br'\b2\.([2-9]|\d{2})', True)
235
235
236
236
237 @check("mtn", "monotone client (>= 1.0)")
237 @check("mtn", "monotone client (>= 1.0)")
238 def has_mtn():
238 def has_mtn():
239 return matchoutput('mtn --version', br'monotone', True) and not matchoutput(
239 return matchoutput('mtn --version', br'monotone', True) and not matchoutput(
240 'mtn --version', br'monotone 0\.', True
240 'mtn --version', br'monotone 0\.', True
241 )
241 )
242
242
243
243
244 @check("eol-in-paths", "end-of-lines in paths")
244 @check("eol-in-paths", "end-of-lines in paths")
245 def has_eol_in_paths():
245 def has_eol_in_paths():
246 try:
246 try:
247 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix, suffix='\n\r')
247 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix, suffix='\n\r')
248 os.close(fd)
248 os.close(fd)
249 os.remove(path)
249 os.remove(path)
250 return True
250 return True
251 except (IOError, OSError):
251 except (IOError, OSError):
252 return False
252 return False
253
253
254
254
255 @check("execbit", "executable bit")
255 @check("execbit", "executable bit")
256 def has_executablebit():
256 def has_executablebit():
257 try:
257 try:
258 EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
258 EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
259 fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix)
259 fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix)
260 try:
260 try:
261 os.close(fh)
261 os.close(fh)
262 m = os.stat(fn).st_mode & 0o777
262 m = os.stat(fn).st_mode & 0o777
263 new_file_has_exec = m & EXECFLAGS
263 new_file_has_exec = m & EXECFLAGS
264 os.chmod(fn, m ^ EXECFLAGS)
264 os.chmod(fn, m ^ EXECFLAGS)
265 exec_flags_cannot_flip = (os.stat(fn).st_mode & 0o777) == m
265 exec_flags_cannot_flip = (os.stat(fn).st_mode & 0o777) == m
266 finally:
266 finally:
267 os.unlink(fn)
267 os.unlink(fn)
268 except (IOError, OSError):
268 except (IOError, OSError):
269 # we don't care, the user probably won't be able to commit anyway
269 # we don't care, the user probably won't be able to commit anyway
270 return False
270 return False
271 return not (new_file_has_exec or exec_flags_cannot_flip)
271 return not (new_file_has_exec or exec_flags_cannot_flip)
272
272
273
273
274 @check("suidbit", "setuid and setgid bit")
274 @check("suidbit", "setuid and setgid bit")
275 def has_suidbit():
275 def has_suidbit():
276 if (
276 if (
277 getattr(os, "statvfs", None) is None
277 getattr(os, "statvfs", None) is None
278 or getattr(os, "ST_NOSUID", None) is None
278 or getattr(os, "ST_NOSUID", None) is None
279 ):
279 ):
280 return False
280 return False
281 return bool(os.statvfs('.').f_flag & os.ST_NOSUID)
281 return bool(os.statvfs('.').f_flag & os.ST_NOSUID)
282
282
283
283
284 @check("icasefs", "case insensitive file system")
284 @check("icasefs", "case insensitive file system")
285 def has_icasefs():
285 def has_icasefs():
286 # Stolen from mercurial.util
286 # Stolen from mercurial.util
287 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix)
287 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix)
288 os.close(fd)
288 os.close(fd)
289 try:
289 try:
290 s1 = os.stat(path)
290 s1 = os.stat(path)
291 d, b = os.path.split(path)
291 d, b = os.path.split(path)
292 p2 = os.path.join(d, b.upper())
292 p2 = os.path.join(d, b.upper())
293 if path == p2:
293 if path == p2:
294 p2 = os.path.join(d, b.lower())
294 p2 = os.path.join(d, b.lower())
295 try:
295 try:
296 s2 = os.stat(p2)
296 s2 = os.stat(p2)
297 return s2 == s1
297 return s2 == s1
298 except OSError:
298 except OSError:
299 return False
299 return False
300 finally:
300 finally:
301 os.remove(path)
301 os.remove(path)
302
302
303
303
304 @check("fifo", "named pipes")
304 @check("fifo", "named pipes")
305 def has_fifo():
305 def has_fifo():
306 if getattr(os, "mkfifo", None) is None:
306 if getattr(os, "mkfifo", None) is None:
307 return False
307 return False
308 name = tempfile.mktemp(dir='.', prefix=tempprefix)
308 name = tempfile.mktemp(dir='.', prefix=tempprefix)
309 try:
309 try:
310 os.mkfifo(name)
310 os.mkfifo(name)
311 os.unlink(name)
311 os.unlink(name)
312 return True
312 return True
313 except OSError:
313 except OSError:
314 return False
314 return False
315
315
316
316
317 @check("killdaemons", 'killdaemons.py support')
317 @check("killdaemons", 'killdaemons.py support')
318 def has_killdaemons():
318 def has_killdaemons():
319 return True
319 return True
320
320
321
321
322 @check("cacheable", "cacheable filesystem")
322 @check("cacheable", "cacheable filesystem")
323 def has_cacheable_fs():
323 def has_cacheable_fs():
324 from mercurial import util
324 from mercurial import util
325
325
326 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix)
326 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix)
327 os.close(fd)
327 os.close(fd)
328 try:
328 try:
329 return util.cachestat(_sys2bytes(path)).cacheable()
329 return util.cachestat(_sys2bytes(path)).cacheable()
330 finally:
330 finally:
331 os.remove(path)
331 os.remove(path)
332
332
333
333
334 @check("lsprof", "python lsprof module")
334 @check("lsprof", "python lsprof module")
335 def has_lsprof():
335 def has_lsprof():
336 try:
336 try:
337 import _lsprof
337 import _lsprof
338
338
339 _lsprof.Profiler # silence unused import warning
339 _lsprof.Profiler # silence unused import warning
340 return True
340 return True
341 except ImportError:
341 except ImportError:
342 return False
342 return False
343
343
344
344
345 def _gethgversion():
345 def _gethgversion():
346 m = matchoutput('hg --version --quiet 2>&1', br'(\d+)\.(\d+)')
346 m = matchoutput('hg --version --quiet 2>&1', br'(\d+)\.(\d+)')
347 if not m:
347 if not m:
348 return (0, 0)
348 return (0, 0)
349 return (int(m.group(1)), int(m.group(2)))
349 return (int(m.group(1)), int(m.group(2)))
350
350
351
351
352 _hgversion = None
352 _hgversion = None
353
353
354
354
355 def gethgversion():
355 def gethgversion():
356 global _hgversion
356 global _hgversion
357 if _hgversion is None:
357 if _hgversion is None:
358 _hgversion = _gethgversion()
358 _hgversion = _gethgversion()
359 return _hgversion
359 return _hgversion
360
360
361
361
362 @checkvers(
362 @checkvers(
363 "hg", "Mercurial >= %s", ['%d.%d' % divmod(x, 10) for x in range(9, 99)]
363 "hg", "Mercurial >= %s", ['%d.%d' % divmod(x, 10) for x in range(9, 99)]
364 )
364 )
365 def has_hg_range(v):
365 def has_hg_range(v):
366 major, minor = v.split('.')[0:2]
366 major, minor = v.split('.')[0:2]
367 return gethgversion() >= (int(major), int(minor))
367 return gethgversion() >= (int(major), int(minor))
368
368
369
369
370 @check("rust", "Using the Rust extensions")
370 @check("rust", "Using the Rust extensions")
371 def has_rust():
371 def has_rust():
372 """Check is the mercurial currently running is using some rust code"""
372 """Check is the mercurial currently running is using some rust code"""
373 cmd = 'hg debuginstall --quiet 2>&1'
373 cmd = 'hg debuginstall --quiet 2>&1'
374 match = br'checking module policy \(([^)]+)\)'
374 match = br'checking module policy \(([^)]+)\)'
375 policy = matchoutput(cmd, match)
375 policy = matchoutput(cmd, match)
376 if not policy:
376 if not policy:
377 return False
377 return False
378 return b'rust' in policy.group(1)
378 return b'rust' in policy.group(1)
379
379
380
380
381 @check("hg08", "Mercurial >= 0.8")
381 @check("hg08", "Mercurial >= 0.8")
382 def has_hg08():
382 def has_hg08():
383 if checks["hg09"][0]():
383 if checks["hg09"][0]():
384 return True
384 return True
385 return matchoutput('hg help annotate 2>&1', '--date')
385 return matchoutput('hg help annotate 2>&1', '--date')
386
386
387
387
388 @check("hg07", "Mercurial >= 0.7")
388 @check("hg07", "Mercurial >= 0.7")
389 def has_hg07():
389 def has_hg07():
390 if checks["hg08"][0]():
390 if checks["hg08"][0]():
391 return True
391 return True
392 return matchoutput('hg --version --quiet 2>&1', 'Mercurial Distributed SCM')
392 return matchoutput('hg --version --quiet 2>&1', 'Mercurial Distributed SCM')
393
393
394
394
395 @check("hg06", "Mercurial >= 0.6")
395 @check("hg06", "Mercurial >= 0.6")
396 def has_hg06():
396 def has_hg06():
397 if checks["hg07"][0]():
397 if checks["hg07"][0]():
398 return True
398 return True
399 return matchoutput('hg --version --quiet 2>&1', 'Mercurial version')
399 return matchoutput('hg --version --quiet 2>&1', 'Mercurial version')
400
400
401
401
402 @check("gettext", "GNU Gettext (msgfmt)")
402 @check("gettext", "GNU Gettext (msgfmt)")
403 def has_gettext():
403 def has_gettext():
404 return matchoutput('msgfmt --version', br'GNU gettext-tools')
404 return matchoutput('msgfmt --version', br'GNU gettext-tools')
405
405
406
406
407 @check("git", "git command line client")
407 @check("git", "git command line client")
408 def has_git():
408 def has_git():
409 return matchoutput('git --version 2>&1', br'^git version')
409 return matchoutput('git --version 2>&1', br'^git version')
410
410
411
411
412 def getgitversion():
412 def getgitversion():
413 m = matchoutput('git --version 2>&1', br'git version (\d+)\.(\d+)')
413 m = matchoutput('git --version 2>&1', br'git version (\d+)\.(\d+)')
414 if not m:
414 if not m:
415 return (0, 0)
415 return (0, 0)
416 return (int(m.group(1)), int(m.group(2)))
416 return (int(m.group(1)), int(m.group(2)))
417
417
418
418
419 @check("pygit2", "pygit2 Python library")
419 @check("pygit2", "pygit2 Python library")
420 def has_pygit2():
420 def has_pygit2():
421 try:
421 try:
422 import pygit2
422 import pygit2
423
423
424 pygit2.Oid # silence unused import
424 pygit2.Oid # silence unused import
425 return True
425 return True
426 except ImportError:
426 except ImportError:
427 return False
427 return False
428
428
429
429
430 # https://github.com/git-lfs/lfs-test-server
430 # https://github.com/git-lfs/lfs-test-server
431 @check("lfs-test-server", "git-lfs test server")
431 @check("lfs-test-server", "git-lfs test server")
432 def has_lfsserver():
432 def has_lfsserver():
433 exe = 'lfs-test-server'
433 exe = 'lfs-test-server'
434 if has_windows():
434 if has_windows():
435 exe = 'lfs-test-server.exe'
435 exe = 'lfs-test-server.exe'
436 return any(
436 return any(
437 os.access(os.path.join(path, exe), os.X_OK)
437 os.access(os.path.join(path, exe), os.X_OK)
438 for path in os.environ["PATH"].split(os.pathsep)
438 for path in os.environ["PATH"].split(os.pathsep)
439 )
439 )
440
440
441
441
442 @checkvers("git", "git client (with ext::sh support) version >= %s", ('1.9',))
442 @checkvers("git", "git client (with ext::sh support) version >= %s", ('1.9',))
443 def has_git_range(v):
443 def has_git_range(v):
444 major, minor = v.split('.')[0:2]
444 major, minor = v.split('.')[0:2]
445 return getgitversion() >= (int(major), int(minor))
445 return getgitversion() >= (int(major), int(minor))
446
446
447
447
448 @check("docutils", "Docutils text processing library")
448 @check("docutils", "Docutils text processing library")
449 def has_docutils():
449 def has_docutils():
450 try:
450 try:
451 import docutils.core
451 import docutils.core
452
452
453 docutils.core.publish_cmdline # silence unused import
453 docutils.core.publish_cmdline # silence unused import
454 return True
454 return True
455 except ImportError:
455 except ImportError:
456 return False
456 return False
457
457
458
458
459 def getsvnversion():
459 def getsvnversion():
460 m = matchoutput('svn --version --quiet 2>&1', br'^(\d+)\.(\d+)')
460 m = matchoutput('svn --version --quiet 2>&1', br'^(\d+)\.(\d+)')
461 if not m:
461 if not m:
462 return (0, 0)
462 return (0, 0)
463 return (int(m.group(1)), int(m.group(2)))
463 return (int(m.group(1)), int(m.group(2)))
464
464
465
465
466 @checkvers("svn", "subversion client and admin tools >= %s", ('1.3', '1.5'))
466 @checkvers("svn", "subversion client and admin tools >= %s", ('1.3', '1.5'))
467 def has_svn_range(v):
467 def has_svn_range(v):
468 major, minor = v.split('.')[0:2]
468 major, minor = v.split('.')[0:2]
469 return getsvnversion() >= (int(major), int(minor))
469 return getsvnversion() >= (int(major), int(minor))
470
470
471
471
472 @check("svn", "subversion client and admin tools")
472 @check("svn", "subversion client and admin tools")
473 def has_svn():
473 def has_svn():
474 return matchoutput('svn --version 2>&1', br'^svn, version') and matchoutput(
474 return matchoutput('svn --version 2>&1', br'^svn, version') and matchoutput(
475 'svnadmin --version 2>&1', br'^svnadmin, version'
475 'svnadmin --version 2>&1', br'^svnadmin, version'
476 )
476 )
477
477
478
478
479 @check("svn-bindings", "subversion python bindings")
479 @check("svn-bindings", "subversion python bindings")
480 def has_svn_bindings():
480 def has_svn_bindings():
481 try:
481 try:
482 import svn.core
482 import svn.core
483
483
484 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
484 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
485 if version < (1, 4):
485 if version < (1, 4):
486 return False
486 return False
487 return True
487 return True
488 except ImportError:
488 except ImportError:
489 return False
489 return False
490
490
491
491
492 @check("p4", "Perforce server and client")
492 @check("p4", "Perforce server and client")
493 def has_p4():
493 def has_p4():
494 return matchoutput('p4 -V', br'Rev\. P4/') and matchoutput(
494 return matchoutput('p4 -V', br'Rev\. P4/') and matchoutput(
495 'p4d -V', br'Rev\. P4D/'
495 'p4d -V', br'Rev\. P4D/'
496 )
496 )
497
497
498
498
499 @check("symlink", "symbolic links")
499 @check("symlink", "symbolic links")
500 def has_symlink():
500 def has_symlink():
501 # mercurial.windows.checklink() is a hard 'no' at the moment
501 # mercurial.windows.checklink() is a hard 'no' at the moment
502 if os.name == 'nt' or getattr(os, "symlink", None) is None:
502 if os.name == 'nt' or getattr(os, "symlink", None) is None:
503 return False
503 return False
504 name = tempfile.mktemp(dir='.', prefix=tempprefix)
504 name = tempfile.mktemp(dir='.', prefix=tempprefix)
505 try:
505 try:
506 os.symlink(".", name)
506 os.symlink(".", name)
507 os.unlink(name)
507 os.unlink(name)
508 return True
508 return True
509 except (OSError, AttributeError):
509 except (OSError, AttributeError):
510 return False
510 return False
511
511
512
512
513 @check("hardlink", "hardlinks")
513 @check("hardlink", "hardlinks")
514 def has_hardlink():
514 def has_hardlink():
515 from mercurial import util
515 from mercurial import util
516
516
517 fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix)
517 fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix)
518 os.close(fh)
518 os.close(fh)
519 name = tempfile.mktemp(dir='.', prefix=tempprefix)
519 name = tempfile.mktemp(dir='.', prefix=tempprefix)
520 try:
520 try:
521 util.oslink(_sys2bytes(fn), _sys2bytes(name))
521 util.oslink(_sys2bytes(fn), _sys2bytes(name))
522 os.unlink(name)
522 os.unlink(name)
523 return True
523 return True
524 except OSError:
524 except OSError:
525 return False
525 return False
526 finally:
526 finally:
527 os.unlink(fn)
527 os.unlink(fn)
528
528
529
529
530 @check("hardlink-whitelisted", "hardlinks on whitelisted filesystems")
530 @check("hardlink-whitelisted", "hardlinks on whitelisted filesystems")
531 def has_hardlink_whitelisted():
531 def has_hardlink_whitelisted():
532 from mercurial import util
532 from mercurial import util
533
533
534 try:
534 try:
535 fstype = util.getfstype(b'.')
535 fstype = util.getfstype(b'.')
536 except OSError:
536 except OSError:
537 return False
537 return False
538 return fstype in util._hardlinkfswhitelist
538 return fstype in util._hardlinkfswhitelist
539
539
540
540
541 @check("rmcwd", "can remove current working directory")
541 @check("rmcwd", "can remove current working directory")
542 def has_rmcwd():
542 def has_rmcwd():
543 ocwd = os.getcwd()
543 ocwd = os.getcwd()
544 temp = tempfile.mkdtemp(dir='.', prefix=tempprefix)
544 temp = tempfile.mkdtemp(dir='.', prefix=tempprefix)
545 try:
545 try:
546 os.chdir(temp)
546 os.chdir(temp)
547 # On Linux, 'rmdir .' isn't allowed, but the other names are okay.
547 # On Linux, 'rmdir .' isn't allowed, but the other names are okay.
548 # On Solaris and Windows, the cwd can't be removed by any names.
548 # On Solaris and Windows, the cwd can't be removed by any names.
549 os.rmdir(os.getcwd())
549 os.rmdir(os.getcwd())
550 return True
550 return True
551 except OSError:
551 except OSError:
552 return False
552 return False
553 finally:
553 finally:
554 os.chdir(ocwd)
554 os.chdir(ocwd)
555 # clean up temp dir on platforms where cwd can't be removed
555 # clean up temp dir on platforms where cwd can't be removed
556 try:
556 try:
557 os.rmdir(temp)
557 os.rmdir(temp)
558 except OSError:
558 except OSError:
559 pass
559 pass
560
560
561
561
562 @check("tla", "GNU Arch tla client")
562 @check("tla", "GNU Arch tla client")
563 def has_tla():
563 def has_tla():
564 return matchoutput('tla --version 2>&1', br'The GNU Arch Revision')
564 return matchoutput('tla --version 2>&1', br'The GNU Arch Revision')
565
565
566
566
567 @check("gpg", "gpg client")
567 @check("gpg", "gpg client")
568 def has_gpg():
568 def has_gpg():
569 return matchoutput('gpg --version 2>&1', br'GnuPG')
569 return matchoutput('gpg --version 2>&1', br'GnuPG')
570
570
571
571
572 @check("gpg2", "gpg client v2")
572 @check("gpg2", "gpg client v2")
573 def has_gpg2():
573 def has_gpg2():
574 return matchoutput('gpg --version 2>&1', br'GnuPG[^0-9]+2\.')
574 return matchoutput('gpg --version 2>&1', br'GnuPG[^0-9]+2\.')
575
575
576
576
577 @check("gpg21", "gpg client v2.1+")
577 @check("gpg21", "gpg client v2.1+")
578 def has_gpg21():
578 def has_gpg21():
579 return matchoutput('gpg --version 2>&1', br'GnuPG[^0-9]+2\.(?!0)')
579 return matchoutput('gpg --version 2>&1', br'GnuPG[^0-9]+2\.(?!0)')
580
580
581
581
582 @check("unix-permissions", "unix-style permissions")
582 @check("unix-permissions", "unix-style permissions")
583 def has_unix_permissions():
583 def has_unix_permissions():
584 d = tempfile.mkdtemp(dir='.', prefix=tempprefix)
584 d = tempfile.mkdtemp(dir='.', prefix=tempprefix)
585 try:
585 try:
586 fname = os.path.join(d, 'foo')
586 fname = os.path.join(d, 'foo')
587 for umask in (0o77, 0o07, 0o22):
587 for umask in (0o77, 0o07, 0o22):
588 os.umask(umask)
588 os.umask(umask)
589 f = open(fname, 'w')
589 f = open(fname, 'w')
590 f.close()
590 f.close()
591 mode = os.stat(fname).st_mode
591 mode = os.stat(fname).st_mode
592 os.unlink(fname)
592 os.unlink(fname)
593 if mode & 0o777 != ~umask & 0o666:
593 if mode & 0o777 != ~umask & 0o666:
594 return False
594 return False
595 return True
595 return True
596 finally:
596 finally:
597 os.rmdir(d)
597 os.rmdir(d)
598
598
599
599
600 @check("unix-socket", "AF_UNIX socket family")
600 @check("unix-socket", "AF_UNIX socket family")
601 def has_unix_socket():
601 def has_unix_socket():
602 return getattr(socket, 'AF_UNIX', None) is not None
602 return getattr(socket, 'AF_UNIX', None) is not None
603
603
604
604
605 @check("root", "root permissions")
605 @check("root", "root permissions")
606 def has_root():
606 def has_root():
607 return getattr(os, 'geteuid', None) and os.geteuid() == 0
607 return getattr(os, 'geteuid', None) and os.geteuid() == 0
608
608
609
609
610 @check("pyflakes", "Pyflakes python linter")
610 @check("pyflakes", "Pyflakes python linter")
611 def has_pyflakes():
611 def has_pyflakes():
612 try:
612 try:
613 import pyflakes
613 import pyflakes
614
614
615 pyflakes.__version__
615 pyflakes.__version__
616 except ImportError:
616 except ImportError:
617 return False
617 return False
618 else:
618 else:
619 return True
619 return True
620
620
621
621
622 @check("pylint", "Pylint python linter")
622 @check("pylint", "Pylint python linter")
623 def has_pylint():
623 def has_pylint():
624 return matchoutput("pylint --help", br"[Uu]sage:[ ]+pylint", True)
624 return matchoutput("pylint --help", br"[Uu]sage:[ ]+pylint", True)
625
625
626
626
627 @check("clang-format", "clang-format C code formatter (>= 11)")
627 @check("clang-format", "clang-format C code formatter (11 <= … < 19)")
628 def has_clang_format():
628 def has_clang_format():
629 m = matchoutput('clang-format --version', br'clang-format version (\d+)')
629 m = matchoutput('clang-format --version', br'clang-format version (\d+)')
630 # style changed somewhere between 10.x and 11.x
630 # style changed somewhere between 10.x and 11.x and after 19.
631 if m:
631 if m:
632 return int(m.group(1)) >= 11
632 major_version = int(m.group(1))
633 return 11 <= major_version < 19
633 # Assist Googler contributors, they have a centrally-maintained version of
634 # Assist Googler contributors, they have a centrally-maintained version of
634 # clang-format that is generally very fresh, but unlike most builds (both
635 # clang-format that is generally very fresh, but unlike most builds (both
635 # official and unofficial), it does *not* include a version number.
636 # official and unofficial), it does *not* include a version number.
636 return matchoutput(
637 return matchoutput(
637 'clang-format --version', br'clang-format .*google3-trunk \([0-9a-f]+\)'
638 'clang-format --version', br'clang-format .*google3-trunk \([0-9a-f]+\)'
638 )
639 )
639
640
640
641
641 @check("jshint", "JSHint static code analysis tool")
642 @check("jshint", "JSHint static code analysis tool")
642 def has_jshint():
643 def has_jshint():
643 return matchoutput("jshint --version 2>&1", br"jshint v")
644 return matchoutput("jshint --version 2>&1", br"jshint v")
644
645
645
646
646 @check("pygments", "Pygments source highlighting library")
647 @check("pygments", "Pygments source highlighting library")
647 def has_pygments():
648 def has_pygments():
648 try:
649 try:
649 import pygments
650 import pygments
650
651
651 pygments.highlight # silence unused import warning
652 pygments.highlight # silence unused import warning
652 return True
653 return True
653 except ImportError:
654 except ImportError:
654 return False
655 return False
655
656
656
657
657 def getpygmentsversion():
658 def getpygmentsversion():
658 try:
659 try:
659 import pygments
660 import pygments
660
661
661 v = pygments.__version__
662 v = pygments.__version__
662
663
663 parts = v.split(".")
664 parts = v.split(".")
664 return (int(parts[0]), int(parts[1]))
665 return (int(parts[0]), int(parts[1]))
665 except ImportError:
666 except ImportError:
666 return (0, 0)
667 return (0, 0)
667
668
668
669
669 @checkvers("pygments", "Pygments version >= %s", ('2.5', '2.11', '2.14'))
670 @checkvers("pygments", "Pygments version >= %s", ('2.5', '2.11', '2.14'))
670 def has_pygments_range(v):
671 def has_pygments_range(v):
671 major, minor = v.split('.')[0:2]
672 major, minor = v.split('.')[0:2]
672 return getpygmentsversion() >= (int(major), int(minor))
673 return getpygmentsversion() >= (int(major), int(minor))
673
674
674
675
675 @check("outer-repo", "outer repo")
676 @check("outer-repo", "outer repo")
676 def has_outer_repo():
677 def has_outer_repo():
677 # failing for other reasons than 'no repo' imply that there is a repo
678 # failing for other reasons than 'no repo' imply that there is a repo
678 return not matchoutput('hg root 2>&1', br'abort: no repository found', True)
679 return not matchoutput('hg root 2>&1', br'abort: no repository found', True)
679
680
680
681
681 @check("ssl", "ssl module available")
682 @check("ssl", "ssl module available")
682 def has_ssl():
683 def has_ssl():
683 try:
684 try:
684 import ssl
685 import ssl
685
686
686 ssl.CERT_NONE
687 ssl.CERT_NONE
687 return True
688 return True
688 except ImportError:
689 except ImportError:
689 return False
690 return False
690
691
691
692
692 @check("defaultcacertsloaded", "detected presence of loaded system CA certs")
693 @check("defaultcacertsloaded", "detected presence of loaded system CA certs")
693 def has_defaultcacertsloaded():
694 def has_defaultcacertsloaded():
694 import ssl
695 import ssl
695 from mercurial import sslutil, ui as uimod
696 from mercurial import sslutil, ui as uimod
696
697
697 ui = uimod.ui.load()
698 ui = uimod.ui.load()
698 cafile = sslutil._defaultcacerts(ui)
699 cafile = sslutil._defaultcacerts(ui)
699 ctx = ssl.create_default_context()
700 ctx = ssl.create_default_context()
700 if cafile:
701 if cafile:
701 ctx.load_verify_locations(cafile=cafile)
702 ctx.load_verify_locations(cafile=cafile)
702 else:
703 else:
703 ctx.load_default_certs()
704 ctx.load_default_certs()
704
705
705 return len(ctx.get_ca_certs()) > 0
706 return len(ctx.get_ca_certs()) > 0
706
707
707
708
708 @check("tls1.2", "TLS 1.2 protocol support")
709 @check("tls1.2", "TLS 1.2 protocol support")
709 def has_tls1_2():
710 def has_tls1_2():
710 from mercurial import sslutil
711 from mercurial import sslutil
711
712
712 return b'tls1.2' in sslutil.supportedprotocols
713 return b'tls1.2' in sslutil.supportedprotocols
713
714
714
715
715 @check("windows", "Windows")
716 @check("windows", "Windows")
716 def has_windows():
717 def has_windows():
717 return os.name == 'nt'
718 return os.name == 'nt'
718
719
719
720
720 @check("system-sh", "system() uses sh")
721 @check("system-sh", "system() uses sh")
721 def has_system_sh():
722 def has_system_sh():
722 return os.name != 'nt'
723 return os.name != 'nt'
723
724
724
725
725 @check("serve", "platform and python can manage 'hg serve -d'")
726 @check("serve", "platform and python can manage 'hg serve -d'")
726 def has_serve():
727 def has_serve():
727 return True
728 return True
728
729
729
730
730 @check("setprocname", "whether osutil.setprocname is available or not")
731 @check("setprocname", "whether osutil.setprocname is available or not")
731 def has_setprocname():
732 def has_setprocname():
732 try:
733 try:
733 from mercurial.utils import procutil
734 from mercurial.utils import procutil
734
735
735 procutil.setprocname
736 procutil.setprocname
736 return True
737 return True
737 except AttributeError:
738 except AttributeError:
738 return False
739 return False
739
740
740
741
741 @check("gui", "whether a gui environment is available or not")
742 @check("gui", "whether a gui environment is available or not")
742 def has_gui():
743 def has_gui():
743 from mercurial.utils import procutil
744 from mercurial.utils import procutil
744
745
745 return procutil.gui()
746 return procutil.gui()
746
747
747
748
748 @check("test-repo", "running tests from repository")
749 @check("test-repo", "running tests from repository")
749 def has_test_repo():
750 def has_test_repo():
750 t = os.environ["TESTDIR"]
751 t = os.environ["TESTDIR"]
751 return os.path.isdir(os.path.join(t, "..", ".hg"))
752 return os.path.isdir(os.path.join(t, "..", ".hg"))
752
753
753
754
754 @check("network-io", "whether tests are allowed to access 3rd party services")
755 @check("network-io", "whether tests are allowed to access 3rd party services")
755 def has_network_io():
756 def has_network_io():
756 t = os.environ.get("HGTESTS_ALLOW_NETIO")
757 t = os.environ.get("HGTESTS_ALLOW_NETIO")
757 return t == "1"
758 return t == "1"
758
759
759
760
760 @check("curses", "terminfo compiler and curses module")
761 @check("curses", "terminfo compiler and curses module")
761 def has_curses():
762 def has_curses():
762 try:
763 try:
763 import curses
764 import curses
764
765
765 curses.COLOR_BLUE
766 curses.COLOR_BLUE
766
767
767 # Windows doesn't have a `tic` executable, but the windows_curses
768 # Windows doesn't have a `tic` executable, but the windows_curses
768 # package is sufficient to run the tests without it.
769 # package is sufficient to run the tests without it.
769 if os.name == 'nt':
770 if os.name == 'nt':
770 return True
771 return True
771
772
772 return has_tic()
773 return has_tic()
773
774
774 except (ImportError, AttributeError):
775 except (ImportError, AttributeError):
775 return False
776 return False
776
777
777
778
778 @check("tic", "terminfo compiler")
779 @check("tic", "terminfo compiler")
779 def has_tic():
780 def has_tic():
780 return matchoutput('test -x "`which tic`"', br'')
781 return matchoutput('test -x "`which tic`"', br'')
781
782
782
783
783 @check("xz", "xz compression utility")
784 @check("xz", "xz compression utility")
784 def has_xz():
785 def has_xz():
785 # When Windows invokes a subprocess in shell mode, it uses `cmd.exe`, which
786 # When Windows invokes a subprocess in shell mode, it uses `cmd.exe`, which
786 # only knows `where`, not `which`. So invoke MSYS shell explicitly.
787 # only knows `where`, not `which`. So invoke MSYS shell explicitly.
787 return matchoutput("sh -c 'test -x \"`which xz`\"'", b'')
788 return matchoutput("sh -c 'test -x \"`which xz`\"'", b'')
788
789
789
790
790 @check("msys", "Windows with MSYS")
791 @check("msys", "Windows with MSYS")
791 def has_msys():
792 def has_msys():
792 return os.getenv('MSYSTEM')
793 return os.getenv('MSYSTEM')
793
794
794
795
795 @check("aix", "AIX")
796 @check("aix", "AIX")
796 def has_aix():
797 def has_aix():
797 return sys.platform.startswith("aix")
798 return sys.platform.startswith("aix")
798
799
799
800
800 @check("osx", "OS X")
801 @check("osx", "OS X")
801 def has_osx():
802 def has_osx():
802 return sys.platform == 'darwin'
803 return sys.platform == 'darwin'
803
804
804
805
805 @check("osxpackaging", "OS X packaging tools")
806 @check("osxpackaging", "OS X packaging tools")
806 def has_osxpackaging():
807 def has_osxpackaging():
807 try:
808 try:
808 return (
809 return (
809 matchoutput('pkgbuild', br'Usage: pkgbuild ', ignorestatus=1)
810 matchoutput('pkgbuild', br'Usage: pkgbuild ', ignorestatus=1)
810 and matchoutput(
811 and matchoutput(
811 'productbuild', br'Usage: productbuild ', ignorestatus=1
812 'productbuild', br'Usage: productbuild ', ignorestatus=1
812 )
813 )
813 and matchoutput('lsbom', br'Usage: lsbom', ignorestatus=1)
814 and matchoutput('lsbom', br'Usage: lsbom', ignorestatus=1)
814 and matchoutput('xar --help', br'Usage: xar', ignorestatus=1)
815 and matchoutput('xar --help', br'Usage: xar', ignorestatus=1)
815 )
816 )
816 except ImportError:
817 except ImportError:
817 return False
818 return False
818
819
819
820
820 @check('linuxormacos', 'Linux or MacOS')
821 @check('linuxormacos', 'Linux or MacOS')
821 def has_linuxormacos():
822 def has_linuxormacos():
822 # This isn't a perfect test for MacOS. But it is sufficient for our needs.
823 # This isn't a perfect test for MacOS. But it is sufficient for our needs.
823 return sys.platform.startswith(('linux', 'darwin'))
824 return sys.platform.startswith(('linux', 'darwin'))
824
825
825
826
826 @check("docker", "docker support")
827 @check("docker", "docker support")
827 def has_docker():
828 def has_docker():
828 pat = br'A self-sufficient runtime for'
829 pat = br'A self-sufficient runtime for'
829 if matchoutput('docker --help', pat):
830 if matchoutput('docker --help', pat):
830 if 'linux' not in sys.platform:
831 if 'linux' not in sys.platform:
831 # TODO: in theory we should be able to test docker-based
832 # TODO: in theory we should be able to test docker-based
832 # package creation on non-linux using boot2docker, but in
833 # package creation on non-linux using boot2docker, but in
833 # practice that requires extra coordination to make sure
834 # practice that requires extra coordination to make sure
834 # $TESTTEMP is going to be visible at the same path to the
835 # $TESTTEMP is going to be visible at the same path to the
835 # boot2docker VM. If we figure out how to verify that, we
836 # boot2docker VM. If we figure out how to verify that, we
836 # can use the following instead of just saying False:
837 # can use the following instead of just saying False:
837 # return 'DOCKER_HOST' in os.environ
838 # return 'DOCKER_HOST' in os.environ
838 return False
839 return False
839
840
840 return True
841 return True
841 return False
842 return False
842
843
843
844
844 @check("debhelper", "debian packaging tools")
845 @check("debhelper", "debian packaging tools")
845 def has_debhelper():
846 def has_debhelper():
846 # Some versions of dpkg say `dpkg', some say 'dpkg' (` vs ' on the first
847 # Some versions of dpkg say `dpkg', some say 'dpkg' (` vs ' on the first
847 # quote), so just accept anything in that spot.
848 # quote), so just accept anything in that spot.
848 dpkg = matchoutput(
849 dpkg = matchoutput(
849 'dpkg --version', br"Debian .dpkg' package management program"
850 'dpkg --version', br"Debian .dpkg' package management program"
850 )
851 )
851 dh = matchoutput(
852 dh = matchoutput(
852 'dh --help', br'dh is a part of debhelper.', ignorestatus=True
853 'dh --help', br'dh is a part of debhelper.', ignorestatus=True
853 )
854 )
854 dh_py2 = matchoutput(
855 dh_py2 = matchoutput(
855 'dh_python2 --help', br'other supported Python versions'
856 'dh_python2 --help', br'other supported Python versions'
856 )
857 )
857 # debuild comes from the 'devscripts' package, though you might want
858 # debuild comes from the 'devscripts' package, though you might want
858 # the 'build-debs' package instead, which has a dependency on devscripts.
859 # the 'build-debs' package instead, which has a dependency on devscripts.
859 debuild = matchoutput(
860 debuild = matchoutput(
860 'debuild --help', br'to run debian/rules with given parameter'
861 'debuild --help', br'to run debian/rules with given parameter'
861 )
862 )
862 return dpkg and dh and dh_py2 and debuild
863 return dpkg and dh and dh_py2 and debuild
863
864
864
865
865 @check(
866 @check(
866 "debdeps", "debian build dependencies (run dpkg-checkbuilddeps in contrib/)"
867 "debdeps", "debian build dependencies (run dpkg-checkbuilddeps in contrib/)"
867 )
868 )
868 def has_debdeps():
869 def has_debdeps():
869 # just check exit status (ignoring output)
870 # just check exit status (ignoring output)
870 path = '%s/../contrib/packaging/debian/control' % os.environ['TESTDIR']
871 path = '%s/../contrib/packaging/debian/control' % os.environ['TESTDIR']
871 return matchoutput('dpkg-checkbuilddeps %s' % path, br'')
872 return matchoutput('dpkg-checkbuilddeps %s' % path, br'')
872
873
873
874
874 @check("demandimport", "demandimport enabled")
875 @check("demandimport", "demandimport enabled")
875 def has_demandimport():
876 def has_demandimport():
876 # chg disables demandimport intentionally for performance wins.
877 # chg disables demandimport intentionally for performance wins.
877 return (not has_chg()) and os.environ.get('HGDEMANDIMPORT') != 'disable'
878 return (not has_chg()) and os.environ.get('HGDEMANDIMPORT') != 'disable'
878
879
879
880
880 # Add "py36", "py37", ... as possible feature checks. Note that there's no
881 # Add "py36", "py37", ... as possible feature checks. Note that there's no
881 # punctuation here.
882 # punctuation here.
882 @checkvers(
883 @checkvers(
883 "py",
884 "py",
884 "Python >= %s",
885 "Python >= %s",
885 ('3.6', '3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'),
886 ('3.6', '3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'),
886 )
887 )
887 def has_python_range(v):
888 def has_python_range(v):
888 major, minor = v.split('.')[0:2]
889 major, minor = v.split('.')[0:2]
889 py_major, py_minor = sys.version_info.major, sys.version_info.minor
890 py_major, py_minor = sys.version_info.major, sys.version_info.minor
890
891
891 return (py_major, py_minor) >= (int(major), int(minor))
892 return (py_major, py_minor) >= (int(major), int(minor))
892
893
893
894
894 @check("py3", "running with Python 3.x")
895 @check("py3", "running with Python 3.x")
895 def has_py3():
896 def has_py3():
896 return 3 == sys.version_info[0]
897 return 3 == sys.version_info[0]
897
898
898
899
899 @check("py3exe", "a Python 3.x interpreter is available")
900 @check("py3exe", "a Python 3.x interpreter is available")
900 def has_python3exe():
901 def has_python3exe():
901 py = 'python3'
902 py = 'python3'
902 if os.name == 'nt':
903 if os.name == 'nt':
903 py = 'py -3'
904 py = 'py -3'
904 return matchoutput('%s -V' % py, br'^Python 3.(6|7|8|9|10|11)')
905 return matchoutput('%s -V' % py, br'^Python 3.(6|7|8|9|10|11)')
905
906
906
907
907 @check("pure", "running with pure Python code")
908 @check("pure", "running with pure Python code")
908 def has_pure():
909 def has_pure():
909 return any(
910 return any(
910 [
911 [
911 os.environ.get("HGMODULEPOLICY") == "py",
912 os.environ.get("HGMODULEPOLICY") == "py",
912 os.environ.get("HGTEST_RUN_TESTS_PURE") == "--pure",
913 os.environ.get("HGTEST_RUN_TESTS_PURE") == "--pure",
913 ]
914 ]
914 )
915 )
915
916
916
917
917 @check("slow", "allow slow tests (use --allow-slow-tests)")
918 @check("slow", "allow slow tests (use --allow-slow-tests)")
918 def has_slow():
919 def has_slow():
919 return os.environ.get('HGTEST_SLOW') == 'slow'
920 return os.environ.get('HGTEST_SLOW') == 'slow'
920
921
921
922
922 @check("hypothesis", "Hypothesis automated test generation")
923 @check("hypothesis", "Hypothesis automated test generation")
923 def has_hypothesis():
924 def has_hypothesis():
924 try:
925 try:
925 import hypothesis
926 import hypothesis
926
927
927 hypothesis.given
928 hypothesis.given
928 return True
929 return True
929 except ImportError:
930 except ImportError:
930 return False
931 return False
931
932
932
933
933 @check("unziplinks", "unzip(1) understands and extracts symlinks")
934 @check("unziplinks", "unzip(1) understands and extracts symlinks")
934 def unzip_understands_symlinks():
935 def unzip_understands_symlinks():
935 return matchoutput('unzip --help', br'Info-ZIP')
936 return matchoutput('unzip --help', br'Info-ZIP')
936
937
937
938
938 @check("zstd", "zstd Python module available")
939 @check("zstd", "zstd Python module available")
939 def has_zstd():
940 def has_zstd():
940 try:
941 try:
941 import mercurial.zstd
942 import mercurial.zstd
942
943
943 mercurial.zstd.__version__
944 mercurial.zstd.__version__
944 return True
945 return True
945 except ImportError:
946 except ImportError:
946 return False
947 return False
947
948
948
949
949 @check("devfull", "/dev/full special file")
950 @check("devfull", "/dev/full special file")
950 def has_dev_full():
951 def has_dev_full():
951 return os.path.exists('/dev/full')
952 return os.path.exists('/dev/full')
952
953
953
954
954 @check("ensurepip", "ensurepip module")
955 @check("ensurepip", "ensurepip module")
955 def has_ensurepip():
956 def has_ensurepip():
956 try:
957 try:
957 import ensurepip
958 import ensurepip
958
959
959 ensurepip.bootstrap
960 ensurepip.bootstrap
960 return True
961 return True
961 except ImportError:
962 except ImportError:
962 return False
963 return False
963
964
964
965
965 @check("virtualenv", "virtualenv support")
966 @check("virtualenv", "virtualenv support")
966 def has_virtualenv():
967 def has_virtualenv():
967 try:
968 try:
968 import virtualenv
969 import virtualenv
969
970
970 # --no-site-package became the default in 1.7 (Nov 2011), and the
971 # --no-site-package became the default in 1.7 (Nov 2011), and the
971 # argument was removed in 20.0 (Feb 2020). Rather than make the
972 # argument was removed in 20.0 (Feb 2020). Rather than make the
972 # script complicated, just ignore ancient versions.
973 # script complicated, just ignore ancient versions.
973 return int(virtualenv.__version__.split('.')[0]) > 1
974 return int(virtualenv.__version__.split('.')[0]) > 1
974 except (AttributeError, ImportError, IndexError):
975 except (AttributeError, ImportError, IndexError):
975 return False
976 return False
976
977
977
978
978 @check("fsmonitor", "running tests with fsmonitor")
979 @check("fsmonitor", "running tests with fsmonitor")
979 def has_fsmonitor():
980 def has_fsmonitor():
980 return 'HGFSMONITOR_TESTS' in os.environ
981 return 'HGFSMONITOR_TESTS' in os.environ
981
982
982
983
983 @check("fuzzywuzzy", "Fuzzy string matching library")
984 @check("fuzzywuzzy", "Fuzzy string matching library")
984 def has_fuzzywuzzy():
985 def has_fuzzywuzzy():
985 try:
986 try:
986 import fuzzywuzzy
987 import fuzzywuzzy
987
988
988 fuzzywuzzy.__version__
989 fuzzywuzzy.__version__
989 return True
990 return True
990 except ImportError:
991 except ImportError:
991 return False
992 return False
992
993
993
994
994 @check("clang-libfuzzer", "clang new enough to include libfuzzer")
995 @check("clang-libfuzzer", "clang new enough to include libfuzzer")
995 def has_clang_libfuzzer():
996 def has_clang_libfuzzer():
996 mat = matchoutput('clang --version', br'clang version (\d)')
997 mat = matchoutput('clang --version', br'clang version (\d)')
997 if mat:
998 if mat:
998 # libfuzzer is new in clang 6
999 # libfuzzer is new in clang 6
999 return int(mat.group(1)) > 5
1000 return int(mat.group(1)) > 5
1000 return False
1001 return False
1001
1002
1002
1003
1003 @check("clang-6.0", "clang 6.0 with version suffix (libfuzzer included)")
1004 @check("clang-6.0", "clang 6.0 with version suffix (libfuzzer included)")
1004 def has_clang60():
1005 def has_clang60():
1005 return matchoutput('clang-6.0 --version', br'clang version 6\.')
1006 return matchoutput('clang-6.0 --version', br'clang version 6\.')
1006
1007
1007
1008
1008 @check("xdiff", "xdiff algorithm")
1009 @check("xdiff", "xdiff algorithm")
1009 def has_xdiff():
1010 def has_xdiff():
1010 try:
1011 try:
1011 from mercurial import policy
1012 from mercurial import policy
1012
1013
1013 bdiff = policy.importmod('bdiff')
1014 bdiff = policy.importmod('bdiff')
1014 return bdiff.xdiffblocks(b'', b'') == [(0, 0, 0, 0)]
1015 return bdiff.xdiffblocks(b'', b'') == [(0, 0, 0, 0)]
1015 except (ImportError, AttributeError):
1016 except (ImportError, AttributeError):
1016 return False
1017 return False
1017
1018
1018
1019
1019 @check('extraextensions', 'whether tests are running with extra extensions')
1020 @check('extraextensions', 'whether tests are running with extra extensions')
1020 def has_extraextensions():
1021 def has_extraextensions():
1021 return 'HGTESTEXTRAEXTENSIONS' in os.environ
1022 return 'HGTESTEXTRAEXTENSIONS' in os.environ
1022
1023
1023
1024
1024 def getrepofeatures():
1025 def getrepofeatures():
1025 """Obtain set of repository features in use.
1026 """Obtain set of repository features in use.
1026
1027
1027 HGREPOFEATURES can be used to define or remove features. It contains
1028 HGREPOFEATURES can be used to define or remove features. It contains
1028 a space-delimited list of feature strings. Strings beginning with ``-``
1029 a space-delimited list of feature strings. Strings beginning with ``-``
1029 mean to remove.
1030 mean to remove.
1030 """
1031 """
1031 # Default list provided by core.
1032 # Default list provided by core.
1032 features = {
1033 features = {
1033 'bundlerepo',
1034 'bundlerepo',
1034 'revlogstore',
1035 'revlogstore',
1035 'fncache',
1036 'fncache',
1036 }
1037 }
1037
1038
1038 # Features that imply other features.
1039 # Features that imply other features.
1039 implies = {
1040 implies = {
1040 'simplestore': ['-revlogstore', '-bundlerepo', '-fncache'],
1041 'simplestore': ['-revlogstore', '-bundlerepo', '-fncache'],
1041 }
1042 }
1042
1043
1043 for override in os.environ.get('HGREPOFEATURES', '').split(' '):
1044 for override in os.environ.get('HGREPOFEATURES', '').split(' '):
1044 if not override:
1045 if not override:
1045 continue
1046 continue
1046
1047
1047 if override.startswith('-'):
1048 if override.startswith('-'):
1048 if override[1:] in features:
1049 if override[1:] in features:
1049 features.remove(override[1:])
1050 features.remove(override[1:])
1050 else:
1051 else:
1051 features.add(override)
1052 features.add(override)
1052
1053
1053 for imply in implies.get(override, []):
1054 for imply in implies.get(override, []):
1054 if imply.startswith('-'):
1055 if imply.startswith('-'):
1055 if imply[1:] in features:
1056 if imply[1:] in features:
1056 features.remove(imply[1:])
1057 features.remove(imply[1:])
1057 else:
1058 else:
1058 features.add(imply)
1059 features.add(imply)
1059
1060
1060 return features
1061 return features
1061
1062
1062
1063
1063 @check('reporevlogstore', 'repository using the default revlog store')
1064 @check('reporevlogstore', 'repository using the default revlog store')
1064 def has_reporevlogstore():
1065 def has_reporevlogstore():
1065 return 'revlogstore' in getrepofeatures()
1066 return 'revlogstore' in getrepofeatures()
1066
1067
1067
1068
1068 @check('reposimplestore', 'repository using simple storage extension')
1069 @check('reposimplestore', 'repository using simple storage extension')
1069 def has_reposimplestore():
1070 def has_reposimplestore():
1070 return 'simplestore' in getrepofeatures()
1071 return 'simplestore' in getrepofeatures()
1071
1072
1072
1073
1073 @check('repobundlerepo', 'whether we can open bundle files as repos')
1074 @check('repobundlerepo', 'whether we can open bundle files as repos')
1074 def has_repobundlerepo():
1075 def has_repobundlerepo():
1075 return 'bundlerepo' in getrepofeatures()
1076 return 'bundlerepo' in getrepofeatures()
1076
1077
1077
1078
1078 @check('repofncache', 'repository has an fncache')
1079 @check('repofncache', 'repository has an fncache')
1079 def has_repofncache():
1080 def has_repofncache():
1080 return 'fncache' in getrepofeatures()
1081 return 'fncache' in getrepofeatures()
1081
1082
1082
1083
1083 @check('dirstate-v2', 'using the v2 format of .hg/dirstate')
1084 @check('dirstate-v2', 'using the v2 format of .hg/dirstate')
1084 def has_dirstate_v2():
1085 def has_dirstate_v2():
1085 # Keep this logic in sync with `newreporequirements()` in `mercurial/localrepo.py`
1086 # Keep this logic in sync with `newreporequirements()` in `mercurial/localrepo.py`
1086 return matchoutput(
1087 return matchoutput(
1087 'hg config format.use-dirstate-v2', b'(?i)1|yes|true|on|always'
1088 'hg config format.use-dirstate-v2', b'(?i)1|yes|true|on|always'
1088 )
1089 )
1089
1090
1090
1091
1091 @check('sqlite', 'sqlite3 module and matching cli is available')
1092 @check('sqlite', 'sqlite3 module and matching cli is available')
1092 def has_sqlite():
1093 def has_sqlite():
1093 try:
1094 try:
1094 import sqlite3
1095 import sqlite3
1095
1096
1096 version = sqlite3.sqlite_version_info
1097 version = sqlite3.sqlite_version_info
1097 except ImportError:
1098 except ImportError:
1098 return False
1099 return False
1099
1100
1100 if version < (3, 8, 3):
1101 if version < (3, 8, 3):
1101 # WITH clause not supported
1102 # WITH clause not supported
1102 return False
1103 return False
1103
1104
1104 return matchoutput('sqlite3 -version', br'^3\.\d+')
1105 return matchoutput('sqlite3 -version', br'^3\.\d+')
1105
1106
1106
1107
1107 @check('vcr', 'vcr http mocking library (pytest-vcr)')
1108 @check('vcr', 'vcr http mocking library (pytest-vcr)')
1108 def has_vcr():
1109 def has_vcr():
1109 try:
1110 try:
1110 import vcr
1111 import vcr
1111
1112
1112 vcr.VCR
1113 vcr.VCR
1113 return True
1114 return True
1114 except (ImportError, AttributeError):
1115 except (ImportError, AttributeError):
1115 pass
1116 pass
1116 return False
1117 return False
1117
1118
1118
1119
1119 @check('emacs', 'GNU Emacs')
1120 @check('emacs', 'GNU Emacs')
1120 def has_emacs():
1121 def has_emacs():
1121 # Our emacs lisp uses `with-eval-after-load` which is new in emacs
1122 # Our emacs lisp uses `with-eval-after-load` which is new in emacs
1122 # 24.4, so we allow emacs 24.4, 24.5, and 25+ (24.5 was the last
1123 # 24.4, so we allow emacs 24.4, 24.5, and 25+ (24.5 was the last
1123 # 24 release)
1124 # 24 release)
1124 return matchoutput('emacs --version', b'GNU Emacs 2(4.4|4.5|5|6|7|8|9)')
1125 return matchoutput('emacs --version', b'GNU Emacs 2(4.4|4.5|5|6|7|8|9)')
1125
1126
1126
1127
1127 @check('black', 'the black formatter for python >=23.3.0')
1128 @check('black', 'the black formatter for python >=23.3.0')
1128 def has_black():
1129 def has_black():
1129 blackcmd = 'black --version'
1130 blackcmd = 'black --version'
1130 version_regex = b'black, (?:version )?([0-9a-b.]+)'
1131 version_regex = b'black, (?:version )?([0-9a-b.]+)'
1131 version = matchoutput(blackcmd, version_regex)
1132 version = matchoutput(blackcmd, version_regex)
1132 if not version:
1133 if not version:
1133 return False
1134 return False
1134 return Version(_bytes2sys(version.group(1))) >= Version('23.3.0')
1135 return Version(_bytes2sys(version.group(1))) >= Version('23.3.0')
1135
1136
1136
1137
1137 @check('pytype', 'the pytype type checker')
1138 @check('pytype', 'the pytype type checker')
1138 def has_pytype():
1139 def has_pytype():
1139 pytypecmd = 'pytype --version'
1140 pytypecmd = 'pytype --version'
1140 version = matchoutput(pytypecmd, b'[0-9a-b.]+')
1141 version = matchoutput(pytypecmd, b'[0-9a-b.]+')
1141 if not version:
1142 if not version:
1142 return False
1143 return False
1143 return Version(_bytes2sys(version.group(0))) >= Version('2019.10.17')
1144 return Version(_bytes2sys(version.group(0))) >= Version('2019.10.17')
1144
1145
1145
1146
1146 @check("rustfmt", "rustfmt tool at version nightly-2024-07-16")
1147 @check("rustfmt", "rustfmt tool at version nightly-2024-07-16")
1147 def has_rustfmt():
1148 def has_rustfmt():
1148 # We use Nightly's rustfmt due to current unstable config options.
1149 # We use Nightly's rustfmt due to current unstable config options.
1149 return matchoutput(
1150 return matchoutput(
1150 '`rustup which --toolchain nightly-2024-07-16 rustfmt` --version',
1151 '`rustup which --toolchain nightly-2024-07-16 rustfmt` --version',
1151 b'rustfmt',
1152 b'rustfmt',
1152 )
1153 )
1153
1154
1154
1155
1155 @check("cargo", "cargo tool")
1156 @check("cargo", "cargo tool")
1156 def has_cargo():
1157 def has_cargo():
1157 return matchoutput('`rustup which cargo` --version', b'cargo')
1158 return matchoutput('`rustup which cargo` --version', b'cargo')
1158
1159
1159
1160
1160 @check("lzma", "python lzma module")
1161 @check("lzma", "python lzma module")
1161 def has_lzma():
1162 def has_lzma():
1162 try:
1163 try:
1163 import _lzma
1164 import _lzma
1164
1165
1165 _lzma.FORMAT_XZ
1166 _lzma.FORMAT_XZ
1166 return True
1167 return True
1167 except ImportError:
1168 except ImportError:
1168 return False
1169 return False
1169
1170
1170
1171
1171 @check("bash", "bash shell")
1172 @check("bash", "bash shell")
1172 def has_bash():
1173 def has_bash():
1173 return matchoutput("bash -c 'echo hi'", b'^hi$')
1174 return matchoutput("bash -c 'echo hi'", b'^hi$')
1174
1175
1175
1176
1176 @check("bigendian", "big-endian CPU")
1177 @check("bigendian", "big-endian CPU")
1177 def has_bigendian():
1178 def has_bigendian():
1178 return sys.byteorder == 'big'
1179 return sys.byteorder == 'big'
General Comments 0
You need to be logged in to leave comments. Login now