##// END OF EJS Templates
sslutil: move code examining _canloaddefaultcerts out of _defaultcacerts...
Gregory Szorc -
r29107:c8fbfb91 default
parent child Browse files
Show More
@@ -1,335 +1,338 b''
1 # sslutil.py - SSL handling for mercurial
1 # sslutil.py - SSL handling for mercurial
2 #
2 #
3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 from __future__ import absolute_import
10 from __future__ import absolute_import
11
11
12 import os
12 import os
13 import ssl
13 import ssl
14 import sys
14 import sys
15
15
16 from .i18n import _
16 from .i18n import _
17 from . import (
17 from . import (
18 error,
18 error,
19 util,
19 util,
20 )
20 )
21
21
22 # Python 2.7.9+ overhauled the built-in SSL/TLS features of Python. It added
22 # Python 2.7.9+ overhauled the built-in SSL/TLS features of Python. It added
23 # support for TLS 1.1, TLS 1.2, SNI, system CA stores, etc. These features are
23 # support for TLS 1.1, TLS 1.2, SNI, system CA stores, etc. These features are
24 # all exposed via the "ssl" module.
24 # all exposed via the "ssl" module.
25 #
25 #
26 # Depending on the version of Python being used, SSL/TLS support is either
26 # Depending on the version of Python being used, SSL/TLS support is either
27 # modern/secure or legacy/insecure. Many operations in this module have
27 # modern/secure or legacy/insecure. Many operations in this module have
28 # separate code paths depending on support in Python.
28 # separate code paths depending on support in Python.
29
29
30 hassni = getattr(ssl, 'HAS_SNI', False)
30 hassni = getattr(ssl, 'HAS_SNI', False)
31
31
32 try:
32 try:
33 OP_NO_SSLv2 = ssl.OP_NO_SSLv2
33 OP_NO_SSLv2 = ssl.OP_NO_SSLv2
34 OP_NO_SSLv3 = ssl.OP_NO_SSLv3
34 OP_NO_SSLv3 = ssl.OP_NO_SSLv3
35 except AttributeError:
35 except AttributeError:
36 OP_NO_SSLv2 = 0x1000000
36 OP_NO_SSLv2 = 0x1000000
37 OP_NO_SSLv3 = 0x2000000
37 OP_NO_SSLv3 = 0x2000000
38
38
39 try:
39 try:
40 # ssl.SSLContext was added in 2.7.9 and presence indicates modern
40 # ssl.SSLContext was added in 2.7.9 and presence indicates modern
41 # SSL/TLS features are available.
41 # SSL/TLS features are available.
42 SSLContext = ssl.SSLContext
42 SSLContext = ssl.SSLContext
43 modernssl = True
43 modernssl = True
44 _canloaddefaultcerts = util.safehasattr(SSLContext, 'load_default_certs')
44 _canloaddefaultcerts = util.safehasattr(SSLContext, 'load_default_certs')
45 except AttributeError:
45 except AttributeError:
46 modernssl = False
46 modernssl = False
47 _canloaddefaultcerts = False
47 _canloaddefaultcerts = False
48
48
49 # We implement SSLContext using the interface from the standard library.
49 # We implement SSLContext using the interface from the standard library.
50 class SSLContext(object):
50 class SSLContext(object):
51 # ssl.wrap_socket gained the "ciphers" named argument in 2.7.
51 # ssl.wrap_socket gained the "ciphers" named argument in 2.7.
52 _supportsciphers = sys.version_info >= (2, 7)
52 _supportsciphers = sys.version_info >= (2, 7)
53
53
54 def __init__(self, protocol):
54 def __init__(self, protocol):
55 # From the public interface of SSLContext
55 # From the public interface of SSLContext
56 self.protocol = protocol
56 self.protocol = protocol
57 self.check_hostname = False
57 self.check_hostname = False
58 self.options = 0
58 self.options = 0
59 self.verify_mode = ssl.CERT_NONE
59 self.verify_mode = ssl.CERT_NONE
60
60
61 # Used by our implementation.
61 # Used by our implementation.
62 self._certfile = None
62 self._certfile = None
63 self._keyfile = None
63 self._keyfile = None
64 self._certpassword = None
64 self._certpassword = None
65 self._cacerts = None
65 self._cacerts = None
66 self._ciphers = None
66 self._ciphers = None
67
67
68 def load_cert_chain(self, certfile, keyfile=None, password=None):
68 def load_cert_chain(self, certfile, keyfile=None, password=None):
69 self._certfile = certfile
69 self._certfile = certfile
70 self._keyfile = keyfile
70 self._keyfile = keyfile
71 self._certpassword = password
71 self._certpassword = password
72
72
73 def load_default_certs(self, purpose=None):
73 def load_default_certs(self, purpose=None):
74 pass
74 pass
75
75
76 def load_verify_locations(self, cafile=None, capath=None, cadata=None):
76 def load_verify_locations(self, cafile=None, capath=None, cadata=None):
77 if capath:
77 if capath:
78 raise error.Abort('capath not supported')
78 raise error.Abort('capath not supported')
79 if cadata:
79 if cadata:
80 raise error.Abort('cadata not supported')
80 raise error.Abort('cadata not supported')
81
81
82 self._cacerts = cafile
82 self._cacerts = cafile
83
83
84 def set_ciphers(self, ciphers):
84 def set_ciphers(self, ciphers):
85 if not self._supportsciphers:
85 if not self._supportsciphers:
86 raise error.Abort('setting ciphers not supported')
86 raise error.Abort('setting ciphers not supported')
87
87
88 self._ciphers = ciphers
88 self._ciphers = ciphers
89
89
90 def wrap_socket(self, socket, server_hostname=None, server_side=False):
90 def wrap_socket(self, socket, server_hostname=None, server_side=False):
91 # server_hostname is unique to SSLContext.wrap_socket and is used
91 # server_hostname is unique to SSLContext.wrap_socket and is used
92 # for SNI in that context. So there's nothing for us to do with it
92 # for SNI in that context. So there's nothing for us to do with it
93 # in this legacy code since we don't support SNI.
93 # in this legacy code since we don't support SNI.
94
94
95 args = {
95 args = {
96 'keyfile': self._keyfile,
96 'keyfile': self._keyfile,
97 'certfile': self._certfile,
97 'certfile': self._certfile,
98 'server_side': server_side,
98 'server_side': server_side,
99 'cert_reqs': self.verify_mode,
99 'cert_reqs': self.verify_mode,
100 'ssl_version': self.protocol,
100 'ssl_version': self.protocol,
101 'ca_certs': self._cacerts,
101 'ca_certs': self._cacerts,
102 }
102 }
103
103
104 if self._supportsciphers:
104 if self._supportsciphers:
105 args['ciphers'] = self._ciphers
105 args['ciphers'] = self._ciphers
106
106
107 return ssl.wrap_socket(socket, **args)
107 return ssl.wrap_socket(socket, **args)
108
108
109 def wrapsocket(sock, keyfile, certfile, ui, cert_reqs=ssl.CERT_NONE,
109 def wrapsocket(sock, keyfile, certfile, ui, cert_reqs=ssl.CERT_NONE,
110 ca_certs=None, serverhostname=None):
110 ca_certs=None, serverhostname=None):
111 """Add SSL/TLS to a socket.
111 """Add SSL/TLS to a socket.
112
112
113 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
113 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
114 choices based on what security options are available.
114 choices based on what security options are available.
115
115
116 In addition to the arguments supported by ``ssl.wrap_socket``, we allow
116 In addition to the arguments supported by ``ssl.wrap_socket``, we allow
117 the following additional arguments:
117 the following additional arguments:
118
118
119 * serverhostname - The expected hostname of the remote server. If the
119 * serverhostname - The expected hostname of the remote server. If the
120 server (and client) support SNI, this tells the server which certificate
120 server (and client) support SNI, this tells the server which certificate
121 to use.
121 to use.
122 """
122 """
123 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol
123 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol
124 # that both ends support, including TLS protocols. On legacy stacks,
124 # that both ends support, including TLS protocols. On legacy stacks,
125 # the highest it likely goes in TLS 1.0. On modern stacks, it can
125 # the highest it likely goes in TLS 1.0. On modern stacks, it can
126 # support TLS 1.2.
126 # support TLS 1.2.
127 #
127 #
128 # The PROTOCOL_TLSv* constants select a specific TLS version
128 # The PROTOCOL_TLSv* constants select a specific TLS version
129 # only (as opposed to multiple versions). So the method for
129 # only (as opposed to multiple versions). So the method for
130 # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and
130 # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and
131 # disable protocols via SSLContext.options and OP_NO_* constants.
131 # disable protocols via SSLContext.options and OP_NO_* constants.
132 # However, SSLContext.options doesn't work unless we have the
132 # However, SSLContext.options doesn't work unless we have the
133 # full/real SSLContext available to us.
133 # full/real SSLContext available to us.
134 #
134 #
135 # SSLv2 and SSLv3 are broken. We ban them outright.
135 # SSLv2 and SSLv3 are broken. We ban them outright.
136 if modernssl:
136 if modernssl:
137 protocol = ssl.PROTOCOL_SSLv23
137 protocol = ssl.PROTOCOL_SSLv23
138 else:
138 else:
139 protocol = ssl.PROTOCOL_TLSv1
139 protocol = ssl.PROTOCOL_TLSv1
140
140
141 # TODO use ssl.create_default_context() on modernssl.
141 # TODO use ssl.create_default_context() on modernssl.
142 sslcontext = SSLContext(protocol)
142 sslcontext = SSLContext(protocol)
143
143
144 # This is a no-op on old Python.
144 # This is a no-op on old Python.
145 sslcontext.options |= OP_NO_SSLv2 | OP_NO_SSLv3
145 sslcontext.options |= OP_NO_SSLv2 | OP_NO_SSLv3
146
146
147 # This still works on our fake SSLContext.
147 # This still works on our fake SSLContext.
148 sslcontext.verify_mode = cert_reqs
148 sslcontext.verify_mode = cert_reqs
149
149
150 if certfile is not None:
150 if certfile is not None:
151 def password():
151 def password():
152 f = keyfile or certfile
152 f = keyfile or certfile
153 return ui.getpass(_('passphrase for %s: ') % f, '')
153 return ui.getpass(_('passphrase for %s: ') % f, '')
154 sslcontext.load_cert_chain(certfile, keyfile, password)
154 sslcontext.load_cert_chain(certfile, keyfile, password)
155
155
156 if ca_certs is not None:
156 if ca_certs is not None:
157 sslcontext.load_verify_locations(cafile=ca_certs)
157 sslcontext.load_verify_locations(cafile=ca_certs)
158 else:
158 else:
159 # This is a no-op on old Python.
159 # This is a no-op on old Python.
160 sslcontext.load_default_certs()
160 sslcontext.load_default_certs()
161
161
162 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
162 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
163 # check if wrap_socket failed silently because socket had been
163 # check if wrap_socket failed silently because socket had been
164 # closed
164 # closed
165 # - see http://bugs.python.org/issue13721
165 # - see http://bugs.python.org/issue13721
166 if not sslsocket.cipher():
166 if not sslsocket.cipher():
167 raise error.Abort(_('ssl connection failed'))
167 raise error.Abort(_('ssl connection failed'))
168 return sslsocket
168 return sslsocket
169
169
170 def _verifycert(cert, hostname):
170 def _verifycert(cert, hostname):
171 '''Verify that cert (in socket.getpeercert() format) matches hostname.
171 '''Verify that cert (in socket.getpeercert() format) matches hostname.
172 CRLs is not handled.
172 CRLs is not handled.
173
173
174 Returns error message if any problems are found and None on success.
174 Returns error message if any problems are found and None on success.
175 '''
175 '''
176 if not cert:
176 if not cert:
177 return _('no certificate received')
177 return _('no certificate received')
178 dnsname = hostname.lower()
178 dnsname = hostname.lower()
179 def matchdnsname(certname):
179 def matchdnsname(certname):
180 return (certname == dnsname or
180 return (certname == dnsname or
181 '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1])
181 '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1])
182
182
183 san = cert.get('subjectAltName', [])
183 san = cert.get('subjectAltName', [])
184 if san:
184 if san:
185 certnames = [value.lower() for key, value in san if key == 'DNS']
185 certnames = [value.lower() for key, value in san if key == 'DNS']
186 for name in certnames:
186 for name in certnames:
187 if matchdnsname(name):
187 if matchdnsname(name):
188 return None
188 return None
189 if certnames:
189 if certnames:
190 return _('certificate is for %s') % ', '.join(certnames)
190 return _('certificate is for %s') % ', '.join(certnames)
191
191
192 # subject is only checked when subjectAltName is empty
192 # subject is only checked when subjectAltName is empty
193 for s in cert.get('subject', []):
193 for s in cert.get('subject', []):
194 key, value = s[0]
194 key, value = s[0]
195 if key == 'commonName':
195 if key == 'commonName':
196 try:
196 try:
197 # 'subject' entries are unicode
197 # 'subject' entries are unicode
198 certname = value.lower().encode('ascii')
198 certname = value.lower().encode('ascii')
199 except UnicodeEncodeError:
199 except UnicodeEncodeError:
200 return _('IDN in certificate not supported')
200 return _('IDN in certificate not supported')
201 if matchdnsname(certname):
201 if matchdnsname(certname):
202 return None
202 return None
203 return _('certificate is for %s') % certname
203 return _('certificate is for %s') % certname
204 return _('no commonName or subjectAltName found in certificate')
204 return _('no commonName or subjectAltName found in certificate')
205
205
206
206
207 # CERT_REQUIRED means fetch the cert from the server all the time AND
207 # CERT_REQUIRED means fetch the cert from the server all the time AND
208 # validate it against the CA store provided in web.cacerts.
208 # validate it against the CA store provided in web.cacerts.
209
209
210 def _plainapplepython():
210 def _plainapplepython():
211 """return true if this seems to be a pure Apple Python that
211 """return true if this seems to be a pure Apple Python that
212 * is unfrozen and presumably has the whole mercurial module in the file
212 * is unfrozen and presumably has the whole mercurial module in the file
213 system
213 system
214 * presumably is an Apple Python that uses Apple OpenSSL which has patches
214 * presumably is an Apple Python that uses Apple OpenSSL which has patches
215 for using system certificate store CAs in addition to the provided
215 for using system certificate store CAs in addition to the provided
216 cacerts file
216 cacerts file
217 """
217 """
218 if sys.platform != 'darwin' or util.mainfrozen() or not sys.executable:
218 if sys.platform != 'darwin' or util.mainfrozen() or not sys.executable:
219 return False
219 return False
220 exe = os.path.realpath(sys.executable).lower()
220 exe = os.path.realpath(sys.executable).lower()
221 return (exe.startswith('/usr/bin/python') or
221 return (exe.startswith('/usr/bin/python') or
222 exe.startswith('/system/library/frameworks/python.framework/'))
222 exe.startswith('/system/library/frameworks/python.framework/'))
223
223
224 def _defaultcacerts():
224 def _defaultcacerts():
225 """return path to CA certificates; None for system's store; ! to disable"""
225 """return path to default CA certificates or None."""
226 if _plainapplepython():
226 if _plainapplepython():
227 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
227 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
228 if os.path.exists(dummycert):
228 if os.path.exists(dummycert):
229 return dummycert
229 return dummycert
230 if _canloaddefaultcerts:
230
231 return None
231 return None
232 return '!'
233
232
234 def sslkwargs(ui, host):
233 def sslkwargs(ui, host):
235 """Determine arguments to pass to wrapsocket().
234 """Determine arguments to pass to wrapsocket().
236
235
237 ``host`` is the hostname being connected to.
236 ``host`` is the hostname being connected to.
238 """
237 """
239 kws = {'ui': ui}
238 kws = {'ui': ui}
240
239
241 # If a host key fingerprint is on file, it is the only thing that matters
240 # If a host key fingerprint is on file, it is the only thing that matters
242 # and CA certs don't come into play.
241 # and CA certs don't come into play.
243 hostfingerprint = ui.config('hostfingerprints', host)
242 hostfingerprint = ui.config('hostfingerprints', host)
244 if hostfingerprint:
243 if hostfingerprint:
245 return kws
244 return kws
246
245
247 # dispatch sets web.cacerts=! when --insecure is used.
246 # dispatch sets web.cacerts=! when --insecure is used.
248 cacerts = ui.config('web', 'cacerts')
247 cacerts = ui.config('web', 'cacerts')
249 if cacerts == '!':
248 if cacerts == '!':
250 return kws
249 return kws
251
250
252 # If a value is set in the config, validate against a path and load
251 # If a value is set in the config, validate against a path and load
253 # and require those certs.
252 # and require those certs.
254 if cacerts:
253 if cacerts:
255 cacerts = util.expandpath(cacerts)
254 cacerts = util.expandpath(cacerts)
256 if not os.path.exists(cacerts):
255 if not os.path.exists(cacerts):
257 raise error.Abort(_('could not find web.cacerts: %s') % cacerts)
256 raise error.Abort(_('could not find web.cacerts: %s') % cacerts)
258
257
259 kws.update({'ca_certs': cacerts,
258 kws.update({'ca_certs': cacerts,
260 'cert_reqs': ssl.CERT_REQUIRED})
259 'cert_reqs': ssl.CERT_REQUIRED})
261 return kws
260 return kws
262
261
263 # No CAs in config. See if we can load defaults.
262 # No CAs in config. See if we can load defaults.
264 cacerts = _defaultcacerts()
263 cacerts = _defaultcacerts()
265 if cacerts and cacerts != '!':
264 if cacerts:
266 ui.debug('using %s to enable OS X system CA\n' % cacerts)
265 ui.debug('using %s to enable OS X system CA\n' % cacerts)
266 else:
267 if not _canloaddefaultcerts:
268 cacerts = '!'
269
267 ui.setconfig('web', 'cacerts', cacerts, 'defaultcacerts')
270 ui.setconfig('web', 'cacerts', cacerts, 'defaultcacerts')
268
271
269 if cacerts != '!':
272 if cacerts != '!':
270 kws.update({'ca_certs': cacerts,
273 kws.update({'ca_certs': cacerts,
271 'cert_reqs': ssl.CERT_REQUIRED,
274 'cert_reqs': ssl.CERT_REQUIRED,
272 })
275 })
273 return kws
276 return kws
274
277
275 class validator(object):
278 class validator(object):
276 def __init__(self, ui, host):
279 def __init__(self, ui, host):
277 self.ui = ui
280 self.ui = ui
278 self.host = host
281 self.host = host
279
282
280 def __call__(self, sock, strict=False):
283 def __call__(self, sock, strict=False):
281 host = self.host
284 host = self.host
282
285
283 if not sock.cipher(): # work around http://bugs.python.org/issue13721
286 if not sock.cipher(): # work around http://bugs.python.org/issue13721
284 raise error.Abort(_('%s ssl connection error') % host)
287 raise error.Abort(_('%s ssl connection error') % host)
285 try:
288 try:
286 peercert = sock.getpeercert(True)
289 peercert = sock.getpeercert(True)
287 peercert2 = sock.getpeercert()
290 peercert2 = sock.getpeercert()
288 except AttributeError:
291 except AttributeError:
289 raise error.Abort(_('%s ssl connection error') % host)
292 raise error.Abort(_('%s ssl connection error') % host)
290
293
291 if not peercert:
294 if not peercert:
292 raise error.Abort(_('%s certificate error: '
295 raise error.Abort(_('%s certificate error: '
293 'no certificate received') % host)
296 'no certificate received') % host)
294
297
295 # If a certificate fingerprint is pinned, use it and only it to
298 # If a certificate fingerprint is pinned, use it and only it to
296 # validate the remote cert.
299 # validate the remote cert.
297 hostfingerprints = self.ui.configlist('hostfingerprints', host)
300 hostfingerprints = self.ui.configlist('hostfingerprints', host)
298 peerfingerprint = util.sha1(peercert).hexdigest()
301 peerfingerprint = util.sha1(peercert).hexdigest()
299 nicefingerprint = ":".join([peerfingerprint[x:x + 2]
302 nicefingerprint = ":".join([peerfingerprint[x:x + 2]
300 for x in xrange(0, len(peerfingerprint), 2)])
303 for x in xrange(0, len(peerfingerprint), 2)])
301 if hostfingerprints:
304 if hostfingerprints:
302 fingerprintmatch = False
305 fingerprintmatch = False
303 for hostfingerprint in hostfingerprints:
306 for hostfingerprint in hostfingerprints:
304 if peerfingerprint.lower() == \
307 if peerfingerprint.lower() == \
305 hostfingerprint.replace(':', '').lower():
308 hostfingerprint.replace(':', '').lower():
306 fingerprintmatch = True
309 fingerprintmatch = True
307 break
310 break
308 if not fingerprintmatch:
311 if not fingerprintmatch:
309 raise error.Abort(_('certificate for %s has unexpected '
312 raise error.Abort(_('certificate for %s has unexpected '
310 'fingerprint %s') % (host, nicefingerprint),
313 'fingerprint %s') % (host, nicefingerprint),
311 hint=_('check hostfingerprint configuration'))
314 hint=_('check hostfingerprint configuration'))
312 self.ui.debug('%s certificate matched fingerprint %s\n' %
315 self.ui.debug('%s certificate matched fingerprint %s\n' %
313 (host, nicefingerprint))
316 (host, nicefingerprint))
314 return
317 return
315
318
316 # No pinned fingerprint. Establish trust by looking at the CAs.
319 # No pinned fingerprint. Establish trust by looking at the CAs.
317 cacerts = self.ui.config('web', 'cacerts')
320 cacerts = self.ui.config('web', 'cacerts')
318 if cacerts != '!':
321 if cacerts != '!':
319 msg = _verifycert(peercert2, host)
322 msg = _verifycert(peercert2, host)
320 if msg:
323 if msg:
321 raise error.Abort(_('%s certificate error: %s') % (host, msg),
324 raise error.Abort(_('%s certificate error: %s') % (host, msg),
322 hint=_('configure hostfingerprint %s or use '
325 hint=_('configure hostfingerprint %s or use '
323 '--insecure to connect insecurely') %
326 '--insecure to connect insecurely') %
324 nicefingerprint)
327 nicefingerprint)
325 self.ui.debug('%s certificate successfully verified\n' % host)
328 self.ui.debug('%s certificate successfully verified\n' % host)
326 elif strict:
329 elif strict:
327 raise error.Abort(_('%s certificate with fingerprint %s not '
330 raise error.Abort(_('%s certificate with fingerprint %s not '
328 'verified') % (host, nicefingerprint),
331 'verified') % (host, nicefingerprint),
329 hint=_('check hostfingerprints or web.cacerts '
332 hint=_('check hostfingerprints or web.cacerts '
330 'config setting'))
333 'config setting'))
331 else:
334 else:
332 self.ui.warn(_('warning: %s certificate with fingerprint %s not '
335 self.ui.warn(_('warning: %s certificate with fingerprint %s not '
333 'verified (check hostfingerprints or web.cacerts '
336 'verified (check hostfingerprints or web.cacerts '
334 'config setting)\n') %
337 'config setting)\n') %
335 (host, nicefingerprint))
338 (host, nicefingerprint))
@@ -1,531 +1,531 b''
1 from __future__ import absolute_import
1 from __future__ import absolute_import
2
2
3 import errno
3 import errno
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 }
17 }
18
18
19 def check(name, desc):
19 def check(name, desc):
20 """Registers a check function for a feature."""
20 """Registers a check function for a feature."""
21 def decorator(func):
21 def decorator(func):
22 checks[name] = (func, desc)
22 checks[name] = (func, desc)
23 return func
23 return func
24 return decorator
24 return decorator
25
25
26 def checkvers(name, desc, vers):
26 def checkvers(name, desc, vers):
27 """Registers a check function for each of a series of versions.
27 """Registers a check function for each of a series of versions.
28
28
29 vers can be a list or an iterator"""
29 vers can be a list or an iterator"""
30 def decorator(func):
30 def decorator(func):
31 def funcv(v):
31 def funcv(v):
32 def f():
32 def f():
33 return func(v)
33 return func(v)
34 return f
34 return f
35 for v in vers:
35 for v in vers:
36 v = str(v)
36 v = str(v)
37 f = funcv(v)
37 f = funcv(v)
38 checks['%s%s' % (name, v.replace('.', ''))] = (f, desc % v)
38 checks['%s%s' % (name, v.replace('.', ''))] = (f, desc % v)
39 return func
39 return func
40 return decorator
40 return decorator
41
41
42 def checkfeatures(features):
42 def checkfeatures(features):
43 result = {
43 result = {
44 'error': [],
44 'error': [],
45 'missing': [],
45 'missing': [],
46 'skipped': [],
46 'skipped': [],
47 }
47 }
48
48
49 for feature in features:
49 for feature in features:
50 negate = feature.startswith('no-')
50 negate = feature.startswith('no-')
51 if negate:
51 if negate:
52 feature = feature[3:]
52 feature = feature[3:]
53
53
54 if feature not in checks:
54 if feature not in checks:
55 result['missing'].append(feature)
55 result['missing'].append(feature)
56 continue
56 continue
57
57
58 check, desc = checks[feature]
58 check, desc = checks[feature]
59 try:
59 try:
60 available = check()
60 available = check()
61 except Exception:
61 except Exception:
62 result['error'].append('hghave check failed: %s' % feature)
62 result['error'].append('hghave check failed: %s' % feature)
63 continue
63 continue
64
64
65 if not negate and not available:
65 if not negate and not available:
66 result['skipped'].append('missing feature: %s' % desc)
66 result['skipped'].append('missing feature: %s' % desc)
67 elif negate and available:
67 elif negate and available:
68 result['skipped'].append('system supports %s' % desc)
68 result['skipped'].append('system supports %s' % desc)
69
69
70 return result
70 return result
71
71
72 def require(features):
72 def require(features):
73 """Require that features are available, exiting if not."""
73 """Require that features are available, exiting if not."""
74 result = checkfeatures(features)
74 result = checkfeatures(features)
75
75
76 for missing in result['missing']:
76 for missing in result['missing']:
77 sys.stderr.write('skipped: unknown feature: %s\n' % missing)
77 sys.stderr.write('skipped: unknown feature: %s\n' % missing)
78 for msg in result['skipped']:
78 for msg in result['skipped']:
79 sys.stderr.write('skipped: %s\n' % msg)
79 sys.stderr.write('skipped: %s\n' % msg)
80 for msg in result['error']:
80 for msg in result['error']:
81 sys.stderr.write('%s\n' % msg)
81 sys.stderr.write('%s\n' % msg)
82
82
83 if result['missing']:
83 if result['missing']:
84 sys.exit(2)
84 sys.exit(2)
85
85
86 if result['skipped'] or result['error']:
86 if result['skipped'] or result['error']:
87 sys.exit(1)
87 sys.exit(1)
88
88
89 def matchoutput(cmd, regexp, ignorestatus=False):
89 def matchoutput(cmd, regexp, ignorestatus=False):
90 """Return the match object if cmd executes successfully and its output
90 """Return the match object if cmd executes successfully and its output
91 is matched by the supplied regular expression.
91 is matched by the supplied regular expression.
92 """
92 """
93 r = re.compile(regexp)
93 r = re.compile(regexp)
94 try:
94 try:
95 p = subprocess.Popen(
95 p = subprocess.Popen(
96 cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
96 cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
97 except OSError as e:
97 except OSError as e:
98 if e.errno != errno.ENOENT:
98 if e.errno != errno.ENOENT:
99 raise
99 raise
100 ret = -1
100 ret = -1
101 ret = p.wait()
101 ret = p.wait()
102 s = p.stdout.read()
102 s = p.stdout.read()
103 return (ignorestatus or not ret) and r.search(s)
103 return (ignorestatus or not ret) and r.search(s)
104
104
105 @check("baz", "GNU Arch baz client")
105 @check("baz", "GNU Arch baz client")
106 def has_baz():
106 def has_baz():
107 return matchoutput('baz --version 2>&1', r'baz Bazaar version')
107 return matchoutput('baz --version 2>&1', r'baz Bazaar version')
108
108
109 @check("bzr", "Canonical's Bazaar client")
109 @check("bzr", "Canonical's Bazaar client")
110 def has_bzr():
110 def has_bzr():
111 try:
111 try:
112 import bzrlib
112 import bzrlib
113 return bzrlib.__doc__ is not None
113 return bzrlib.__doc__ is not None
114 except ImportError:
114 except ImportError:
115 return False
115 return False
116
116
117 @checkvers("bzr", "Canonical's Bazaar client >= %s", (1.14,))
117 @checkvers("bzr", "Canonical's Bazaar client >= %s", (1.14,))
118 def has_bzr_range(v):
118 def has_bzr_range(v):
119 major, minor = v.split('.')[0:2]
119 major, minor = v.split('.')[0:2]
120 try:
120 try:
121 import bzrlib
121 import bzrlib
122 return (bzrlib.__doc__ is not None
122 return (bzrlib.__doc__ is not None
123 and bzrlib.version_info[:2] >= (int(major), int(minor)))
123 and bzrlib.version_info[:2] >= (int(major), int(minor)))
124 except ImportError:
124 except ImportError:
125 return False
125 return False
126
126
127 @check("chg", "running with chg")
127 @check("chg", "running with chg")
128 def has_chg():
128 def has_chg():
129 return 'CHGHG' in os.environ
129 return 'CHGHG' in os.environ
130
130
131 @check("cvs", "cvs client/server")
131 @check("cvs", "cvs client/server")
132 def has_cvs():
132 def has_cvs():
133 re = r'Concurrent Versions System.*?server'
133 re = r'Concurrent Versions System.*?server'
134 return matchoutput('cvs --version 2>&1', re) and not has_msys()
134 return matchoutput('cvs --version 2>&1', re) and not has_msys()
135
135
136 @check("cvs112", "cvs client/server 1.12.* (not cvsnt)")
136 @check("cvs112", "cvs client/server 1.12.* (not cvsnt)")
137 def has_cvs112():
137 def has_cvs112():
138 re = r'Concurrent Versions System \(CVS\) 1.12.*?server'
138 re = r'Concurrent Versions System \(CVS\) 1.12.*?server'
139 return matchoutput('cvs --version 2>&1', re) and not has_msys()
139 return matchoutput('cvs --version 2>&1', re) and not has_msys()
140
140
141 @check("cvsnt", "cvsnt client/server")
141 @check("cvsnt", "cvsnt client/server")
142 def has_cvsnt():
142 def has_cvsnt():
143 re = r'Concurrent Versions System \(CVSNT\) (\d+).(\d+).*\(client/server\)'
143 re = r'Concurrent Versions System \(CVSNT\) (\d+).(\d+).*\(client/server\)'
144 return matchoutput('cvsnt --version 2>&1', re)
144 return matchoutput('cvsnt --version 2>&1', re)
145
145
146 @check("darcs", "darcs client")
146 @check("darcs", "darcs client")
147 def has_darcs():
147 def has_darcs():
148 return matchoutput('darcs --version', r'2\.[2-9]', True)
148 return matchoutput('darcs --version', r'2\.[2-9]', True)
149
149
150 @check("mtn", "monotone client (>= 1.0)")
150 @check("mtn", "monotone client (>= 1.0)")
151 def has_mtn():
151 def has_mtn():
152 return matchoutput('mtn --version', r'monotone', True) and not matchoutput(
152 return matchoutput('mtn --version', r'monotone', True) and not matchoutput(
153 'mtn --version', r'monotone 0\.', True)
153 'mtn --version', r'monotone 0\.', True)
154
154
155 @check("eol-in-paths", "end-of-lines in paths")
155 @check("eol-in-paths", "end-of-lines in paths")
156 def has_eol_in_paths():
156 def has_eol_in_paths():
157 try:
157 try:
158 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix, suffix='\n\r')
158 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix, suffix='\n\r')
159 os.close(fd)
159 os.close(fd)
160 os.remove(path)
160 os.remove(path)
161 return True
161 return True
162 except (IOError, OSError):
162 except (IOError, OSError):
163 return False
163 return False
164
164
165 @check("execbit", "executable bit")
165 @check("execbit", "executable bit")
166 def has_executablebit():
166 def has_executablebit():
167 try:
167 try:
168 EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
168 EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
169 fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix)
169 fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix)
170 try:
170 try:
171 os.close(fh)
171 os.close(fh)
172 m = os.stat(fn).st_mode & 0o777
172 m = os.stat(fn).st_mode & 0o777
173 new_file_has_exec = m & EXECFLAGS
173 new_file_has_exec = m & EXECFLAGS
174 os.chmod(fn, m ^ EXECFLAGS)
174 os.chmod(fn, m ^ EXECFLAGS)
175 exec_flags_cannot_flip = ((os.stat(fn).st_mode & 0o777) == m)
175 exec_flags_cannot_flip = ((os.stat(fn).st_mode & 0o777) == m)
176 finally:
176 finally:
177 os.unlink(fn)
177 os.unlink(fn)
178 except (IOError, OSError):
178 except (IOError, OSError):
179 # we don't care, the user probably won't be able to commit anyway
179 # we don't care, the user probably won't be able to commit anyway
180 return False
180 return False
181 return not (new_file_has_exec or exec_flags_cannot_flip)
181 return not (new_file_has_exec or exec_flags_cannot_flip)
182
182
183 @check("icasefs", "case insensitive file system")
183 @check("icasefs", "case insensitive file system")
184 def has_icasefs():
184 def has_icasefs():
185 # Stolen from mercurial.util
185 # Stolen from mercurial.util
186 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix)
186 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix)
187 os.close(fd)
187 os.close(fd)
188 try:
188 try:
189 s1 = os.stat(path)
189 s1 = os.stat(path)
190 d, b = os.path.split(path)
190 d, b = os.path.split(path)
191 p2 = os.path.join(d, b.upper())
191 p2 = os.path.join(d, b.upper())
192 if path == p2:
192 if path == p2:
193 p2 = os.path.join(d, b.lower())
193 p2 = os.path.join(d, b.lower())
194 try:
194 try:
195 s2 = os.stat(p2)
195 s2 = os.stat(p2)
196 return s2 == s1
196 return s2 == s1
197 except OSError:
197 except OSError:
198 return False
198 return False
199 finally:
199 finally:
200 os.remove(path)
200 os.remove(path)
201
201
202 @check("fifo", "named pipes")
202 @check("fifo", "named pipes")
203 def has_fifo():
203 def has_fifo():
204 if getattr(os, "mkfifo", None) is None:
204 if getattr(os, "mkfifo", None) is None:
205 return False
205 return False
206 name = tempfile.mktemp(dir='.', prefix=tempprefix)
206 name = tempfile.mktemp(dir='.', prefix=tempprefix)
207 try:
207 try:
208 os.mkfifo(name)
208 os.mkfifo(name)
209 os.unlink(name)
209 os.unlink(name)
210 return True
210 return True
211 except OSError:
211 except OSError:
212 return False
212 return False
213
213
214 @check("killdaemons", 'killdaemons.py support')
214 @check("killdaemons", 'killdaemons.py support')
215 def has_killdaemons():
215 def has_killdaemons():
216 return True
216 return True
217
217
218 @check("cacheable", "cacheable filesystem")
218 @check("cacheable", "cacheable filesystem")
219 def has_cacheable_fs():
219 def has_cacheable_fs():
220 from mercurial import util
220 from mercurial import util
221
221
222 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix)
222 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix)
223 os.close(fd)
223 os.close(fd)
224 try:
224 try:
225 return util.cachestat(path).cacheable()
225 return util.cachestat(path).cacheable()
226 finally:
226 finally:
227 os.remove(path)
227 os.remove(path)
228
228
229 @check("lsprof", "python lsprof module")
229 @check("lsprof", "python lsprof module")
230 def has_lsprof():
230 def has_lsprof():
231 try:
231 try:
232 import _lsprof
232 import _lsprof
233 _lsprof.Profiler # silence unused import warning
233 _lsprof.Profiler # silence unused import warning
234 return True
234 return True
235 except ImportError:
235 except ImportError:
236 return False
236 return False
237
237
238 def gethgversion():
238 def gethgversion():
239 m = matchoutput('hg --version --quiet 2>&1', r'(\d+)\.(\d+)')
239 m = matchoutput('hg --version --quiet 2>&1', r'(\d+)\.(\d+)')
240 if not m:
240 if not m:
241 return (0, 0)
241 return (0, 0)
242 return (int(m.group(1)), int(m.group(2)))
242 return (int(m.group(1)), int(m.group(2)))
243
243
244 @checkvers("hg", "Mercurial >= %s",
244 @checkvers("hg", "Mercurial >= %s",
245 list([(1.0 * x) / 10 for x in range(9, 40)]))
245 list([(1.0 * x) / 10 for x in range(9, 40)]))
246 def has_hg_range(v):
246 def has_hg_range(v):
247 major, minor = v.split('.')[0:2]
247 major, minor = v.split('.')[0:2]
248 return gethgversion() >= (int(major), int(minor))
248 return gethgversion() >= (int(major), int(minor))
249
249
250 @check("hg08", "Mercurial >= 0.8")
250 @check("hg08", "Mercurial >= 0.8")
251 def has_hg08():
251 def has_hg08():
252 if checks["hg09"][0]():
252 if checks["hg09"][0]():
253 return True
253 return True
254 return matchoutput('hg help annotate 2>&1', '--date')
254 return matchoutput('hg help annotate 2>&1', '--date')
255
255
256 @check("hg07", "Mercurial >= 0.7")
256 @check("hg07", "Mercurial >= 0.7")
257 def has_hg07():
257 def has_hg07():
258 if checks["hg08"][0]():
258 if checks["hg08"][0]():
259 return True
259 return True
260 return matchoutput('hg --version --quiet 2>&1', 'Mercurial Distributed SCM')
260 return matchoutput('hg --version --quiet 2>&1', 'Mercurial Distributed SCM')
261
261
262 @check("hg06", "Mercurial >= 0.6")
262 @check("hg06", "Mercurial >= 0.6")
263 def has_hg06():
263 def has_hg06():
264 if checks["hg07"][0]():
264 if checks["hg07"][0]():
265 return True
265 return True
266 return matchoutput('hg --version --quiet 2>&1', 'Mercurial version')
266 return matchoutput('hg --version --quiet 2>&1', 'Mercurial version')
267
267
268 @check("gettext", "GNU Gettext (msgfmt)")
268 @check("gettext", "GNU Gettext (msgfmt)")
269 def has_gettext():
269 def has_gettext():
270 return matchoutput('msgfmt --version', 'GNU gettext-tools')
270 return matchoutput('msgfmt --version', 'GNU gettext-tools')
271
271
272 @check("git", "git command line client")
272 @check("git", "git command line client")
273 def has_git():
273 def has_git():
274 return matchoutput('git --version 2>&1', r'^git version')
274 return matchoutput('git --version 2>&1', r'^git version')
275
275
276 @check("docutils", "Docutils text processing library")
276 @check("docutils", "Docutils text processing library")
277 def has_docutils():
277 def has_docutils():
278 try:
278 try:
279 import docutils.core
279 import docutils.core
280 docutils.core.publish_cmdline # silence unused import
280 docutils.core.publish_cmdline # silence unused import
281 return True
281 return True
282 except ImportError:
282 except ImportError:
283 return False
283 return False
284
284
285 def getsvnversion():
285 def getsvnversion():
286 m = matchoutput('svn --version --quiet 2>&1', r'^(\d+)\.(\d+)')
286 m = matchoutput('svn --version --quiet 2>&1', r'^(\d+)\.(\d+)')
287 if not m:
287 if not m:
288 return (0, 0)
288 return (0, 0)
289 return (int(m.group(1)), int(m.group(2)))
289 return (int(m.group(1)), int(m.group(2)))
290
290
291 @checkvers("svn", "subversion client and admin tools >= %s", (1.3, 1.5))
291 @checkvers("svn", "subversion client and admin tools >= %s", (1.3, 1.5))
292 def has_svn_range(v):
292 def has_svn_range(v):
293 major, minor = v.split('.')[0:2]
293 major, minor = v.split('.')[0:2]
294 return getsvnversion() >= (int(major), int(minor))
294 return getsvnversion() >= (int(major), int(minor))
295
295
296 @check("svn", "subversion client and admin tools")
296 @check("svn", "subversion client and admin tools")
297 def has_svn():
297 def has_svn():
298 return matchoutput('svn --version 2>&1', r'^svn, version') and \
298 return matchoutput('svn --version 2>&1', r'^svn, version') and \
299 matchoutput('svnadmin --version 2>&1', r'^svnadmin, version')
299 matchoutput('svnadmin --version 2>&1', r'^svnadmin, version')
300
300
301 @check("svn-bindings", "subversion python bindings")
301 @check("svn-bindings", "subversion python bindings")
302 def has_svn_bindings():
302 def has_svn_bindings():
303 try:
303 try:
304 import svn.core
304 import svn.core
305 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
305 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
306 if version < (1, 4):
306 if version < (1, 4):
307 return False
307 return False
308 return True
308 return True
309 except ImportError:
309 except ImportError:
310 return False
310 return False
311
311
312 @check("p4", "Perforce server and client")
312 @check("p4", "Perforce server and client")
313 def has_p4():
313 def has_p4():
314 return (matchoutput('p4 -V', r'Rev\. P4/') and
314 return (matchoutput('p4 -V', r'Rev\. P4/') and
315 matchoutput('p4d -V', r'Rev\. P4D/'))
315 matchoutput('p4d -V', r'Rev\. P4D/'))
316
316
317 @check("symlink", "symbolic links")
317 @check("symlink", "symbolic links")
318 def has_symlink():
318 def has_symlink():
319 if getattr(os, "symlink", None) is None:
319 if getattr(os, "symlink", None) is None:
320 return False
320 return False
321 name = tempfile.mktemp(dir='.', prefix=tempprefix)
321 name = tempfile.mktemp(dir='.', prefix=tempprefix)
322 try:
322 try:
323 os.symlink(".", name)
323 os.symlink(".", name)
324 os.unlink(name)
324 os.unlink(name)
325 return True
325 return True
326 except (OSError, AttributeError):
326 except (OSError, AttributeError):
327 return False
327 return False
328
328
329 @check("hardlink", "hardlinks")
329 @check("hardlink", "hardlinks")
330 def has_hardlink():
330 def has_hardlink():
331 from mercurial import util
331 from mercurial import util
332 fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix)
332 fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix)
333 os.close(fh)
333 os.close(fh)
334 name = tempfile.mktemp(dir='.', prefix=tempprefix)
334 name = tempfile.mktemp(dir='.', prefix=tempprefix)
335 try:
335 try:
336 util.oslink(fn, name)
336 util.oslink(fn, name)
337 os.unlink(name)
337 os.unlink(name)
338 return True
338 return True
339 except OSError:
339 except OSError:
340 return False
340 return False
341 finally:
341 finally:
342 os.unlink(fn)
342 os.unlink(fn)
343
343
344 @check("tla", "GNU Arch tla client")
344 @check("tla", "GNU Arch tla client")
345 def has_tla():
345 def has_tla():
346 return matchoutput('tla --version 2>&1', r'The GNU Arch Revision')
346 return matchoutput('tla --version 2>&1', r'The GNU Arch Revision')
347
347
348 @check("gpg", "gpg client")
348 @check("gpg", "gpg client")
349 def has_gpg():
349 def has_gpg():
350 return matchoutput('gpg --version 2>&1', r'GnuPG')
350 return matchoutput('gpg --version 2>&1', r'GnuPG')
351
351
352 @check("unix-permissions", "unix-style permissions")
352 @check("unix-permissions", "unix-style permissions")
353 def has_unix_permissions():
353 def has_unix_permissions():
354 d = tempfile.mkdtemp(dir='.', prefix=tempprefix)
354 d = tempfile.mkdtemp(dir='.', prefix=tempprefix)
355 try:
355 try:
356 fname = os.path.join(d, 'foo')
356 fname = os.path.join(d, 'foo')
357 for umask in (0o77, 0o07, 0o22):
357 for umask in (0o77, 0o07, 0o22):
358 os.umask(umask)
358 os.umask(umask)
359 f = open(fname, 'w')
359 f = open(fname, 'w')
360 f.close()
360 f.close()
361 mode = os.stat(fname).st_mode
361 mode = os.stat(fname).st_mode
362 os.unlink(fname)
362 os.unlink(fname)
363 if mode & 0o777 != ~umask & 0o666:
363 if mode & 0o777 != ~umask & 0o666:
364 return False
364 return False
365 return True
365 return True
366 finally:
366 finally:
367 os.rmdir(d)
367 os.rmdir(d)
368
368
369 @check("unix-socket", "AF_UNIX socket family")
369 @check("unix-socket", "AF_UNIX socket family")
370 def has_unix_socket():
370 def has_unix_socket():
371 return getattr(socket, 'AF_UNIX', None) is not None
371 return getattr(socket, 'AF_UNIX', None) is not None
372
372
373 @check("root", "root permissions")
373 @check("root", "root permissions")
374 def has_root():
374 def has_root():
375 return getattr(os, 'geteuid', None) and os.geteuid() == 0
375 return getattr(os, 'geteuid', None) and os.geteuid() == 0
376
376
377 @check("pyflakes", "Pyflakes python linter")
377 @check("pyflakes", "Pyflakes python linter")
378 def has_pyflakes():
378 def has_pyflakes():
379 return matchoutput("sh -c \"echo 'import re' 2>&1 | pyflakes\"",
379 return matchoutput("sh -c \"echo 'import re' 2>&1 | pyflakes\"",
380 r"<stdin>:1: 're' imported but unused",
380 r"<stdin>:1: 're' imported but unused",
381 True)
381 True)
382
382
383 @check("pygments", "Pygments source highlighting library")
383 @check("pygments", "Pygments source highlighting library")
384 def has_pygments():
384 def has_pygments():
385 try:
385 try:
386 import pygments
386 import pygments
387 pygments.highlight # silence unused import warning
387 pygments.highlight # silence unused import warning
388 return True
388 return True
389 except ImportError:
389 except ImportError:
390 return False
390 return False
391
391
392 @check("outer-repo", "outer repo")
392 @check("outer-repo", "outer repo")
393 def has_outer_repo():
393 def has_outer_repo():
394 # failing for other reasons than 'no repo' imply that there is a repo
394 # failing for other reasons than 'no repo' imply that there is a repo
395 return not matchoutput('hg root 2>&1',
395 return not matchoutput('hg root 2>&1',
396 r'abort: no repository found', True)
396 r'abort: no repository found', True)
397
397
398 @check("ssl", "ssl module available")
398 @check("ssl", "ssl module available")
399 def has_ssl():
399 def has_ssl():
400 try:
400 try:
401 import ssl
401 import ssl
402 ssl.CERT_NONE
402 ssl.CERT_NONE
403 return True
403 return True
404 except ImportError:
404 except ImportError:
405 return False
405 return False
406
406
407 @check("sslcontext", "python >= 2.7.9 ssl")
407 @check("sslcontext", "python >= 2.7.9 ssl")
408 def has_sslcontext():
408 def has_sslcontext():
409 try:
409 try:
410 import ssl
410 import ssl
411 ssl.SSLContext
411 ssl.SSLContext
412 return True
412 return True
413 except (ImportError, AttributeError):
413 except (ImportError, AttributeError):
414 return False
414 return False
415
415
416 @check("defaultcacerts", "can verify SSL certs by system's CA certs store")
416 @check("defaultcacerts", "can verify SSL certs by system's CA certs store")
417 def has_defaultcacerts():
417 def has_defaultcacerts():
418 from mercurial import sslutil
418 from mercurial import sslutil
419 return sslutil._defaultcacerts() != '!'
419 return sslutil._defaultcacerts() or sslutil._canloaddefaultcerts
420
420
421 @check("windows", "Windows")
421 @check("windows", "Windows")
422 def has_windows():
422 def has_windows():
423 return os.name == 'nt'
423 return os.name == 'nt'
424
424
425 @check("system-sh", "system() uses sh")
425 @check("system-sh", "system() uses sh")
426 def has_system_sh():
426 def has_system_sh():
427 return os.name != 'nt'
427 return os.name != 'nt'
428
428
429 @check("serve", "platform and python can manage 'hg serve -d'")
429 @check("serve", "platform and python can manage 'hg serve -d'")
430 def has_serve():
430 def has_serve():
431 return os.name != 'nt' # gross approximation
431 return os.name != 'nt' # gross approximation
432
432
433 @check("test-repo", "running tests from repository")
433 @check("test-repo", "running tests from repository")
434 def has_test_repo():
434 def has_test_repo():
435 t = os.environ["TESTDIR"]
435 t = os.environ["TESTDIR"]
436 return os.path.isdir(os.path.join(t, "..", ".hg"))
436 return os.path.isdir(os.path.join(t, "..", ".hg"))
437
437
438 @check("tic", "terminfo compiler and curses module")
438 @check("tic", "terminfo compiler and curses module")
439 def has_tic():
439 def has_tic():
440 try:
440 try:
441 import curses
441 import curses
442 curses.COLOR_BLUE
442 curses.COLOR_BLUE
443 return matchoutput('test -x "`which tic`"', '')
443 return matchoutput('test -x "`which tic`"', '')
444 except ImportError:
444 except ImportError:
445 return False
445 return False
446
446
447 @check("msys", "Windows with MSYS")
447 @check("msys", "Windows with MSYS")
448 def has_msys():
448 def has_msys():
449 return os.getenv('MSYSTEM')
449 return os.getenv('MSYSTEM')
450
450
451 @check("aix", "AIX")
451 @check("aix", "AIX")
452 def has_aix():
452 def has_aix():
453 return sys.platform.startswith("aix")
453 return sys.platform.startswith("aix")
454
454
455 @check("osx", "OS X")
455 @check("osx", "OS X")
456 def has_osx():
456 def has_osx():
457 return sys.platform == 'darwin'
457 return sys.platform == 'darwin'
458
458
459 @check("osxpackaging", "OS X packaging tools")
459 @check("osxpackaging", "OS X packaging tools")
460 def has_osxpackaging():
460 def has_osxpackaging():
461 try:
461 try:
462 return (matchoutput('pkgbuild', 'Usage: pkgbuild ', ignorestatus=1)
462 return (matchoutput('pkgbuild', 'Usage: pkgbuild ', ignorestatus=1)
463 and matchoutput(
463 and matchoutput(
464 'productbuild', 'Usage: productbuild ',
464 'productbuild', 'Usage: productbuild ',
465 ignorestatus=1)
465 ignorestatus=1)
466 and matchoutput('lsbom', 'Usage: lsbom', ignorestatus=1)
466 and matchoutput('lsbom', 'Usage: lsbom', ignorestatus=1)
467 and matchoutput(
467 and matchoutput(
468 'xar --help', 'Usage: xar', ignorestatus=1))
468 'xar --help', 'Usage: xar', ignorestatus=1))
469 except ImportError:
469 except ImportError:
470 return False
470 return False
471
471
472 @check("docker", "docker support")
472 @check("docker", "docker support")
473 def has_docker():
473 def has_docker():
474 pat = r'A self-sufficient runtime for'
474 pat = r'A self-sufficient runtime for'
475 if matchoutput('docker --help', pat):
475 if matchoutput('docker --help', pat):
476 if 'linux' not in sys.platform:
476 if 'linux' not in sys.platform:
477 # TODO: in theory we should be able to test docker-based
477 # TODO: in theory we should be able to test docker-based
478 # package creation on non-linux using boot2docker, but in
478 # package creation on non-linux using boot2docker, but in
479 # practice that requires extra coordination to make sure
479 # practice that requires extra coordination to make sure
480 # $TESTTEMP is going to be visible at the same path to the
480 # $TESTTEMP is going to be visible at the same path to the
481 # boot2docker VM. If we figure out how to verify that, we
481 # boot2docker VM. If we figure out how to verify that, we
482 # can use the following instead of just saying False:
482 # can use the following instead of just saying False:
483 # return 'DOCKER_HOST' in os.environ
483 # return 'DOCKER_HOST' in os.environ
484 return False
484 return False
485
485
486 return True
486 return True
487 return False
487 return False
488
488
489 @check("debhelper", "debian packaging tools")
489 @check("debhelper", "debian packaging tools")
490 def has_debhelper():
490 def has_debhelper():
491 dpkg = matchoutput('dpkg --version',
491 dpkg = matchoutput('dpkg --version',
492 "Debian `dpkg' package management program")
492 "Debian `dpkg' package management program")
493 dh = matchoutput('dh --help',
493 dh = matchoutput('dh --help',
494 'dh is a part of debhelper.', ignorestatus=True)
494 'dh is a part of debhelper.', ignorestatus=True)
495 dh_py2 = matchoutput('dh_python2 --help',
495 dh_py2 = matchoutput('dh_python2 --help',
496 'other supported Python versions')
496 'other supported Python versions')
497 return dpkg and dh and dh_py2
497 return dpkg and dh and dh_py2
498
498
499 @check("absimport", "absolute_import in __future__")
499 @check("absimport", "absolute_import in __future__")
500 def has_absimport():
500 def has_absimport():
501 import __future__
501 import __future__
502 from mercurial import util
502 from mercurial import util
503 return util.safehasattr(__future__, "absolute_import")
503 return util.safehasattr(__future__, "absolute_import")
504
504
505 @check("py3k", "running with Python 3.x")
505 @check("py3k", "running with Python 3.x")
506 def has_py3k():
506 def has_py3k():
507 return 3 == sys.version_info[0]
507 return 3 == sys.version_info[0]
508
508
509 @check("py3exe", "a Python 3.x interpreter is available")
509 @check("py3exe", "a Python 3.x interpreter is available")
510 def has_python3exe():
510 def has_python3exe():
511 return 'PYTHON3' in os.environ
511 return 'PYTHON3' in os.environ
512
512
513 @check("pure", "running with pure Python code")
513 @check("pure", "running with pure Python code")
514 def has_pure():
514 def has_pure():
515 return any([
515 return any([
516 os.environ.get("HGMODULEPOLICY") == "py",
516 os.environ.get("HGMODULEPOLICY") == "py",
517 os.environ.get("HGTEST_RUN_TESTS_PURE") == "--pure",
517 os.environ.get("HGTEST_RUN_TESTS_PURE") == "--pure",
518 ])
518 ])
519
519
520 @check("slow", "allow slow tests")
520 @check("slow", "allow slow tests")
521 def has_slow():
521 def has_slow():
522 return os.environ.get('HGTEST_SLOW') == 'slow'
522 return os.environ.get('HGTEST_SLOW') == 'slow'
523
523
524 @check("hypothesis", "Hypothesis automated test generation")
524 @check("hypothesis", "Hypothesis automated test generation")
525 def has_hypothesis():
525 def has_hypothesis():
526 try:
526 try:
527 import hypothesis
527 import hypothesis
528 hypothesis.given
528 hypothesis.given
529 return True
529 return True
530 except ImportError:
530 except ImportError:
531 return False
531 return False
General Comments 0
You need to be logged in to leave comments. Login now