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