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