##// END OF EJS Templates
extensions: also search for extension in the 'hgext3rd' package...
Pierre-Yves David -
r28541:4b81487a default
parent child Browse files
Show More
@@ -1,3 +1,4 b''
1 # name space package to host third party extensions
1 2 from __future__ import absolute_import
2 3 import pkgutil
3 4 __path__ = pkgutil.extend_path(__path__, __name__)
@@ -1,487 +1,491 b''
1 1 # extensions.py - extension handling for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import imp
11 11 import os
12 12
13 13 from .i18n import (
14 14 _,
15 15 gettext,
16 16 )
17 17
18 18 from . import (
19 19 cmdutil,
20 20 error,
21 21 util,
22 22 )
23 23
24 24 _extensions = {}
25 25 _aftercallbacks = {}
26 26 _order = []
27 27 _builtin = set(['hbisect', 'bookmarks', 'parentrevspec', 'progress', 'interhg',
28 28 'inotify'])
29 29
30 30 def extensions(ui=None):
31 31 if ui:
32 32 def enabled(name):
33 33 for format in ['%s', 'hgext.%s']:
34 34 conf = ui.config('extensions', format % name)
35 35 if conf is not None and not conf.startswith('!'):
36 36 return True
37 37 else:
38 38 enabled = lambda name: True
39 39 for name in _order:
40 40 module = _extensions[name]
41 41 if module and enabled(name):
42 42 yield name, module
43 43
44 44 def find(name):
45 45 '''return module with given extension name'''
46 46 mod = None
47 47 try:
48 48 mod = _extensions[name]
49 49 except KeyError:
50 50 for k, v in _extensions.iteritems():
51 51 if k.endswith('.' + name) or k.endswith('/' + name):
52 52 mod = v
53 53 break
54 54 if not mod:
55 55 raise KeyError(name)
56 56 return mod
57 57
58 58 def loadpath(path, module_name):
59 59 module_name = module_name.replace('.', '_')
60 60 path = util.normpath(util.expandpath(path))
61 61 if os.path.isdir(path):
62 62 # module/__init__.py style
63 63 d, f = os.path.split(path)
64 64 fd, fpath, desc = imp.find_module(f, [d])
65 65 return imp.load_module(module_name, fd, fpath, desc)
66 66 else:
67 67 try:
68 68 return imp.load_source(module_name, path)
69 69 except IOError as exc:
70 70 if not exc.filename:
71 71 exc.filename = path # python does not fill this
72 72 raise
73 73
74 74 def _importh(name):
75 75 """import and return the <name> module"""
76 76 mod = __import__(name)
77 77 components = name.split('.')
78 78 for comp in components[1:]:
79 79 mod = getattr(mod, comp)
80 80 return mod
81 81
82 82 def _reportimporterror(ui, err, failed, next):
83 83 ui.debug('could not import %s (%s): trying %s\n'
84 84 % (failed, err, next))
85 85 if ui.debugflag:
86 86 ui.traceback()
87 87
88 88 def load(ui, name, path):
89 89 if name.startswith('hgext.') or name.startswith('hgext/'):
90 90 shortname = name[6:]
91 91 else:
92 92 shortname = name
93 93 if shortname in _builtin:
94 94 return None
95 95 if shortname in _extensions:
96 96 return _extensions[shortname]
97 97 _extensions[shortname] = None
98 98 if path:
99 99 # the module will be loaded in sys.modules
100 100 # choose an unique name so that it doesn't
101 101 # conflicts with other modules
102 102 mod = loadpath(path, 'hgext.%s' % name)
103 103 else:
104 104 try:
105 105 mod = _importh("hgext.%s" % name)
106 106 except ImportError as err:
107 107 _reportimporterror(ui, err, "hgext.%s" % name, name)
108 mod = _importh(name)
108 try:
109 mod = _importh("hgext3rd.%s" % name)
110 except ImportError as err:
111 _reportimporterror(ui, err, "hgext3rd.%s" % name, name)
112 mod = _importh(name)
109 113
110 114 # Before we do anything with the extension, check against minimum stated
111 115 # compatibility. This gives extension authors a mechanism to have their
112 116 # extensions short circuit when loaded with a known incompatible version
113 117 # of Mercurial.
114 118 minver = getattr(mod, 'minimumhgversion', None)
115 119 if minver and util.versiontuple(minver, 2) > util.versiontuple(n=2):
116 120 ui.warn(_('(third party extension %s requires version %s or newer '
117 121 'of Mercurial; disabling)\n') % (shortname, minver))
118 122 return
119 123
120 124 _extensions[shortname] = mod
121 125 _order.append(shortname)
122 126 for fn in _aftercallbacks.get(shortname, []):
123 127 fn(loaded=True)
124 128 return mod
125 129
126 130 def loadall(ui):
127 131 result = ui.configitems("extensions")
128 132 newindex = len(_order)
129 133 for (name, path) in result:
130 134 if path:
131 135 if path[0] == '!':
132 136 continue
133 137 try:
134 138 load(ui, name, path)
135 139 except KeyboardInterrupt:
136 140 raise
137 141 except Exception as inst:
138 142 if path:
139 143 ui.warn(_("*** failed to import extension %s from %s: %s\n")
140 144 % (name, path, inst))
141 145 else:
142 146 ui.warn(_("*** failed to import extension %s: %s\n")
143 147 % (name, inst))
144 148 ui.traceback()
145 149
146 150 for name in _order[newindex:]:
147 151 uisetup = getattr(_extensions[name], 'uisetup', None)
148 152 if uisetup:
149 153 uisetup(ui)
150 154
151 155 for name in _order[newindex:]:
152 156 extsetup = getattr(_extensions[name], 'extsetup', None)
153 157 if extsetup:
154 158 try:
155 159 extsetup(ui)
156 160 except TypeError:
157 161 if extsetup.func_code.co_argcount != 0:
158 162 raise
159 163 extsetup() # old extsetup with no ui argument
160 164
161 165 # Call aftercallbacks that were never met.
162 166 for shortname in _aftercallbacks:
163 167 if shortname in _extensions:
164 168 continue
165 169
166 170 for fn in _aftercallbacks[shortname]:
167 171 fn(loaded=False)
168 172
169 173 # loadall() is called multiple times and lingering _aftercallbacks
170 174 # entries could result in double execution. See issue4646.
171 175 _aftercallbacks.clear()
172 176
173 177 def afterloaded(extension, callback):
174 178 '''Run the specified function after a named extension is loaded.
175 179
176 180 If the named extension is already loaded, the callback will be called
177 181 immediately.
178 182
179 183 If the named extension never loads, the callback will be called after
180 184 all extensions have been loaded.
181 185
182 186 The callback receives the named argument ``loaded``, which is a boolean
183 187 indicating whether the dependent extension actually loaded.
184 188 '''
185 189
186 190 if extension in _extensions:
187 191 callback(loaded=True)
188 192 else:
189 193 _aftercallbacks.setdefault(extension, []).append(callback)
190 194
191 195 def bind(func, *args):
192 196 '''Partial function application
193 197
194 198 Returns a new function that is the partial application of args and kwargs
195 199 to func. For example,
196 200
197 201 f(1, 2, bar=3) === bind(f, 1)(2, bar=3)'''
198 202 assert callable(func)
199 203 def closure(*a, **kw):
200 204 return func(*(args + a), **kw)
201 205 return closure
202 206
203 207 def _updatewrapper(wrap, origfn):
204 208 '''Copy attributes to wrapper function'''
205 209 wrap.__module__ = getattr(origfn, '__module__')
206 210 wrap.__doc__ = getattr(origfn, '__doc__')
207 211 wrap.__dict__.update(getattr(origfn, '__dict__', {}))
208 212
209 213 def wrapcommand(table, command, wrapper, synopsis=None, docstring=None):
210 214 '''Wrap the command named `command' in table
211 215
212 216 Replace command in the command table with wrapper. The wrapped command will
213 217 be inserted into the command table specified by the table argument.
214 218
215 219 The wrapper will be called like
216 220
217 221 wrapper(orig, *args, **kwargs)
218 222
219 223 where orig is the original (wrapped) function, and *args, **kwargs
220 224 are the arguments passed to it.
221 225
222 226 Optionally append to the command synopsis and docstring, used for help.
223 227 For example, if your extension wraps the ``bookmarks`` command to add the
224 228 flags ``--remote`` and ``--all`` you might call this function like so:
225 229
226 230 synopsis = ' [-a] [--remote]'
227 231 docstring = """
228 232
229 233 The ``remotenames`` extension adds the ``--remote`` and ``--all`` (``-a``)
230 234 flags to the bookmarks command. Either flag will show the remote bookmarks
231 235 known to the repository; ``--remote`` will also suppress the output of the
232 236 local bookmarks.
233 237 """
234 238
235 239 extensions.wrapcommand(commands.table, 'bookmarks', exbookmarks,
236 240 synopsis, docstring)
237 241 '''
238 242 assert callable(wrapper)
239 243 aliases, entry = cmdutil.findcmd(command, table)
240 244 for alias, e in table.iteritems():
241 245 if e is entry:
242 246 key = alias
243 247 break
244 248
245 249 origfn = entry[0]
246 250 wrap = bind(util.checksignature(wrapper), util.checksignature(origfn))
247 251 _updatewrapper(wrap, origfn)
248 252 if docstring is not None:
249 253 wrap.__doc__ += docstring
250 254
251 255 newentry = list(entry)
252 256 newentry[0] = wrap
253 257 if synopsis is not None:
254 258 newentry[2] += synopsis
255 259 table[key] = tuple(newentry)
256 260 return entry
257 261
258 262 def wrapfunction(container, funcname, wrapper):
259 263 '''Wrap the function named funcname in container
260 264
261 265 Replace the funcname member in the given container with the specified
262 266 wrapper. The container is typically a module, class, or instance.
263 267
264 268 The wrapper will be called like
265 269
266 270 wrapper(orig, *args, **kwargs)
267 271
268 272 where orig is the original (wrapped) function, and *args, **kwargs
269 273 are the arguments passed to it.
270 274
271 275 Wrapping methods of the repository object is not recommended since
272 276 it conflicts with extensions that extend the repository by
273 277 subclassing. All extensions that need to extend methods of
274 278 localrepository should use this subclassing trick: namely,
275 279 reposetup() should look like
276 280
277 281 def reposetup(ui, repo):
278 282 class myrepo(repo.__class__):
279 283 def whatever(self, *args, **kwargs):
280 284 [...extension stuff...]
281 285 super(myrepo, self).whatever(*args, **kwargs)
282 286 [...extension stuff...]
283 287
284 288 repo.__class__ = myrepo
285 289
286 290 In general, combining wrapfunction() with subclassing does not
287 291 work. Since you cannot control what other extensions are loaded by
288 292 your end users, you should play nicely with others by using the
289 293 subclass trick.
290 294 '''
291 295 assert callable(wrapper)
292 296
293 297 origfn = getattr(container, funcname)
294 298 assert callable(origfn)
295 299 wrap = bind(wrapper, origfn)
296 300 _updatewrapper(wrap, origfn)
297 301 setattr(container, funcname, wrap)
298 302 return origfn
299 303
300 304 def _disabledpaths(strip_init=False):
301 305 '''find paths of disabled extensions. returns a dict of {name: path}
302 306 removes /__init__.py from packages if strip_init is True'''
303 307 import hgext
304 308 extpath = os.path.dirname(os.path.abspath(hgext.__file__))
305 309 try: # might not be a filesystem path
306 310 files = os.listdir(extpath)
307 311 except OSError:
308 312 return {}
309 313
310 314 exts = {}
311 315 for e in files:
312 316 if e.endswith('.py'):
313 317 name = e.rsplit('.', 1)[0]
314 318 path = os.path.join(extpath, e)
315 319 else:
316 320 name = e
317 321 path = os.path.join(extpath, e, '__init__.py')
318 322 if not os.path.exists(path):
319 323 continue
320 324 if strip_init:
321 325 path = os.path.dirname(path)
322 326 if name in exts or name in _order or name == '__init__':
323 327 continue
324 328 exts[name] = path
325 329 return exts
326 330
327 331 def _moduledoc(file):
328 332 '''return the top-level python documentation for the given file
329 333
330 334 Loosely inspired by pydoc.source_synopsis(), but rewritten to
331 335 handle triple quotes and to return the whole text instead of just
332 336 the synopsis'''
333 337 result = []
334 338
335 339 line = file.readline()
336 340 while line[:1] == '#' or not line.strip():
337 341 line = file.readline()
338 342 if not line:
339 343 break
340 344
341 345 start = line[:3]
342 346 if start == '"""' or start == "'''":
343 347 line = line[3:]
344 348 while line:
345 349 if line.rstrip().endswith(start):
346 350 line = line.split(start)[0]
347 351 if line:
348 352 result.append(line)
349 353 break
350 354 elif not line:
351 355 return None # unmatched delimiter
352 356 result.append(line)
353 357 line = file.readline()
354 358 else:
355 359 return None
356 360
357 361 return ''.join(result)
358 362
359 363 def _disabledhelp(path):
360 364 '''retrieve help synopsis of a disabled extension (without importing)'''
361 365 try:
362 366 file = open(path)
363 367 except IOError:
364 368 return
365 369 else:
366 370 doc = _moduledoc(file)
367 371 file.close()
368 372
369 373 if doc: # extracting localized synopsis
370 374 return gettext(doc).splitlines()[0]
371 375 else:
372 376 return _('(no help text available)')
373 377
374 378 def disabled():
375 379 '''find disabled extensions from hgext. returns a dict of {name: desc}'''
376 380 try:
377 381 from hgext import __index__
378 382 return dict((name, gettext(desc))
379 383 for name, desc in __index__.docs.iteritems()
380 384 if name not in _order)
381 385 except (ImportError, AttributeError):
382 386 pass
383 387
384 388 paths = _disabledpaths()
385 389 if not paths:
386 390 return {}
387 391
388 392 exts = {}
389 393 for name, path in paths.iteritems():
390 394 doc = _disabledhelp(path)
391 395 if doc:
392 396 exts[name] = doc
393 397
394 398 return exts
395 399
396 400 def disabledext(name):
397 401 '''find a specific disabled extension from hgext. returns desc'''
398 402 try:
399 403 from hgext import __index__
400 404 if name in _order: # enabled
401 405 return
402 406 else:
403 407 return gettext(__index__.docs.get(name))
404 408 except (ImportError, AttributeError):
405 409 pass
406 410
407 411 paths = _disabledpaths()
408 412 if name in paths:
409 413 return _disabledhelp(paths[name])
410 414
411 415 def disabledcmd(ui, cmd, strict=False):
412 416 '''import disabled extensions until cmd is found.
413 417 returns (cmdname, extname, module)'''
414 418
415 419 paths = _disabledpaths(strip_init=True)
416 420 if not paths:
417 421 raise error.UnknownCommand(cmd)
418 422
419 423 def findcmd(cmd, name, path):
420 424 try:
421 425 mod = loadpath(path, 'hgext.%s' % name)
422 426 except Exception:
423 427 return
424 428 try:
425 429 aliases, entry = cmdutil.findcmd(cmd,
426 430 getattr(mod, 'cmdtable', {}), strict)
427 431 except (error.AmbiguousCommand, error.UnknownCommand):
428 432 return
429 433 except Exception:
430 434 ui.warn(_('warning: error finding commands in %s\n') % path)
431 435 ui.traceback()
432 436 return
433 437 for c in aliases:
434 438 if c.startswith(cmd):
435 439 cmd = c
436 440 break
437 441 else:
438 442 cmd = aliases[0]
439 443 return (cmd, name, mod)
440 444
441 445 ext = None
442 446 # first, search for an extension with the same name as the command
443 447 path = paths.pop(cmd, None)
444 448 if path:
445 449 ext = findcmd(cmd, cmd, path)
446 450 if not ext:
447 451 # otherwise, interrogate each extension until there's a match
448 452 for name, path in paths.iteritems():
449 453 ext = findcmd(cmd, name, path)
450 454 if ext:
451 455 break
452 456 if ext and 'DEPRECATED' not in ext.__doc__:
453 457 return ext
454 458
455 459 raise error.UnknownCommand(cmd)
456 460
457 461 def enabled(shortname=True):
458 462 '''return a dict of {name: desc} of extensions'''
459 463 exts = {}
460 464 for ename, ext in extensions():
461 465 doc = (gettext(ext.__doc__) or _('(no help text available)'))
462 466 if shortname:
463 467 ename = ename.split('.')[-1]
464 468 exts[ename] = doc.splitlines()[0].strip()
465 469
466 470 return exts
467 471
468 472 def notloaded():
469 473 '''return short names of extensions that failed to load'''
470 474 return [name for name, mod in _extensions.iteritems() if mod is None]
471 475
472 476 def moduleversion(module):
473 477 '''return version information from given module as a string'''
474 478 if (util.safehasattr(module, 'getversion')
475 479 and callable(module.getversion)):
476 480 version = module.getversion()
477 481 elif util.safehasattr(module, '__version__'):
478 482 version = module.__version__
479 483 else:
480 484 version = ''
481 485 if isinstance(version, (list, tuple)):
482 486 version = '.'.join(str(o) for o in version)
483 487 return version
484 488
485 489 def ismoduleinternal(module):
486 490 exttestedwith = getattr(module, 'testedwith', None)
487 491 return exttestedwith == "internal"
@@ -1,677 +1,677 b''
1 1 #
2 2 # This is the mercurial setup script.
3 3 #
4 4 # 'python setup.py install', or
5 5 # 'python setup.py --help' for more options
6 6
7 7 import sys, platform
8 8 if getattr(sys, 'version_info', (0, 0, 0)) < (2, 6, 0, 'final'):
9 9 raise SystemExit("Mercurial requires Python 2.6 or later.")
10 10
11 11 if sys.version_info[0] >= 3:
12 12 printf = eval('print')
13 13 libdir_escape = 'unicode_escape'
14 14 else:
15 15 libdir_escape = 'string_escape'
16 16 def printf(*args, **kwargs):
17 17 f = kwargs.get('file', sys.stdout)
18 18 end = kwargs.get('end', '\n')
19 19 f.write(b' '.join(args) + end)
20 20
21 21 # Solaris Python packaging brain damage
22 22 try:
23 23 import hashlib
24 24 sha = hashlib.sha1()
25 25 except ImportError:
26 26 try:
27 27 import sha
28 28 sha.sha # silence unused import warning
29 29 except ImportError:
30 30 raise SystemExit(
31 31 "Couldn't import standard hashlib (incomplete Python install).")
32 32
33 33 try:
34 34 import zlib
35 35 zlib.compressobj # silence unused import warning
36 36 except ImportError:
37 37 raise SystemExit(
38 38 "Couldn't import standard zlib (incomplete Python install).")
39 39
40 40 # The base IronPython distribution (as of 2.7.1) doesn't support bz2
41 41 isironpython = False
42 42 try:
43 43 isironpython = (platform.python_implementation()
44 44 .lower().find("ironpython") != -1)
45 45 except AttributeError:
46 46 pass
47 47
48 48 if isironpython:
49 49 sys.stderr.write("warning: IronPython detected (no bz2 support)\n")
50 50 else:
51 51 try:
52 52 import bz2
53 53 bz2.BZ2Compressor # silence unused import warning
54 54 except ImportError:
55 55 raise SystemExit(
56 56 "Couldn't import standard bz2 (incomplete Python install).")
57 57
58 58 ispypy = "PyPy" in sys.version
59 59
60 60 import os, stat, subprocess, time
61 61 import re
62 62 import shutil
63 63 import tempfile
64 64 from distutils import log
65 65 if 'FORCE_SETUPTOOLS' in os.environ:
66 66 from setuptools import setup
67 67 else:
68 68 from distutils.core import setup
69 69 from distutils.core import Command, Extension
70 70 from distutils.dist import Distribution
71 71 from distutils.command.build import build
72 72 from distutils.command.build_ext import build_ext
73 73 from distutils.command.build_py import build_py
74 74 from distutils.command.build_scripts import build_scripts
75 75 from distutils.command.install_lib import install_lib
76 76 from distutils.command.install_scripts import install_scripts
77 77 from distutils.spawn import spawn, find_executable
78 78 from distutils import file_util
79 79 from distutils.errors import (
80 80 CCompilerError,
81 81 DistutilsError,
82 82 DistutilsExecError,
83 83 )
84 84 from distutils.sysconfig import get_python_inc, get_config_var
85 85 from distutils.version import StrictVersion
86 86
87 87 scripts = ['hg']
88 88 if os.name == 'nt':
89 89 # We remove hg.bat if we are able to build hg.exe.
90 90 scripts.append('contrib/win32/hg.bat')
91 91
92 92 # simplified version of distutils.ccompiler.CCompiler.has_function
93 93 # that actually removes its temporary files.
94 94 def hasfunction(cc, funcname):
95 95 tmpdir = tempfile.mkdtemp(prefix='hg-install-')
96 96 devnull = oldstderr = None
97 97 try:
98 98 fname = os.path.join(tmpdir, 'funcname.c')
99 99 f = open(fname, 'w')
100 100 f.write('int main(void) {\n')
101 101 f.write(' %s();\n' % funcname)
102 102 f.write('}\n')
103 103 f.close()
104 104 # Redirect stderr to /dev/null to hide any error messages
105 105 # from the compiler.
106 106 # This will have to be changed if we ever have to check
107 107 # for a function on Windows.
108 108 devnull = open('/dev/null', 'w')
109 109 oldstderr = os.dup(sys.stderr.fileno())
110 110 os.dup2(devnull.fileno(), sys.stderr.fileno())
111 111 objects = cc.compile([fname], output_dir=tmpdir)
112 112 cc.link_executable(objects, os.path.join(tmpdir, "a.out"))
113 113 return True
114 114 except Exception:
115 115 return False
116 116 finally:
117 117 if oldstderr is not None:
118 118 os.dup2(oldstderr, sys.stderr.fileno())
119 119 if devnull is not None:
120 120 devnull.close()
121 121 shutil.rmtree(tmpdir)
122 122
123 123 # py2exe needs to be installed to work
124 124 try:
125 125 import py2exe
126 126 py2exe.Distribution # silence unused import warning
127 127 py2exeloaded = True
128 128 # import py2exe's patched Distribution class
129 129 from distutils.core import Distribution
130 130 except ImportError:
131 131 py2exeloaded = False
132 132
133 133 def runcmd(cmd, env):
134 134 if (sys.platform == 'plan9'
135 135 and (sys.version_info[0] == 2 and sys.version_info[1] < 7)):
136 136 # subprocess kludge to work around issues in half-baked Python
137 137 # ports, notably bichued/python:
138 138 _, out, err = os.popen3(cmd)
139 139 return str(out), str(err)
140 140 else:
141 141 p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
142 142 stderr=subprocess.PIPE, env=env)
143 143 out, err = p.communicate()
144 144 return out, err
145 145
146 146 def runhg(cmd, env):
147 147 out, err = runcmd(cmd, env)
148 148 # If root is executing setup.py, but the repository is owned by
149 149 # another user (as in "sudo python setup.py install") we will get
150 150 # trust warnings since the .hg/hgrc file is untrusted. That is
151 151 # fine, we don't want to load it anyway. Python may warn about
152 152 # a missing __init__.py in mercurial/locale, we also ignore that.
153 153 err = [e for e in err.splitlines()
154 154 if not e.startswith(b'not trusting file') \
155 155 and not e.startswith(b'warning: Not importing') \
156 156 and not e.startswith(b'obsolete feature not enabled')]
157 157 if err:
158 158 printf("stderr from '%s':" % (' '.join(cmd)), file=sys.stderr)
159 159 printf(b'\n'.join([b' ' + e for e in err]), file=sys.stderr)
160 160 return ''
161 161 return out
162 162
163 163 version = ''
164 164
165 165 # Execute hg out of this directory with a custom environment which takes care
166 166 # to not use any hgrc files and do no localization.
167 167 env = {'HGMODULEPOLICY': 'py',
168 168 'HGRCPATH': '',
169 169 'LANGUAGE': 'C'}
170 170 if 'LD_LIBRARY_PATH' in os.environ:
171 171 env['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH']
172 172 if 'SystemRoot' in os.environ:
173 173 # Copy SystemRoot into the custom environment for Python 2.6
174 174 # under Windows. Otherwise, the subprocess will fail with
175 175 # error 0xc0150004. See: http://bugs.python.org/issue3440
176 176 env['SystemRoot'] = os.environ['SystemRoot']
177 177
178 178 if os.path.isdir('.hg'):
179 179 cmd = [sys.executable, 'hg', 'log', '-r', '.', '--template', '{tags}\n']
180 180 numerictags = [t for t in runhg(cmd, env).split() if t[0].isdigit()]
181 181 hgid = runhg([sys.executable, 'hg', 'id', '-i'], env).strip()
182 182 if numerictags: # tag(s) found
183 183 version = numerictags[-1]
184 184 if hgid.endswith('+'): # propagate the dirty status to the tag
185 185 version += '+'
186 186 else: # no tag found
187 187 ltagcmd = [sys.executable, 'hg', 'parents', '--template',
188 188 '{latesttag}']
189 189 ltag = runhg(ltagcmd, env)
190 190 changessincecmd = [sys.executable, 'hg', 'log', '-T', 'x\n', '-r',
191 191 "only(.,'%s')" % ltag]
192 192 changessince = len(runhg(changessincecmd, env).splitlines())
193 193 version = '%s+%s-%s' % (ltag, changessince, hgid)
194 194 if version.endswith('+'):
195 195 version += time.strftime('%Y%m%d')
196 196 elif os.path.exists('.hg_archival.txt'):
197 197 kw = dict([[t.strip() for t in l.split(':', 1)]
198 198 for l in open('.hg_archival.txt')])
199 199 if 'tag' in kw:
200 200 version = kw['tag']
201 201 elif 'latesttag' in kw:
202 202 if 'changessincelatesttag' in kw:
203 203 version = '%(latesttag)s+%(changessincelatesttag)s-%(node).12s' % kw
204 204 else:
205 205 version = '%(latesttag)s+%(latesttagdistance)s-%(node).12s' % kw
206 206 else:
207 207 version = kw.get('node', '')[:12]
208 208
209 209 if version:
210 210 with open("mercurial/__version__.py", "w") as f:
211 211 f.write('# this file is autogenerated by setup.py\n')
212 212 f.write('version = "%s"\n' % version)
213 213
214 214 try:
215 215 oldpolicy = os.environ.get('HGMODULEPOLICY', None)
216 216 os.environ['HGMODULEPOLICY'] = 'py'
217 217 from mercurial import __version__
218 218 version = __version__.version
219 219 except ImportError:
220 220 version = 'unknown'
221 221 finally:
222 222 if oldpolicy is None:
223 223 del os.environ['HGMODULEPOLICY']
224 224 else:
225 225 os.environ['HGMODULEPOLICY'] = oldpolicy
226 226
227 227 class hgbuild(build):
228 228 # Insert hgbuildmo first so that files in mercurial/locale/ are found
229 229 # when build_py is run next.
230 230 sub_commands = [('build_mo', None)] + build.sub_commands
231 231
232 232 class hgbuildmo(build):
233 233
234 234 description = "build translations (.mo files)"
235 235
236 236 def run(self):
237 237 if not find_executable('msgfmt'):
238 238 self.warn("could not find msgfmt executable, no translations "
239 239 "will be built")
240 240 return
241 241
242 242 podir = 'i18n'
243 243 if not os.path.isdir(podir):
244 244 self.warn("could not find %s/ directory" % podir)
245 245 return
246 246
247 247 join = os.path.join
248 248 for po in os.listdir(podir):
249 249 if not po.endswith('.po'):
250 250 continue
251 251 pofile = join(podir, po)
252 252 modir = join('locale', po[:-3], 'LC_MESSAGES')
253 253 mofile = join(modir, 'hg.mo')
254 254 mobuildfile = join('mercurial', mofile)
255 255 cmd = ['msgfmt', '-v', '-o', mobuildfile, pofile]
256 256 if sys.platform != 'sunos5':
257 257 # msgfmt on Solaris does not know about -c
258 258 cmd.append('-c')
259 259 self.mkpath(join('mercurial', modir))
260 260 self.make_file([pofile], mobuildfile, spawn, (cmd,))
261 261
262 262
263 263 class hgdist(Distribution):
264 264 pure = ispypy
265 265
266 266 global_options = Distribution.global_options + \
267 267 [('pure', None, "use pure (slow) Python "
268 268 "code instead of C extensions"),
269 269 ]
270 270
271 271 def has_ext_modules(self):
272 272 # self.ext_modules is emptied in hgbuildpy.finalize_options which is
273 273 # too late for some cases
274 274 return not self.pure and Distribution.has_ext_modules(self)
275 275
276 276 class hgbuildext(build_ext):
277 277
278 278 def build_extension(self, ext):
279 279 try:
280 280 build_ext.build_extension(self, ext)
281 281 except CCompilerError:
282 282 if not getattr(ext, 'optional', False):
283 283 raise
284 284 log.warn("Failed to build optional extension '%s' (skipping)",
285 285 ext.name)
286 286
287 287 class hgbuildscripts(build_scripts):
288 288 def run(self):
289 289 if os.name != 'nt' or self.distribution.pure:
290 290 return build_scripts.run(self)
291 291
292 292 exebuilt = False
293 293 try:
294 294 self.run_command('build_hgexe')
295 295 exebuilt = True
296 296 except (DistutilsError, CCompilerError):
297 297 log.warn('failed to build optional hg.exe')
298 298
299 299 if exebuilt:
300 300 # Copying hg.exe to the scripts build directory ensures it is
301 301 # installed by the install_scripts command.
302 302 hgexecommand = self.get_finalized_command('build_hgexe')
303 303 dest = os.path.join(self.build_dir, 'hg.exe')
304 304 self.mkpath(self.build_dir)
305 305 self.copy_file(hgexecommand.hgexepath, dest)
306 306
307 307 # Remove hg.bat because it is redundant with hg.exe.
308 308 self.scripts.remove('contrib/win32/hg.bat')
309 309
310 310 return build_scripts.run(self)
311 311
312 312 class hgbuildpy(build_py):
313 313 def finalize_options(self):
314 314 build_py.finalize_options(self)
315 315
316 316 if self.distribution.pure:
317 317 self.distribution.ext_modules = []
318 318 else:
319 319 h = os.path.join(get_python_inc(), 'Python.h')
320 320 if not os.path.exists(h):
321 321 raise SystemExit('Python headers are required to build '
322 322 'Mercurial but weren\'t found in %s' % h)
323 323
324 324 def run(self):
325 325 if self.distribution.pure:
326 326 modulepolicy = 'py'
327 327 else:
328 328 modulepolicy = 'c'
329 329 with open("mercurial/__modulepolicy__.py", "w") as f:
330 330 f.write('# this file is autogenerated by setup.py\n')
331 331 f.write('modulepolicy = "%s"\n' % modulepolicy)
332 332
333 333 build_py.run(self)
334 334
335 335 class buildhgextindex(Command):
336 336 description = 'generate prebuilt index of hgext (for frozen package)'
337 337 user_options = []
338 338 _indexfilename = 'hgext/__index__.py'
339 339
340 340 def initialize_options(self):
341 341 pass
342 342
343 343 def finalize_options(self):
344 344 pass
345 345
346 346 def run(self):
347 347 if os.path.exists(self._indexfilename):
348 348 with open(self._indexfilename, 'w') as f:
349 349 f.write('# empty\n')
350 350
351 351 # here no extension enabled, disabled() lists up everything
352 352 code = ('import pprint; from mercurial import extensions; '
353 353 'pprint.pprint(extensions.disabled())')
354 354 out, err = runcmd([sys.executable, '-c', code], env)
355 355 if err:
356 356 raise DistutilsExecError(err)
357 357
358 358 with open(self._indexfilename, 'w') as f:
359 359 f.write('# this file is autogenerated by setup.py\n')
360 360 f.write('docs = ')
361 361 f.write(out)
362 362
363 363 class buildhgexe(build_ext):
364 364 description = 'compile hg.exe from mercurial/exewrapper.c'
365 365
366 366 def build_extensions(self):
367 367 if os.name != 'nt':
368 368 return
369 369 if isinstance(self.compiler, HackedMingw32CCompiler):
370 370 self.compiler.compiler_so = self.compiler.compiler # no -mdll
371 371 self.compiler.dll_libraries = [] # no -lmsrvc90
372 372 hv = sys.hexversion
373 373 pythonlib = 'python%d%d' % (hv >> 24, (hv >> 16) & 0xff)
374 374 with open('mercurial/hgpythonlib.h', 'wb') as f:
375 375 f.write('/* this file is autogenerated by setup.py */\n')
376 376 f.write('#define HGPYTHONLIB "%s"\n' % pythonlib)
377 377 objects = self.compiler.compile(['mercurial/exewrapper.c'],
378 378 output_dir=self.build_temp)
379 379 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
380 380 target = os.path.join(dir, 'hg')
381 381 self.compiler.link_executable(objects, target,
382 382 libraries=[],
383 383 output_dir=self.build_temp)
384 384
385 385 @property
386 386 def hgexepath(self):
387 387 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
388 388 return os.path.join(self.build_temp, dir, 'hg.exe')
389 389
390 390 class hginstalllib(install_lib):
391 391 '''
392 392 This is a specialization of install_lib that replaces the copy_file used
393 393 there so that it supports setting the mode of files after copying them,
394 394 instead of just preserving the mode that the files originally had. If your
395 395 system has a umask of something like 027, preserving the permissions when
396 396 copying will lead to a broken install.
397 397
398 398 Note that just passing keep_permissions=False to copy_file would be
399 399 insufficient, as it might still be applying a umask.
400 400 '''
401 401
402 402 def run(self):
403 403 realcopyfile = file_util.copy_file
404 404 def copyfileandsetmode(*args, **kwargs):
405 405 src, dst = args[0], args[1]
406 406 dst, copied = realcopyfile(*args, **kwargs)
407 407 if copied:
408 408 st = os.stat(src)
409 409 # Persist executable bit (apply it to group and other if user
410 410 # has it)
411 411 if st[stat.ST_MODE] & stat.S_IXUSR:
412 412 setmode = int('0755', 8)
413 413 else:
414 414 setmode = int('0644', 8)
415 415 m = stat.S_IMODE(st[stat.ST_MODE])
416 416 m = (m & ~int('0777', 8)) | setmode
417 417 os.chmod(dst, m)
418 418 file_util.copy_file = copyfileandsetmode
419 419 try:
420 420 install_lib.run(self)
421 421 finally:
422 422 file_util.copy_file = realcopyfile
423 423
424 424 class hginstallscripts(install_scripts):
425 425 '''
426 426 This is a specialization of install_scripts that replaces the @LIBDIR@ with
427 427 the configured directory for modules. If possible, the path is made relative
428 428 to the directory for scripts.
429 429 '''
430 430
431 431 def initialize_options(self):
432 432 install_scripts.initialize_options(self)
433 433
434 434 self.install_lib = None
435 435
436 436 def finalize_options(self):
437 437 install_scripts.finalize_options(self)
438 438 self.set_undefined_options('install',
439 439 ('install_lib', 'install_lib'))
440 440
441 441 def run(self):
442 442 install_scripts.run(self)
443 443
444 444 # It only makes sense to replace @LIBDIR@ with the install path if
445 445 # the install path is known. For wheels, the logic below calculates
446 446 # the libdir to be "../..". This is because the internal layout of a
447 447 # wheel archive looks like:
448 448 #
449 449 # mercurial-3.6.1.data/scripts/hg
450 450 # mercurial/__init__.py
451 451 #
452 452 # When installing wheels, the subdirectories of the "<pkg>.data"
453 453 # directory are translated to system local paths and files therein
454 454 # are copied in place. The mercurial/* files are installed into the
455 455 # site-packages directory. However, the site-packages directory
456 456 # isn't known until wheel install time. This means we have no clue
457 457 # at wheel generation time what the installed site-packages directory
458 458 # will be. And, wheels don't appear to provide the ability to register
459 459 # custom code to run during wheel installation. This all means that
460 460 # we can't reliably set the libdir in wheels: the default behavior
461 461 # of looking in sys.path must do.
462 462
463 463 if (os.path.splitdrive(self.install_dir)[0] !=
464 464 os.path.splitdrive(self.install_lib)[0]):
465 465 # can't make relative paths from one drive to another, so use an
466 466 # absolute path instead
467 467 libdir = self.install_lib
468 468 else:
469 469 common = os.path.commonprefix((self.install_dir, self.install_lib))
470 470 rest = self.install_dir[len(common):]
471 471 uplevel = len([n for n in os.path.split(rest) if n])
472 472
473 473 libdir = uplevel * ('..' + os.sep) + self.install_lib[len(common):]
474 474
475 475 for outfile in self.outfiles:
476 476 with open(outfile, 'rb') as fp:
477 477 data = fp.read()
478 478
479 479 # skip binary files
480 480 if b'\0' in data:
481 481 continue
482 482
483 483 # During local installs, the shebang will be rewritten to the final
484 484 # install path. During wheel packaging, the shebang has a special
485 485 # value.
486 486 if data.startswith(b'#!python'):
487 487 log.info('not rewriting @LIBDIR@ in %s because install path '
488 488 'not known' % outfile)
489 489 continue
490 490
491 491 data = data.replace(b'@LIBDIR@', libdir.encode(libdir_escape))
492 492 with open(outfile, 'wb') as fp:
493 493 fp.write(data)
494 494
495 495 cmdclass = {'build': hgbuild,
496 496 'build_mo': hgbuildmo,
497 497 'build_ext': hgbuildext,
498 498 'build_py': hgbuildpy,
499 499 'build_scripts': hgbuildscripts,
500 500 'build_hgextindex': buildhgextindex,
501 501 'install_lib': hginstalllib,
502 502 'install_scripts': hginstallscripts,
503 503 'build_hgexe': buildhgexe,
504 504 }
505 505
506 506 packages = ['mercurial', 'mercurial.hgweb', 'mercurial.httpclient',
507 507 'mercurial.pure',
508 508 'hgext', 'hgext.convert', 'hgext.highlight', 'hgext.zeroconf',
509 'hgext.largefiles']
509 'hgext.largefiles', 'hgext3rd']
510 510
511 511 common_depends = ['mercurial/util.h']
512 512
513 513 osutil_ldflags = []
514 514
515 515 if sys.platform == 'darwin':
516 516 osutil_ldflags += ['-framework', 'ApplicationServices']
517 517
518 518 extmodules = [
519 519 Extension('mercurial.base85', ['mercurial/base85.c'],
520 520 depends=common_depends),
521 521 Extension('mercurial.bdiff', ['mercurial/bdiff.c'],
522 522 depends=common_depends),
523 523 Extension('mercurial.diffhelpers', ['mercurial/diffhelpers.c'],
524 524 depends=common_depends),
525 525 Extension('mercurial.mpatch', ['mercurial/mpatch.c'],
526 526 depends=common_depends),
527 527 Extension('mercurial.parsers', ['mercurial/dirs.c',
528 528 'mercurial/manifest.c',
529 529 'mercurial/parsers.c',
530 530 'mercurial/pathencode.c'],
531 531 depends=common_depends),
532 532 Extension('mercurial.osutil', ['mercurial/osutil.c'],
533 533 extra_link_args=osutil_ldflags,
534 534 depends=common_depends),
535 535 Extension('hgext.fsmonitor.pywatchman.bser',
536 536 ['hgext/fsmonitor/pywatchman/bser.c']),
537 537 ]
538 538
539 539 try:
540 540 from distutils import cygwinccompiler
541 541
542 542 # the -mno-cygwin option has been deprecated for years
543 543 compiler = cygwinccompiler.Mingw32CCompiler
544 544
545 545 class HackedMingw32CCompiler(cygwinccompiler.Mingw32CCompiler):
546 546 def __init__(self, *args, **kwargs):
547 547 compiler.__init__(self, *args, **kwargs)
548 548 for i in 'compiler compiler_so linker_exe linker_so'.split():
549 549 try:
550 550 getattr(self, i).remove('-mno-cygwin')
551 551 except ValueError:
552 552 pass
553 553
554 554 cygwinccompiler.Mingw32CCompiler = HackedMingw32CCompiler
555 555 except ImportError:
556 556 # the cygwinccompiler package is not available on some Python
557 557 # distributions like the ones from the optware project for Synology
558 558 # DiskStation boxes
559 559 class HackedMingw32CCompiler(object):
560 560 pass
561 561
562 562 packagedata = {'mercurial': ['locale/*/LC_MESSAGES/hg.mo',
563 563 'help/*.txt',
564 564 'help/internals/*.txt',
565 565 'default.d/*.rc',
566 566 'dummycert.pem']}
567 567
568 568 def ordinarypath(p):
569 569 return p and p[0] != '.' and p[-1] != '~'
570 570
571 571 for root in ('templates',):
572 572 for curdir, dirs, files in os.walk(os.path.join('mercurial', root)):
573 573 curdir = curdir.split(os.sep, 1)[1]
574 574 dirs[:] = filter(ordinarypath, dirs)
575 575 for f in filter(ordinarypath, files):
576 576 f = os.path.join(curdir, f)
577 577 packagedata['mercurial'].append(f)
578 578
579 579 datafiles = []
580 580 setupversion = version
581 581 extra = {}
582 582
583 583 if py2exeloaded:
584 584 extra['console'] = [
585 585 {'script':'hg',
586 586 'copyright':'Copyright (C) 2005-2016 Matt Mackall and others',
587 587 'product_version':version}]
588 588 # sub command of 'build' because 'py2exe' does not handle sub_commands
589 589 build.sub_commands.insert(0, ('build_hgextindex', None))
590 590 # put dlls in sub directory so that they won't pollute PATH
591 591 extra['zipfile'] = 'lib/library.zip'
592 592
593 593 if os.name == 'nt':
594 594 # Windows binary file versions for exe/dll files must have the
595 595 # form W.X.Y.Z, where W,X,Y,Z are numbers in the range 0..65535
596 596 setupversion = version.split('+', 1)[0]
597 597
598 598 if sys.platform == 'darwin' and os.path.exists('/usr/bin/xcodebuild'):
599 599 version = runcmd(['/usr/bin/xcodebuild', '-version'], {})[0].splitlines()
600 600 if version:
601 601 version = version[0]
602 602 if sys.version_info[0] == 3:
603 603 version = version.decode('utf-8')
604 604 xcode4 = (version.startswith('Xcode') and
605 605 StrictVersion(version.split()[1]) >= StrictVersion('4.0'))
606 606 xcode51 = re.match(r'^Xcode\s+5\.1', version) is not None
607 607 else:
608 608 # xcodebuild returns empty on OS X Lion with XCode 4.3 not
609 609 # installed, but instead with only command-line tools. Assume
610 610 # that only happens on >= Lion, thus no PPC support.
611 611 xcode4 = True
612 612 xcode51 = False
613 613
614 614 # XCode 4.0 dropped support for ppc architecture, which is hardcoded in
615 615 # distutils.sysconfig
616 616 if xcode4:
617 617 os.environ['ARCHFLAGS'] = ''
618 618
619 619 # XCode 5.1 changes clang such that it now fails to compile if the
620 620 # -mno-fused-madd flag is passed, but the version of Python shipped with
621 621 # OS X 10.9 Mavericks includes this flag. This causes problems in all
622 622 # C extension modules, and a bug has been filed upstream at
623 623 # http://bugs.python.org/issue21244. We also need to patch this here
624 624 # so Mercurial can continue to compile in the meantime.
625 625 if xcode51:
626 626 cflags = get_config_var('CFLAGS')
627 627 if cflags and re.search(r'-mno-fused-madd\b', cflags) is not None:
628 628 os.environ['CFLAGS'] = (
629 629 os.environ.get('CFLAGS', '') + ' -Qunused-arguments')
630 630
631 631 setup(name='mercurial',
632 632 version=setupversion,
633 633 author='Matt Mackall and many others',
634 634 author_email='mercurial@selenic.com',
635 635 url='https://mercurial-scm.org/',
636 636 download_url='https://mercurial-scm.org/release/',
637 637 description=('Fast scalable distributed SCM (revision control, version '
638 638 'control) system'),
639 639 long_description=('Mercurial is a distributed SCM tool written in Python.'
640 640 ' It is used by a number of large projects that require'
641 641 ' fast, reliable distributed revision control, such as '
642 642 'Mozilla.'),
643 643 license='GNU GPLv2 or any later version',
644 644 classifiers=[
645 645 'Development Status :: 6 - Mature',
646 646 'Environment :: Console',
647 647 'Intended Audience :: Developers',
648 648 'Intended Audience :: System Administrators',
649 649 'License :: OSI Approved :: GNU General Public License (GPL)',
650 650 'Natural Language :: Danish',
651 651 'Natural Language :: English',
652 652 'Natural Language :: German',
653 653 'Natural Language :: Italian',
654 654 'Natural Language :: Japanese',
655 655 'Natural Language :: Portuguese (Brazilian)',
656 656 'Operating System :: Microsoft :: Windows',
657 657 'Operating System :: OS Independent',
658 658 'Operating System :: POSIX',
659 659 'Programming Language :: C',
660 660 'Programming Language :: Python',
661 661 'Topic :: Software Development :: Version Control',
662 662 ],
663 663 scripts=scripts,
664 664 packages=packages,
665 665 ext_modules=extmodules,
666 666 data_files=datafiles,
667 667 package_data=packagedata,
668 668 cmdclass=cmdclass,
669 669 distclass=hgdist,
670 670 options={'py2exe': {'packages': ['hgext', 'email']},
671 671 'bdist_mpkg': {'zipdist': False,
672 672 'license': 'COPYING',
673 673 'readme': 'contrib/macosx/Readme.html',
674 674 'welcome': 'contrib/macosx/Welcome.html',
675 675 },
676 676 },
677 677 **extra)
@@ -1,70 +1,73 b''
1 1 $ echo 'raise Exception("bit bucket overflow")' > badext.py
2 2 $ abspathexc=`pwd`/badext.py
3 3
4 4 $ cat >baddocext.py <<EOF
5 5 > """
6 6 > baddocext is bad
7 7 > """
8 8 > EOF
9 9 $ abspathdoc=`pwd`/baddocext.py
10 10
11 11 $ cat <<EOF >> $HGRCPATH
12 12 > [extensions]
13 13 > gpg =
14 14 > hgext.gpg =
15 15 > badext = $abspathexc
16 16 > baddocext = $abspathdoc
17 17 > badext2 =
18 18 > EOF
19 19
20 20 $ hg -q help help 2>&1 |grep extension
21 21 *** failed to import extension badext from $TESTTMP/badext.py: bit bucket overflow
22 22 *** failed to import extension badext2: No module named badext2
23 23
24 24 show traceback
25 25
26 26 $ hg -q help help --traceback 2>&1 | egrep ' extension|^Exception|Traceback|ImportError'
27 27 *** failed to import extension badext from $TESTTMP/badext.py: bit bucket overflow
28 28 Traceback (most recent call last):
29 29 Exception: bit bucket overflow
30 30 *** failed to import extension badext2: No module named badext2
31 31 Traceback (most recent call last):
32 32 ImportError: No module named badext2
33 33
34 34 names of extensions failed to load can be accessed via extensions.notloaded()
35 35
36 36 $ cat <<EOF > showbadexts.py
37 37 > from mercurial import cmdutil, commands, extensions
38 38 > cmdtable = {}
39 39 > command = cmdutil.command(cmdtable)
40 40 > @command('showbadexts', norepo=True)
41 41 > def showbadexts(ui, *pats, **opts):
42 42 > ui.write('BADEXTS: %s\n' % ' '.join(sorted(extensions.notloaded())))
43 43 > EOF
44 44 $ hg --config extensions.badexts=showbadexts.py showbadexts 2>&1 | grep '^BADEXTS'
45 45 BADEXTS: badext badext2
46 46
47 47 show traceback for ImportError of hgext.name if debug is set
48 48 (note that --debug option isn't applied yet when loading extensions)
49 49
50 50 $ (hg -q help help --traceback --config ui.debug=True 2>&1) \
51 51 > | grep -v '^ ' \
52 52 > | egrep 'extension..[^p]|^Exception|Traceback|ImportError|not import'
53 53 *** failed to import extension badext from $TESTTMP/badext.py: bit bucket overflow
54 54 Traceback (most recent call last):
55 55 Exception: bit bucket overflow
56 56 could not import hgext.badext2 (No module named *badext2): trying badext2 (glob)
57 57 Traceback (most recent call last):
58 58 ImportError: No module named *badext2 (glob)
59 could not import hgext3rd.badext2 (No module named badext2): trying badext2
60 Traceback (most recent call last):
61 ImportError: No module named badext2
59 62 *** failed to import extension badext2: No module named badext2
60 63 Traceback (most recent call last):
61 64 ImportError: No module named badext2
62 65
63 66 confirm that there's no crash when an extension's documentation is bad
64 67
65 68 $ hg help --keyword baddocext
66 69 *** failed to import extension badext from $TESTTMP/badext.py: bit bucket overflow
67 70 *** failed to import extension badext2: No module named badext2
68 71 Topics:
69 72
70 73 extensions Using Additional Features
General Comments 0
You need to be logged in to leave comments. Login now