##// END OF EJS Templates
setup.py: attempt to build and install hg.exe on Windows...
Gregory Szorc -
r27268:ed1660ce default
parent child Browse files
Show More
@@ -1,650 +1,687 b''
1 #
1 #
2 # This is the mercurial setup script.
2 # This is the mercurial setup script.
3 #
3 #
4 # 'python setup.py install', or
4 # 'python setup.py install', or
5 # 'python setup.py --help' for more options
5 # 'python setup.py --help' for more options
6
6
7 import sys, platform
7 import sys, platform
8 if getattr(sys, 'version_info', (0, 0, 0)) < (2, 6, 0, 'final'):
8 if getattr(sys, 'version_info', (0, 0, 0)) < (2, 6, 0, 'final'):
9 raise SystemExit("Mercurial requires Python 2.6 or later.")
9 raise SystemExit("Mercurial requires Python 2.6 or later.")
10
10
11 if sys.version_info[0] >= 3:
11 if sys.version_info[0] >= 3:
12 def b(s):
12 def b(s):
13 '''A helper function to emulate 2.6+ bytes literals using string
13 '''A helper function to emulate 2.6+ bytes literals using string
14 literals.'''
14 literals.'''
15 return s.encode('latin1')
15 return s.encode('latin1')
16 printf = eval('print')
16 printf = eval('print')
17 libdir_escape = 'unicode_escape'
17 libdir_escape = 'unicode_escape'
18 else:
18 else:
19 libdir_escape = 'string_escape'
19 libdir_escape = 'string_escape'
20 def b(s):
20 def b(s):
21 '''A helper function to emulate 2.6+ bytes literals using string
21 '''A helper function to emulate 2.6+ bytes literals using string
22 literals.'''
22 literals.'''
23 return s
23 return s
24 def printf(*args, **kwargs):
24 def printf(*args, **kwargs):
25 f = kwargs.get('file', sys.stdout)
25 f = kwargs.get('file', sys.stdout)
26 end = kwargs.get('end', '\n')
26 end = kwargs.get('end', '\n')
27 f.write(b(' ').join(args) + end)
27 f.write(b(' ').join(args) + end)
28
28
29 # Solaris Python packaging brain damage
29 # Solaris Python packaging brain damage
30 try:
30 try:
31 import hashlib
31 import hashlib
32 sha = hashlib.sha1()
32 sha = hashlib.sha1()
33 except ImportError:
33 except ImportError:
34 try:
34 try:
35 import sha
35 import sha
36 sha.sha # silence unused import warning
36 sha.sha # silence unused import warning
37 except ImportError:
37 except ImportError:
38 raise SystemExit(
38 raise SystemExit(
39 "Couldn't import standard hashlib (incomplete Python install).")
39 "Couldn't import standard hashlib (incomplete Python install).")
40
40
41 try:
41 try:
42 import zlib
42 import zlib
43 zlib.compressobj # silence unused import warning
43 zlib.compressobj # silence unused import warning
44 except ImportError:
44 except ImportError:
45 raise SystemExit(
45 raise SystemExit(
46 "Couldn't import standard zlib (incomplete Python install).")
46 "Couldn't import standard zlib (incomplete Python install).")
47
47
48 # The base IronPython distribution (as of 2.7.1) doesn't support bz2
48 # The base IronPython distribution (as of 2.7.1) doesn't support bz2
49 isironpython = False
49 isironpython = False
50 try:
50 try:
51 isironpython = (platform.python_implementation()
51 isironpython = (platform.python_implementation()
52 .lower().find("ironpython") != -1)
52 .lower().find("ironpython") != -1)
53 except AttributeError:
53 except AttributeError:
54 pass
54 pass
55
55
56 if isironpython:
56 if isironpython:
57 sys.stderr.write("warning: IronPython detected (no bz2 support)\n")
57 sys.stderr.write("warning: IronPython detected (no bz2 support)\n")
58 else:
58 else:
59 try:
59 try:
60 import bz2
60 import bz2
61 bz2.BZ2Compressor # silence unused import warning
61 bz2.BZ2Compressor # silence unused import warning
62 except ImportError:
62 except ImportError:
63 raise SystemExit(
63 raise SystemExit(
64 "Couldn't import standard bz2 (incomplete Python install).")
64 "Couldn't import standard bz2 (incomplete Python install).")
65
65
66 ispypy = "PyPy" in sys.version
66 ispypy = "PyPy" in sys.version
67
67
68 import os, stat, subprocess, time
68 import os, stat, subprocess, time
69 import re
69 import re
70 import shutil
70 import shutil
71 import tempfile
71 import tempfile
72 from distutils import log
72 from distutils import log
73 if 'FORCE_SETUPTOOLS' in os.environ:
73 if 'FORCE_SETUPTOOLS' in os.environ:
74 from setuptools import setup
74 from setuptools import setup
75 else:
75 else:
76 from distutils.core import setup
76 from distutils.core import setup
77 from distutils.core import Command, Extension
77 from distutils.core import Command, Extension
78 from distutils.dist import Distribution
78 from distutils.dist import Distribution
79 from distutils.command.build import build
79 from distutils.command.build import build
80 from distutils.command.build_ext import build_ext
80 from distutils.command.build_ext import build_ext
81 from distutils.command.build_py import build_py
81 from distutils.command.build_py import build_py
82 from distutils.command.build_scripts import build_scripts
82 from distutils.command.install_lib import install_lib
83 from distutils.command.install_lib import install_lib
83 from distutils.command.install_scripts import install_scripts
84 from distutils.command.install_scripts import install_scripts
84 from distutils.spawn import spawn, find_executable
85 from distutils.spawn import spawn, find_executable
85 from distutils import file_util
86 from distutils import file_util
86 from distutils.errors import CCompilerError, DistutilsExecError
87 from distutils.errors import (
88 CCompilerError,
89 DistutilsError,
90 DistutilsExecError,
91 )
87 from distutils.sysconfig import get_python_inc, get_config_var
92 from distutils.sysconfig import get_python_inc, get_config_var
88 from distutils.version import StrictVersion
93 from distutils.version import StrictVersion
89
94
90 convert2to3 = '--c2to3' in sys.argv
95 convert2to3 = '--c2to3' in sys.argv
91 if convert2to3:
96 if convert2to3:
92 try:
97 try:
93 from distutils.command.build_py import build_py_2to3 as build_py
98 from distutils.command.build_py import build_py_2to3 as build_py
94 from lib2to3.refactor import get_fixers_from_package as getfixers
99 from lib2to3.refactor import get_fixers_from_package as getfixers
95 except ImportError:
100 except ImportError:
96 if sys.version_info[0] < 3:
101 if sys.version_info[0] < 3:
97 raise SystemExit("--c2to3 is only compatible with python3.")
102 raise SystemExit("--c2to3 is only compatible with python3.")
98 raise
103 raise
99 sys.path.append('contrib')
104 sys.path.append('contrib')
100 elif sys.version_info[0] >= 3:
105 elif sys.version_info[0] >= 3:
101 raise SystemExit("setup.py with python3 needs --c2to3 (experimental)")
106 raise SystemExit("setup.py with python3 needs --c2to3 (experimental)")
102
107
103 scripts = ['hg']
108 scripts = ['hg']
104 if os.name == 'nt':
109 if os.name == 'nt':
110 # We remove hg.bat if we are able to build hg.exe.
105 scripts.append('contrib/win32/hg.bat')
111 scripts.append('contrib/win32/hg.bat')
106
112
107 # simplified version of distutils.ccompiler.CCompiler.has_function
113 # simplified version of distutils.ccompiler.CCompiler.has_function
108 # that actually removes its temporary files.
114 # that actually removes its temporary files.
109 def hasfunction(cc, funcname):
115 def hasfunction(cc, funcname):
110 tmpdir = tempfile.mkdtemp(prefix='hg-install-')
116 tmpdir = tempfile.mkdtemp(prefix='hg-install-')
111 devnull = oldstderr = None
117 devnull = oldstderr = None
112 try:
118 try:
113 fname = os.path.join(tmpdir, 'funcname.c')
119 fname = os.path.join(tmpdir, 'funcname.c')
114 f = open(fname, 'w')
120 f = open(fname, 'w')
115 f.write('int main(void) {\n')
121 f.write('int main(void) {\n')
116 f.write(' %s();\n' % funcname)
122 f.write(' %s();\n' % funcname)
117 f.write('}\n')
123 f.write('}\n')
118 f.close()
124 f.close()
119 # Redirect stderr to /dev/null to hide any error messages
125 # Redirect stderr to /dev/null to hide any error messages
120 # from the compiler.
126 # from the compiler.
121 # This will have to be changed if we ever have to check
127 # This will have to be changed if we ever have to check
122 # for a function on Windows.
128 # for a function on Windows.
123 devnull = open('/dev/null', 'w')
129 devnull = open('/dev/null', 'w')
124 oldstderr = os.dup(sys.stderr.fileno())
130 oldstderr = os.dup(sys.stderr.fileno())
125 os.dup2(devnull.fileno(), sys.stderr.fileno())
131 os.dup2(devnull.fileno(), sys.stderr.fileno())
126 objects = cc.compile([fname], output_dir=tmpdir)
132 objects = cc.compile([fname], output_dir=tmpdir)
127 cc.link_executable(objects, os.path.join(tmpdir, "a.out"))
133 cc.link_executable(objects, os.path.join(tmpdir, "a.out"))
128 return True
134 return True
129 except Exception:
135 except Exception:
130 return False
136 return False
131 finally:
137 finally:
132 if oldstderr is not None:
138 if oldstderr is not None:
133 os.dup2(oldstderr, sys.stderr.fileno())
139 os.dup2(oldstderr, sys.stderr.fileno())
134 if devnull is not None:
140 if devnull is not None:
135 devnull.close()
141 devnull.close()
136 shutil.rmtree(tmpdir)
142 shutil.rmtree(tmpdir)
137
143
138 # py2exe needs to be installed to work
144 # py2exe needs to be installed to work
139 try:
145 try:
140 import py2exe
146 import py2exe
141 py2exe.Distribution # silence unused import warning
147 py2exe.Distribution # silence unused import warning
142 py2exeloaded = True
148 py2exeloaded = True
143 # import py2exe's patched Distribution class
149 # import py2exe's patched Distribution class
144 from distutils.core import Distribution
150 from distutils.core import Distribution
145 except ImportError:
151 except ImportError:
146 py2exeloaded = False
152 py2exeloaded = False
147
153
148 def runcmd(cmd, env):
154 def runcmd(cmd, env):
149 if (sys.platform == 'plan9'
155 if (sys.platform == 'plan9'
150 and (sys.version_info[0] == 2 and sys.version_info[1] < 7)):
156 and (sys.version_info[0] == 2 and sys.version_info[1] < 7)):
151 # subprocess kludge to work around issues in half-baked Python
157 # subprocess kludge to work around issues in half-baked Python
152 # ports, notably bichued/python:
158 # ports, notably bichued/python:
153 _, out, err = os.popen3(cmd)
159 _, out, err = os.popen3(cmd)
154 return str(out), str(err)
160 return str(out), str(err)
155 else:
161 else:
156 p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
162 p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
157 stderr=subprocess.PIPE, env=env)
163 stderr=subprocess.PIPE, env=env)
158 out, err = p.communicate()
164 out, err = p.communicate()
159 return out, err
165 return out, err
160
166
161 def runhg(cmd, env):
167 def runhg(cmd, env):
162 out, err = runcmd(cmd, env)
168 out, err = runcmd(cmd, env)
163 # If root is executing setup.py, but the repository is owned by
169 # If root is executing setup.py, but the repository is owned by
164 # another user (as in "sudo python setup.py install") we will get
170 # another user (as in "sudo python setup.py install") we will get
165 # trust warnings since the .hg/hgrc file is untrusted. That is
171 # trust warnings since the .hg/hgrc file is untrusted. That is
166 # fine, we don't want to load it anyway. Python may warn about
172 # fine, we don't want to load it anyway. Python may warn about
167 # a missing __init__.py in mercurial/locale, we also ignore that.
173 # a missing __init__.py in mercurial/locale, we also ignore that.
168 err = [e for e in err.splitlines()
174 err = [e for e in err.splitlines()
169 if not e.startswith(b('not trusting file')) \
175 if not e.startswith(b('not trusting file')) \
170 and not e.startswith(b('warning: Not importing')) \
176 and not e.startswith(b('warning: Not importing')) \
171 and not e.startswith(b('obsolete feature not enabled'))]
177 and not e.startswith(b('obsolete feature not enabled'))]
172 if err:
178 if err:
173 printf("stderr from '%s':" % (' '.join(cmd)), file=sys.stderr)
179 printf("stderr from '%s':" % (' '.join(cmd)), file=sys.stderr)
174 printf(b('\n').join([b(' ') + e for e in err]), file=sys.stderr)
180 printf(b('\n').join([b(' ') + e for e in err]), file=sys.stderr)
175 return ''
181 return ''
176 return out
182 return out
177
183
178 version = ''
184 version = ''
179
185
180 # Execute hg out of this directory with a custom environment which takes care
186 # Execute hg out of this directory with a custom environment which takes care
181 # to not use any hgrc files and do no localization.
187 # to not use any hgrc files and do no localization.
182 env = {'HGMODULEPOLICY': 'py',
188 env = {'HGMODULEPOLICY': 'py',
183 'HGRCPATH': '',
189 'HGRCPATH': '',
184 'LANGUAGE': 'C'}
190 'LANGUAGE': 'C'}
185 if 'LD_LIBRARY_PATH' in os.environ:
191 if 'LD_LIBRARY_PATH' in os.environ:
186 env['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH']
192 env['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH']
187 if 'SystemRoot' in os.environ:
193 if 'SystemRoot' in os.environ:
188 # Copy SystemRoot into the custom environment for Python 2.6
194 # Copy SystemRoot into the custom environment for Python 2.6
189 # under Windows. Otherwise, the subprocess will fail with
195 # under Windows. Otherwise, the subprocess will fail with
190 # error 0xc0150004. See: http://bugs.python.org/issue3440
196 # error 0xc0150004. See: http://bugs.python.org/issue3440
191 env['SystemRoot'] = os.environ['SystemRoot']
197 env['SystemRoot'] = os.environ['SystemRoot']
192
198
193 if os.path.isdir('.hg'):
199 if os.path.isdir('.hg'):
194 cmd = [sys.executable, 'hg', 'log', '-r', '.', '--template', '{tags}\n']
200 cmd = [sys.executable, 'hg', 'log', '-r', '.', '--template', '{tags}\n']
195 numerictags = [t for t in runhg(cmd, env).split() if t[0].isdigit()]
201 numerictags = [t for t in runhg(cmd, env).split() if t[0].isdigit()]
196 hgid = runhg([sys.executable, 'hg', 'id', '-i'], env).strip()
202 hgid = runhg([sys.executable, 'hg', 'id', '-i'], env).strip()
197 if numerictags: # tag(s) found
203 if numerictags: # tag(s) found
198 version = numerictags[-1]
204 version = numerictags[-1]
199 if hgid.endswith('+'): # propagate the dirty status to the tag
205 if hgid.endswith('+'): # propagate the dirty status to the tag
200 version += '+'
206 version += '+'
201 else: # no tag found
207 else: # no tag found
202 ltagcmd = [sys.executable, 'hg', 'parents', '--template',
208 ltagcmd = [sys.executable, 'hg', 'parents', '--template',
203 '{latesttag}']
209 '{latesttag}']
204 ltag = runhg(ltagcmd, env)
210 ltag = runhg(ltagcmd, env)
205 changessincecmd = [sys.executable, 'hg', 'log', '-T', 'x\n', '-r',
211 changessincecmd = [sys.executable, 'hg', 'log', '-T', 'x\n', '-r',
206 "only(.,'%s')" % ltag]
212 "only(.,'%s')" % ltag]
207 changessince = len(runhg(changessincecmd, env).splitlines())
213 changessince = len(runhg(changessincecmd, env).splitlines())
208 version = '%s+%s-%s' % (ltag, changessince, hgid)
214 version = '%s+%s-%s' % (ltag, changessince, hgid)
209 if version.endswith('+'):
215 if version.endswith('+'):
210 version += time.strftime('%Y%m%d')
216 version += time.strftime('%Y%m%d')
211 elif os.path.exists('.hg_archival.txt'):
217 elif os.path.exists('.hg_archival.txt'):
212 kw = dict([[t.strip() for t in l.split(':', 1)]
218 kw = dict([[t.strip() for t in l.split(':', 1)]
213 for l in open('.hg_archival.txt')])
219 for l in open('.hg_archival.txt')])
214 if 'tag' in kw:
220 if 'tag' in kw:
215 version = kw['tag']
221 version = kw['tag']
216 elif 'latesttag' in kw:
222 elif 'latesttag' in kw:
217 if 'changessincelatesttag' in kw:
223 if 'changessincelatesttag' in kw:
218 version = '%(latesttag)s+%(changessincelatesttag)s-%(node).12s' % kw
224 version = '%(latesttag)s+%(changessincelatesttag)s-%(node).12s' % kw
219 else:
225 else:
220 version = '%(latesttag)s+%(latesttagdistance)s-%(node).12s' % kw
226 version = '%(latesttag)s+%(latesttagdistance)s-%(node).12s' % kw
221 else:
227 else:
222 version = kw.get('node', '')[:12]
228 version = kw.get('node', '')[:12]
223
229
224 if version:
230 if version:
225 f = open("mercurial/__version__.py", "w")
231 f = open("mercurial/__version__.py", "w")
226 f.write('# this file is autogenerated by setup.py\n')
232 f.write('# this file is autogenerated by setup.py\n')
227 f.write('version = "%s"\n' % version)
233 f.write('version = "%s"\n' % version)
228 f.close()
234 f.close()
229
235
230
236
231 try:
237 try:
232 from mercurial import __version__
238 from mercurial import __version__
233 version = __version__.version
239 version = __version__.version
234 except ImportError:
240 except ImportError:
235 version = 'unknown'
241 version = 'unknown'
236
242
237 class hgbuild(build):
243 class hgbuild(build):
238 # Insert hgbuildmo first so that files in mercurial/locale/ are found
244 # Insert hgbuildmo first so that files in mercurial/locale/ are found
239 # when build_py is run next.
245 # when build_py is run next.
240 sub_commands = [('build_mo', None),
246 sub_commands = [('build_mo', None),
241
247
242 # We also need build_ext before build_py. Otherwise, when 2to3 is
248 # We also need build_ext before build_py. Otherwise, when 2to3 is
243 # called (in build_py), it will not find osutil & friends,
249 # called (in build_py), it will not find osutil & friends,
244 # thinking that those modules are global and, consequently, making
250 # thinking that those modules are global and, consequently, making
245 # a mess, now that all module imports are global.
251 # a mess, now that all module imports are global.
246
252
247 ('build_ext', build.has_ext_modules),
253 ('build_ext', build.has_ext_modules),
248 ] + build.sub_commands
254 ] + build.sub_commands
249
255
250 class hgbuildmo(build):
256 class hgbuildmo(build):
251
257
252 description = "build translations (.mo files)"
258 description = "build translations (.mo files)"
253
259
254 def run(self):
260 def run(self):
255 if not find_executable('msgfmt'):
261 if not find_executable('msgfmt'):
256 self.warn("could not find msgfmt executable, no translations "
262 self.warn("could not find msgfmt executable, no translations "
257 "will be built")
263 "will be built")
258 return
264 return
259
265
260 podir = 'i18n'
266 podir = 'i18n'
261 if not os.path.isdir(podir):
267 if not os.path.isdir(podir):
262 self.warn("could not find %s/ directory" % podir)
268 self.warn("could not find %s/ directory" % podir)
263 return
269 return
264
270
265 join = os.path.join
271 join = os.path.join
266 for po in os.listdir(podir):
272 for po in os.listdir(podir):
267 if not po.endswith('.po'):
273 if not po.endswith('.po'):
268 continue
274 continue
269 pofile = join(podir, po)
275 pofile = join(podir, po)
270 modir = join('locale', po[:-3], 'LC_MESSAGES')
276 modir = join('locale', po[:-3], 'LC_MESSAGES')
271 mofile = join(modir, 'hg.mo')
277 mofile = join(modir, 'hg.mo')
272 mobuildfile = join('mercurial', mofile)
278 mobuildfile = join('mercurial', mofile)
273 cmd = ['msgfmt', '-v', '-o', mobuildfile, pofile]
279 cmd = ['msgfmt', '-v', '-o', mobuildfile, pofile]
274 if sys.platform != 'sunos5':
280 if sys.platform != 'sunos5':
275 # msgfmt on Solaris does not know about -c
281 # msgfmt on Solaris does not know about -c
276 cmd.append('-c')
282 cmd.append('-c')
277 self.mkpath(join('mercurial', modir))
283 self.mkpath(join('mercurial', modir))
278 self.make_file([pofile], mobuildfile, spawn, (cmd,))
284 self.make_file([pofile], mobuildfile, spawn, (cmd,))
279
285
280
286
281 class hgdist(Distribution):
287 class hgdist(Distribution):
282 pure = ispypy
288 pure = ispypy
283
289
284 global_options = Distribution.global_options + \
290 global_options = Distribution.global_options + \
285 [('pure', None, "use pure (slow) Python "
291 [('pure', None, "use pure (slow) Python "
286 "code instead of C extensions"),
292 "code instead of C extensions"),
287 ('c2to3', None, "(experimental!) convert "
293 ('c2to3', None, "(experimental!) convert "
288 "code with 2to3"),
294 "code with 2to3"),
289 ]
295 ]
290
296
291 def has_ext_modules(self):
297 def has_ext_modules(self):
292 # self.ext_modules is emptied in hgbuildpy.finalize_options which is
298 # self.ext_modules is emptied in hgbuildpy.finalize_options which is
293 # too late for some cases
299 # too late for some cases
294 return not self.pure and Distribution.has_ext_modules(self)
300 return not self.pure and Distribution.has_ext_modules(self)
295
301
296 class hgbuildext(build_ext):
302 class hgbuildext(build_ext):
297
303
298 def build_extension(self, ext):
304 def build_extension(self, ext):
299 try:
305 try:
300 build_ext.build_extension(self, ext)
306 build_ext.build_extension(self, ext)
301 except CCompilerError:
307 except CCompilerError:
302 if not getattr(ext, 'optional', False):
308 if not getattr(ext, 'optional', False):
303 raise
309 raise
304 log.warn("Failed to build optional extension '%s' (skipping)",
310 log.warn("Failed to build optional extension '%s' (skipping)",
305 ext.name)
311 ext.name)
306
312
313 class hgbuildscripts(build_scripts):
314 def run(self):
315 if os.name != 'nt':
316 return build_scripts.run(self)
317
318 exebuilt = False
319 try:
320 self.run_command('build_hgexe')
321 exebuilt = True
322 except (DistutilsError, CCompilerError):
323 log.warn('failed to build optional hg.exe')
324
325 if exebuilt:
326 # Copying hg.exe to the scripts build directory ensures it is
327 # installed by the install_scripts command.
328 hgexecommand = self.get_finalized_command('build_hgexe')
329 dest = os.path.join(self.build_dir, 'hg.exe')
330 self.mkpath(self.build_dir)
331 self.copy_file(hgexecommand.hgexepath, dest)
332
333 # Remove hg.bat because it is redundant with hg.exe.
334 self.scripts.remove('contrib/win32/hg.bat')
335
336 return build_scripts.run(self)
337
307 class hgbuildpy(build_py):
338 class hgbuildpy(build_py):
308 if convert2to3:
339 if convert2to3:
309 fixer_names = sorted(set(getfixers("lib2to3.fixes") +
340 fixer_names = sorted(set(getfixers("lib2to3.fixes") +
310 getfixers("hgfixes")))
341 getfixers("hgfixes")))
311
342
312 def finalize_options(self):
343 def finalize_options(self):
313 build_py.finalize_options(self)
344 build_py.finalize_options(self)
314
345
315 if self.distribution.pure:
346 if self.distribution.pure:
316 self.distribution.ext_modules = []
347 self.distribution.ext_modules = []
317 else:
348 else:
318 h = os.path.join(get_python_inc(), 'Python.h')
349 h = os.path.join(get_python_inc(), 'Python.h')
319 if not os.path.exists(h):
350 if not os.path.exists(h):
320 raise SystemExit('Python headers are required to build '
351 raise SystemExit('Python headers are required to build '
321 'Mercurial but weren\'t found in %s' % h)
352 'Mercurial but weren\'t found in %s' % h)
322
353
323 def copy_file(self, *args, **kwargs):
354 def copy_file(self, *args, **kwargs):
324 dst, copied = build_py.copy_file(self, *args, **kwargs)
355 dst, copied = build_py.copy_file(self, *args, **kwargs)
325
356
326 if copied and dst.endswith('__init__.py'):
357 if copied and dst.endswith('__init__.py'):
327 if self.distribution.pure:
358 if self.distribution.pure:
328 modulepolicy = 'py'
359 modulepolicy = 'py'
329 else:
360 else:
330 modulepolicy = 'c'
361 modulepolicy = 'c'
331 content = open(dst, 'rb').read()
362 content = open(dst, 'rb').read()
332 content = content.replace(b'@MODULELOADPOLICY@',
363 content = content.replace(b'@MODULELOADPOLICY@',
333 modulepolicy.encode(libdir_escape))
364 modulepolicy.encode(libdir_escape))
334 with open(dst, 'wb') as fh:
365 with open(dst, 'wb') as fh:
335 fh.write(content)
366 fh.write(content)
336
367
337 return dst, copied
368 return dst, copied
338
369
339 class buildhgextindex(Command):
370 class buildhgextindex(Command):
340 description = 'generate prebuilt index of hgext (for frozen package)'
371 description = 'generate prebuilt index of hgext (for frozen package)'
341 user_options = []
372 user_options = []
342 _indexfilename = 'hgext/__index__.py'
373 _indexfilename = 'hgext/__index__.py'
343
374
344 def initialize_options(self):
375 def initialize_options(self):
345 pass
376 pass
346
377
347 def finalize_options(self):
378 def finalize_options(self):
348 pass
379 pass
349
380
350 def run(self):
381 def run(self):
351 if os.path.exists(self._indexfilename):
382 if os.path.exists(self._indexfilename):
352 f = open(self._indexfilename, 'w')
383 f = open(self._indexfilename, 'w')
353 f.write('# empty\n')
384 f.write('# empty\n')
354 f.close()
385 f.close()
355
386
356 # here no extension enabled, disabled() lists up everything
387 # here no extension enabled, disabled() lists up everything
357 code = ('import pprint; from mercurial import extensions; '
388 code = ('import pprint; from mercurial import extensions; '
358 'pprint.pprint(extensions.disabled())')
389 'pprint.pprint(extensions.disabled())')
359 out, err = runcmd([sys.executable, '-c', code], env)
390 out, err = runcmd([sys.executable, '-c', code], env)
360 if err:
391 if err:
361 raise DistutilsExecError(err)
392 raise DistutilsExecError(err)
362
393
363 f = open(self._indexfilename, 'w')
394 f = open(self._indexfilename, 'w')
364 f.write('# this file is autogenerated by setup.py\n')
395 f.write('# this file is autogenerated by setup.py\n')
365 f.write('docs = ')
396 f.write('docs = ')
366 f.write(out)
397 f.write(out)
367 f.close()
398 f.close()
368
399
369 class buildhgexe(build_ext):
400 class buildhgexe(build_ext):
370 description = 'compile hg.exe from mercurial/exewrapper.c'
401 description = 'compile hg.exe from mercurial/exewrapper.c'
371
402
372 def build_extensions(self):
403 def build_extensions(self):
373 if os.name != 'nt':
404 if os.name != 'nt':
374 return
405 return
375 if isinstance(self.compiler, HackedMingw32CCompiler):
406 if isinstance(self.compiler, HackedMingw32CCompiler):
376 self.compiler.compiler_so = self.compiler.compiler # no -mdll
407 self.compiler.compiler_so = self.compiler.compiler # no -mdll
377 self.compiler.dll_libraries = [] # no -lmsrvc90
408 self.compiler.dll_libraries = [] # no -lmsrvc90
378 hv = sys.hexversion
409 hv = sys.hexversion
379 pythonlib = 'python%d%d' % (hv >> 24, (hv >> 16) & 0xff)
410 pythonlib = 'python%d%d' % (hv >> 24, (hv >> 16) & 0xff)
380 f = open('mercurial/hgpythonlib.h', 'wb')
411 f = open('mercurial/hgpythonlib.h', 'wb')
381 f.write('/* this file is autogenerated by setup.py */\n')
412 f.write('/* this file is autogenerated by setup.py */\n')
382 f.write('#define HGPYTHONLIB "%s"\n' % pythonlib)
413 f.write('#define HGPYTHONLIB "%s"\n' % pythonlib)
383 f.close()
414 f.close()
384 objects = self.compiler.compile(['mercurial/exewrapper.c'],
415 objects = self.compiler.compile(['mercurial/exewrapper.c'],
385 output_dir=self.build_temp)
416 output_dir=self.build_temp)
386 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
417 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
387 target = os.path.join(dir, 'hg')
418 target = os.path.join(dir, 'hg')
388 self.compiler.link_executable(objects, target,
419 self.compiler.link_executable(objects, target,
389 libraries=[],
420 libraries=[],
390 output_dir=self.build_temp)
421 output_dir=self.build_temp)
391
422
423 @property
424 def hgexepath(self):
425 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
426 return os.path.join(self.build_temp, dir, 'hg.exe')
427
392 class hginstalllib(install_lib):
428 class hginstalllib(install_lib):
393 '''
429 '''
394 This is a specialization of install_lib that replaces the copy_file used
430 This is a specialization of install_lib that replaces the copy_file used
395 there so that it supports setting the mode of files after copying them,
431 there so that it supports setting the mode of files after copying them,
396 instead of just preserving the mode that the files originally had. If your
432 instead of just preserving the mode that the files originally had. If your
397 system has a umask of something like 027, preserving the permissions when
433 system has a umask of something like 027, preserving the permissions when
398 copying will lead to a broken install.
434 copying will lead to a broken install.
399
435
400 Note that just passing keep_permissions=False to copy_file would be
436 Note that just passing keep_permissions=False to copy_file would be
401 insufficient, as it might still be applying a umask.
437 insufficient, as it might still be applying a umask.
402 '''
438 '''
403
439
404 def run(self):
440 def run(self):
405 realcopyfile = file_util.copy_file
441 realcopyfile = file_util.copy_file
406 def copyfileandsetmode(*args, **kwargs):
442 def copyfileandsetmode(*args, **kwargs):
407 src, dst = args[0], args[1]
443 src, dst = args[0], args[1]
408 dst, copied = realcopyfile(*args, **kwargs)
444 dst, copied = realcopyfile(*args, **kwargs)
409 if copied:
445 if copied:
410 st = os.stat(src)
446 st = os.stat(src)
411 # Persist executable bit (apply it to group and other if user
447 # Persist executable bit (apply it to group and other if user
412 # has it)
448 # has it)
413 if st[stat.ST_MODE] & stat.S_IXUSR:
449 if st[stat.ST_MODE] & stat.S_IXUSR:
414 setmode = int('0755', 8)
450 setmode = int('0755', 8)
415 else:
451 else:
416 setmode = int('0644', 8)
452 setmode = int('0644', 8)
417 m = stat.S_IMODE(st[stat.ST_MODE])
453 m = stat.S_IMODE(st[stat.ST_MODE])
418 m = (m & ~int('0777', 8)) | setmode
454 m = (m & ~int('0777', 8)) | setmode
419 os.chmod(dst, m)
455 os.chmod(dst, m)
420 file_util.copy_file = copyfileandsetmode
456 file_util.copy_file = copyfileandsetmode
421 try:
457 try:
422 install_lib.run(self)
458 install_lib.run(self)
423 finally:
459 finally:
424 file_util.copy_file = realcopyfile
460 file_util.copy_file = realcopyfile
425
461
426 class hginstallscripts(install_scripts):
462 class hginstallscripts(install_scripts):
427 '''
463 '''
428 This is a specialization of install_scripts that replaces the @LIBDIR@ with
464 This is a specialization of install_scripts that replaces the @LIBDIR@ with
429 the configured directory for modules. If possible, the path is made relative
465 the configured directory for modules. If possible, the path is made relative
430 to the directory for scripts.
466 to the directory for scripts.
431 '''
467 '''
432
468
433 def initialize_options(self):
469 def initialize_options(self):
434 install_scripts.initialize_options(self)
470 install_scripts.initialize_options(self)
435
471
436 self.install_lib = None
472 self.install_lib = None
437
473
438 def finalize_options(self):
474 def finalize_options(self):
439 install_scripts.finalize_options(self)
475 install_scripts.finalize_options(self)
440 self.set_undefined_options('install',
476 self.set_undefined_options('install',
441 ('install_lib', 'install_lib'))
477 ('install_lib', 'install_lib'))
442
478
443 def run(self):
479 def run(self):
444 install_scripts.run(self)
480 install_scripts.run(self)
445
481
446 if (os.path.splitdrive(self.install_dir)[0] !=
482 if (os.path.splitdrive(self.install_dir)[0] !=
447 os.path.splitdrive(self.install_lib)[0]):
483 os.path.splitdrive(self.install_lib)[0]):
448 # can't make relative paths from one drive to another, so use an
484 # can't make relative paths from one drive to another, so use an
449 # absolute path instead
485 # absolute path instead
450 libdir = self.install_lib
486 libdir = self.install_lib
451 else:
487 else:
452 common = os.path.commonprefix((self.install_dir, self.install_lib))
488 common = os.path.commonprefix((self.install_dir, self.install_lib))
453 rest = self.install_dir[len(common):]
489 rest = self.install_dir[len(common):]
454 uplevel = len([n for n in os.path.split(rest) if n])
490 uplevel = len([n for n in os.path.split(rest) if n])
455
491
456 libdir = uplevel * ('..' + os.sep) + self.install_lib[len(common):]
492 libdir = uplevel * ('..' + os.sep) + self.install_lib[len(common):]
457
493
458 for outfile in self.outfiles:
494 for outfile in self.outfiles:
459 fp = open(outfile, 'rb')
495 fp = open(outfile, 'rb')
460 data = fp.read()
496 data = fp.read()
461 fp.close()
497 fp.close()
462
498
463 # skip binary files
499 # skip binary files
464 if b('\0') in data:
500 if b('\0') in data:
465 continue
501 continue
466
502
467 data = data.replace(b('@LIBDIR@'), libdir.encode(libdir_escape))
503 data = data.replace(b('@LIBDIR@'), libdir.encode(libdir_escape))
468 fp = open(outfile, 'wb')
504 fp = open(outfile, 'wb')
469 fp.write(data)
505 fp.write(data)
470 fp.close()
506 fp.close()
471
507
472 cmdclass = {'build': hgbuild,
508 cmdclass = {'build': hgbuild,
473 'build_mo': hgbuildmo,
509 'build_mo': hgbuildmo,
474 'build_ext': hgbuildext,
510 'build_ext': hgbuildext,
475 'build_py': hgbuildpy,
511 'build_py': hgbuildpy,
512 'build_scripts': hgbuildscripts,
476 'build_hgextindex': buildhgextindex,
513 'build_hgextindex': buildhgextindex,
477 'install_lib': hginstalllib,
514 'install_lib': hginstalllib,
478 'install_scripts': hginstallscripts,
515 'install_scripts': hginstallscripts,
479 'build_hgexe': buildhgexe,
516 'build_hgexe': buildhgexe,
480 }
517 }
481
518
482 packages = ['mercurial', 'mercurial.hgweb', 'mercurial.httpclient',
519 packages = ['mercurial', 'mercurial.hgweb', 'mercurial.httpclient',
483 'mercurial.pure',
520 'mercurial.pure',
484 'hgext', 'hgext.convert', 'hgext.highlight', 'hgext.zeroconf',
521 'hgext', 'hgext.convert', 'hgext.highlight', 'hgext.zeroconf',
485 'hgext.largefiles']
522 'hgext.largefiles']
486
523
487 common_depends = ['mercurial/util.h']
524 common_depends = ['mercurial/util.h']
488
525
489 osutil_ldflags = []
526 osutil_ldflags = []
490
527
491 if sys.platform == 'darwin':
528 if sys.platform == 'darwin':
492 osutil_ldflags += ['-framework', 'ApplicationServices']
529 osutil_ldflags += ['-framework', 'ApplicationServices']
493
530
494 extmodules = [
531 extmodules = [
495 Extension('mercurial.base85', ['mercurial/base85.c'],
532 Extension('mercurial.base85', ['mercurial/base85.c'],
496 depends=common_depends),
533 depends=common_depends),
497 Extension('mercurial.bdiff', ['mercurial/bdiff.c'],
534 Extension('mercurial.bdiff', ['mercurial/bdiff.c'],
498 depends=common_depends),
535 depends=common_depends),
499 Extension('mercurial.diffhelpers', ['mercurial/diffhelpers.c'],
536 Extension('mercurial.diffhelpers', ['mercurial/diffhelpers.c'],
500 depends=common_depends),
537 depends=common_depends),
501 Extension('mercurial.mpatch', ['mercurial/mpatch.c'],
538 Extension('mercurial.mpatch', ['mercurial/mpatch.c'],
502 depends=common_depends),
539 depends=common_depends),
503 Extension('mercurial.parsers', ['mercurial/dirs.c',
540 Extension('mercurial.parsers', ['mercurial/dirs.c',
504 'mercurial/manifest.c',
541 'mercurial/manifest.c',
505 'mercurial/parsers.c',
542 'mercurial/parsers.c',
506 'mercurial/pathencode.c'],
543 'mercurial/pathencode.c'],
507 depends=common_depends),
544 depends=common_depends),
508 Extension('mercurial.osutil', ['mercurial/osutil.c'],
545 Extension('mercurial.osutil', ['mercurial/osutil.c'],
509 extra_link_args=osutil_ldflags,
546 extra_link_args=osutil_ldflags,
510 depends=common_depends),
547 depends=common_depends),
511 ]
548 ]
512
549
513 try:
550 try:
514 from distutils import cygwinccompiler
551 from distutils import cygwinccompiler
515
552
516 # the -mno-cygwin option has been deprecated for years
553 # the -mno-cygwin option has been deprecated for years
517 compiler = cygwinccompiler.Mingw32CCompiler
554 compiler = cygwinccompiler.Mingw32CCompiler
518
555
519 class HackedMingw32CCompiler(cygwinccompiler.Mingw32CCompiler):
556 class HackedMingw32CCompiler(cygwinccompiler.Mingw32CCompiler):
520 def __init__(self, *args, **kwargs):
557 def __init__(self, *args, **kwargs):
521 compiler.__init__(self, *args, **kwargs)
558 compiler.__init__(self, *args, **kwargs)
522 for i in 'compiler compiler_so linker_exe linker_so'.split():
559 for i in 'compiler compiler_so linker_exe linker_so'.split():
523 try:
560 try:
524 getattr(self, i).remove('-mno-cygwin')
561 getattr(self, i).remove('-mno-cygwin')
525 except ValueError:
562 except ValueError:
526 pass
563 pass
527
564
528 cygwinccompiler.Mingw32CCompiler = HackedMingw32CCompiler
565 cygwinccompiler.Mingw32CCompiler = HackedMingw32CCompiler
529 except ImportError:
566 except ImportError:
530 # the cygwinccompiler package is not available on some Python
567 # the cygwinccompiler package is not available on some Python
531 # distributions like the ones from the optware project for Synology
568 # distributions like the ones from the optware project for Synology
532 # DiskStation boxes
569 # DiskStation boxes
533 class HackedMingw32CCompiler(object):
570 class HackedMingw32CCompiler(object):
534 pass
571 pass
535
572
536 packagedata = {'mercurial': ['locale/*/LC_MESSAGES/hg.mo',
573 packagedata = {'mercurial': ['locale/*/LC_MESSAGES/hg.mo',
537 'help/*.txt',
574 'help/*.txt',
538 'default.d/*.rc',
575 'default.d/*.rc',
539 'dummycert.pem']}
576 'dummycert.pem']}
540
577
541 def ordinarypath(p):
578 def ordinarypath(p):
542 return p and p[0] != '.' and p[-1] != '~'
579 return p and p[0] != '.' and p[-1] != '~'
543
580
544 for root in ('templates',):
581 for root in ('templates',):
545 for curdir, dirs, files in os.walk(os.path.join('mercurial', root)):
582 for curdir, dirs, files in os.walk(os.path.join('mercurial', root)):
546 curdir = curdir.split(os.sep, 1)[1]
583 curdir = curdir.split(os.sep, 1)[1]
547 dirs[:] = filter(ordinarypath, dirs)
584 dirs[:] = filter(ordinarypath, dirs)
548 for f in filter(ordinarypath, files):
585 for f in filter(ordinarypath, files):
549 f = os.path.join(curdir, f)
586 f = os.path.join(curdir, f)
550 packagedata['mercurial'].append(f)
587 packagedata['mercurial'].append(f)
551
588
552 datafiles = []
589 datafiles = []
553 setupversion = version
590 setupversion = version
554 extra = {}
591 extra = {}
555
592
556 if py2exeloaded:
593 if py2exeloaded:
557 extra['console'] = [
594 extra['console'] = [
558 {'script':'hg',
595 {'script':'hg',
559 'copyright':'Copyright (C) 2005-2015 Matt Mackall and others',
596 'copyright':'Copyright (C) 2005-2015 Matt Mackall and others',
560 'product_version':version}]
597 'product_version':version}]
561 # sub command of 'build' because 'py2exe' does not handle sub_commands
598 # sub command of 'build' because 'py2exe' does not handle sub_commands
562 build.sub_commands.insert(0, ('build_hgextindex', None))
599 build.sub_commands.insert(0, ('build_hgextindex', None))
563 # put dlls in sub directory so that they won't pollute PATH
600 # put dlls in sub directory so that they won't pollute PATH
564 extra['zipfile'] = 'lib/library.zip'
601 extra['zipfile'] = 'lib/library.zip'
565
602
566 if os.name == 'nt':
603 if os.name == 'nt':
567 # Windows binary file versions for exe/dll files must have the
604 # Windows binary file versions for exe/dll files must have the
568 # form W.X.Y.Z, where W,X,Y,Z are numbers in the range 0..65535
605 # form W.X.Y.Z, where W,X,Y,Z are numbers in the range 0..65535
569 setupversion = version.split('+', 1)[0]
606 setupversion = version.split('+', 1)[0]
570
607
571 if sys.platform == 'darwin' and os.path.exists('/usr/bin/xcodebuild'):
608 if sys.platform == 'darwin' and os.path.exists('/usr/bin/xcodebuild'):
572 version = runcmd(['/usr/bin/xcodebuild', '-version'], {})[0].splitlines()
609 version = runcmd(['/usr/bin/xcodebuild', '-version'], {})[0].splitlines()
573 if version:
610 if version:
574 version = version[0]
611 version = version[0]
575 if sys.version_info[0] == 3:
612 if sys.version_info[0] == 3:
576 version = version.decode('utf-8')
613 version = version.decode('utf-8')
577 xcode4 = (version.startswith('Xcode') and
614 xcode4 = (version.startswith('Xcode') and
578 StrictVersion(version.split()[1]) >= StrictVersion('4.0'))
615 StrictVersion(version.split()[1]) >= StrictVersion('4.0'))
579 xcode51 = re.match(r'^Xcode\s+5\.1', version) is not None
616 xcode51 = re.match(r'^Xcode\s+5\.1', version) is not None
580 else:
617 else:
581 # xcodebuild returns empty on OS X Lion with XCode 4.3 not
618 # xcodebuild returns empty on OS X Lion with XCode 4.3 not
582 # installed, but instead with only command-line tools. Assume
619 # installed, but instead with only command-line tools. Assume
583 # that only happens on >= Lion, thus no PPC support.
620 # that only happens on >= Lion, thus no PPC support.
584 xcode4 = True
621 xcode4 = True
585 xcode51 = False
622 xcode51 = False
586
623
587 # XCode 4.0 dropped support for ppc architecture, which is hardcoded in
624 # XCode 4.0 dropped support for ppc architecture, which is hardcoded in
588 # distutils.sysconfig
625 # distutils.sysconfig
589 if xcode4:
626 if xcode4:
590 os.environ['ARCHFLAGS'] = ''
627 os.environ['ARCHFLAGS'] = ''
591
628
592 # XCode 5.1 changes clang such that it now fails to compile if the
629 # XCode 5.1 changes clang such that it now fails to compile if the
593 # -mno-fused-madd flag is passed, but the version of Python shipped with
630 # -mno-fused-madd flag is passed, but the version of Python shipped with
594 # OS X 10.9 Mavericks includes this flag. This causes problems in all
631 # OS X 10.9 Mavericks includes this flag. This causes problems in all
595 # C extension modules, and a bug has been filed upstream at
632 # C extension modules, and a bug has been filed upstream at
596 # http://bugs.python.org/issue21244. We also need to patch this here
633 # http://bugs.python.org/issue21244. We also need to patch this here
597 # so Mercurial can continue to compile in the meantime.
634 # so Mercurial can continue to compile in the meantime.
598 if xcode51:
635 if xcode51:
599 cflags = get_config_var('CFLAGS')
636 cflags = get_config_var('CFLAGS')
600 if cflags and re.search(r'-mno-fused-madd\b', cflags) is not None:
637 if cflags and re.search(r'-mno-fused-madd\b', cflags) is not None:
601 os.environ['CFLAGS'] = (
638 os.environ['CFLAGS'] = (
602 os.environ.get('CFLAGS', '') + ' -Qunused-arguments')
639 os.environ.get('CFLAGS', '') + ' -Qunused-arguments')
603
640
604 setup(name='mercurial',
641 setup(name='mercurial',
605 version=setupversion,
642 version=setupversion,
606 author='Matt Mackall and many others',
643 author='Matt Mackall and many others',
607 author_email='mercurial@selenic.com',
644 author_email='mercurial@selenic.com',
608 url='https://mercurial-scm.org/',
645 url='https://mercurial-scm.org/',
609 download_url='https://mercurial-scm.org/release/',
646 download_url='https://mercurial-scm.org/release/',
610 description=('Fast scalable distributed SCM (revision control, version '
647 description=('Fast scalable distributed SCM (revision control, version '
611 'control) system'),
648 'control) system'),
612 long_description=('Mercurial is a distributed SCM tool written in Python.'
649 long_description=('Mercurial is a distributed SCM tool written in Python.'
613 ' It is used by a number of large projects that require'
650 ' It is used by a number of large projects that require'
614 ' fast, reliable distributed revision control, such as '
651 ' fast, reliable distributed revision control, such as '
615 'Mozilla.'),
652 'Mozilla.'),
616 license='GNU GPLv2 or any later version',
653 license='GNU GPLv2 or any later version',
617 classifiers=[
654 classifiers=[
618 'Development Status :: 6 - Mature',
655 'Development Status :: 6 - Mature',
619 'Environment :: Console',
656 'Environment :: Console',
620 'Intended Audience :: Developers',
657 'Intended Audience :: Developers',
621 'Intended Audience :: System Administrators',
658 'Intended Audience :: System Administrators',
622 'License :: OSI Approved :: GNU General Public License (GPL)',
659 'License :: OSI Approved :: GNU General Public License (GPL)',
623 'Natural Language :: Danish',
660 'Natural Language :: Danish',
624 'Natural Language :: English',
661 'Natural Language :: English',
625 'Natural Language :: German',
662 'Natural Language :: German',
626 'Natural Language :: Italian',
663 'Natural Language :: Italian',
627 'Natural Language :: Japanese',
664 'Natural Language :: Japanese',
628 'Natural Language :: Portuguese (Brazilian)',
665 'Natural Language :: Portuguese (Brazilian)',
629 'Operating System :: Microsoft :: Windows',
666 'Operating System :: Microsoft :: Windows',
630 'Operating System :: OS Independent',
667 'Operating System :: OS Independent',
631 'Operating System :: POSIX',
668 'Operating System :: POSIX',
632 'Programming Language :: C',
669 'Programming Language :: C',
633 'Programming Language :: Python',
670 'Programming Language :: Python',
634 'Topic :: Software Development :: Version Control',
671 'Topic :: Software Development :: Version Control',
635 ],
672 ],
636 scripts=scripts,
673 scripts=scripts,
637 packages=packages,
674 packages=packages,
638 ext_modules=extmodules,
675 ext_modules=extmodules,
639 data_files=datafiles,
676 data_files=datafiles,
640 package_data=packagedata,
677 package_data=packagedata,
641 cmdclass=cmdclass,
678 cmdclass=cmdclass,
642 distclass=hgdist,
679 distclass=hgdist,
643 options={'py2exe': {'packages': ['hgext', 'email']},
680 options={'py2exe': {'packages': ['hgext', 'email']},
644 'bdist_mpkg': {'zipdist': False,
681 'bdist_mpkg': {'zipdist': False,
645 'license': 'COPYING',
682 'license': 'COPYING',
646 'readme': 'contrib/macosx/Readme.html',
683 'readme': 'contrib/macosx/Readme.html',
647 'welcome': 'contrib/macosx/Welcome.html',
684 'welcome': 'contrib/macosx/Welcome.html',
648 },
685 },
649 },
686 },
650 **extra)
687 **extra)
General Comments 0
You need to be logged in to leave comments. Login now