##// END OF EJS Templates
demandimport: move to separate package...
Siddharth Agarwal -
r32420:0906b85b default
parent child Browse files
Show More
@@ -0,0 +1,23
1 # hgdemandimport - global demand-loading of modules for Mercurial
2 #
3 # Copyright 2017 Facebook Inc.
4 #
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
7
8 '''demandimport - automatic demand-loading of modules'''
9
10 # This is in a separate package from mercurial because in Python 3,
11 # demand loading is per-package. Keeping demandimport in the mercurial package
12 # would disable demand loading for any modules in mercurial.
13
14 from __future__ import absolute_import
15
16 from . import demandimportpy2 as demandimport
17
18 # Re-export.
19 ignore = demandimport.ignore
20 isenabled = demandimport.isenabled
21 enable = demandimport.enable
22 disable = demandimport.disable
23 deactivated = demandimport.deactivated
@@ -1,725 +1,727
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2
2
3 from __future__ import absolute_import, print_function
3 from __future__ import absolute_import, print_function
4
4
5 import ast
5 import ast
6 import collections
6 import collections
7 import os
7 import os
8 import re
8 import re
9 import sys
9 import sys
10
10
11 # Import a minimal set of stdlib modules needed for list_stdlib_modules()
11 # Import a minimal set of stdlib modules needed for list_stdlib_modules()
12 # to work when run from a virtualenv. The modules were chosen empirically
12 # to work when run from a virtualenv. The modules were chosen empirically
13 # so that the return value matches the return value without virtualenv.
13 # so that the return value matches the return value without virtualenv.
14 if True: # disable lexical sorting checks
14 if True: # disable lexical sorting checks
15 import BaseHTTPServer
15 import BaseHTTPServer
16 import zlib
16 import zlib
17
17
18 # Whitelist of modules that symbols can be directly imported from.
18 # Whitelist of modules that symbols can be directly imported from.
19 allowsymbolimports = (
19 allowsymbolimports = (
20 '__future__',
20 '__future__',
21 'mercurial.hgweb.common',
21 'mercurial.hgweb.common',
22 'mercurial.hgweb.request',
22 'mercurial.hgweb.request',
23 'mercurial.i18n',
23 'mercurial.i18n',
24 'mercurial.node',
24 'mercurial.node',
25 )
25 )
26
26
27 # Whitelist of symbols that can be directly imported.
27 # Whitelist of symbols that can be directly imported.
28 directsymbols = ()
28 directsymbols = (
29 'demandimport',
30 )
29
31
30 # Modules that must be aliased because they are commonly confused with
32 # Modules that must be aliased because they are commonly confused with
31 # common variables and can create aliasing and readability issues.
33 # common variables and can create aliasing and readability issues.
32 requirealias = {
34 requirealias = {
33 'ui': 'uimod',
35 'ui': 'uimod',
34 }
36 }
35
37
36 def usingabsolute(root):
38 def usingabsolute(root):
37 """Whether absolute imports are being used."""
39 """Whether absolute imports are being used."""
38 if sys.version_info[0] >= 3:
40 if sys.version_info[0] >= 3:
39 return True
41 return True
40
42
41 for node in ast.walk(root):
43 for node in ast.walk(root):
42 if isinstance(node, ast.ImportFrom):
44 if isinstance(node, ast.ImportFrom):
43 if node.module == '__future__':
45 if node.module == '__future__':
44 for n in node.names:
46 for n in node.names:
45 if n.name == 'absolute_import':
47 if n.name == 'absolute_import':
46 return True
48 return True
47
49
48 return False
50 return False
49
51
50 def walklocal(root):
52 def walklocal(root):
51 """Recursively yield all descendant nodes but not in a different scope"""
53 """Recursively yield all descendant nodes but not in a different scope"""
52 todo = collections.deque(ast.iter_child_nodes(root))
54 todo = collections.deque(ast.iter_child_nodes(root))
53 yield root, False
55 yield root, False
54 while todo:
56 while todo:
55 node = todo.popleft()
57 node = todo.popleft()
56 newscope = isinstance(node, ast.FunctionDef)
58 newscope = isinstance(node, ast.FunctionDef)
57 if not newscope:
59 if not newscope:
58 todo.extend(ast.iter_child_nodes(node))
60 todo.extend(ast.iter_child_nodes(node))
59 yield node, newscope
61 yield node, newscope
60
62
61 def dotted_name_of_path(path):
63 def dotted_name_of_path(path):
62 """Given a relative path to a source file, return its dotted module name.
64 """Given a relative path to a source file, return its dotted module name.
63
65
64 >>> dotted_name_of_path('mercurial/error.py')
66 >>> dotted_name_of_path('mercurial/error.py')
65 'mercurial.error'
67 'mercurial.error'
66 >>> dotted_name_of_path('zlibmodule.so')
68 >>> dotted_name_of_path('zlibmodule.so')
67 'zlib'
69 'zlib'
68 """
70 """
69 parts = path.replace(os.sep, '/').split('/')
71 parts = path.replace(os.sep, '/').split('/')
70 parts[-1] = parts[-1].split('.', 1)[0] # remove .py and .so and .ARCH.so
72 parts[-1] = parts[-1].split('.', 1)[0] # remove .py and .so and .ARCH.so
71 if parts[-1].endswith('module'):
73 if parts[-1].endswith('module'):
72 parts[-1] = parts[-1][:-6]
74 parts[-1] = parts[-1][:-6]
73 return '.'.join(parts)
75 return '.'.join(parts)
74
76
75 def fromlocalfunc(modulename, localmods):
77 def fromlocalfunc(modulename, localmods):
76 """Get a function to examine which locally defined module the
78 """Get a function to examine which locally defined module the
77 target source imports via a specified name.
79 target source imports via a specified name.
78
80
79 `modulename` is an `dotted_name_of_path()`-ed source file path,
81 `modulename` is an `dotted_name_of_path()`-ed source file path,
80 which may have `.__init__` at the end of it, of the target source.
82 which may have `.__init__` at the end of it, of the target source.
81
83
82 `localmods` is a dict (or set), of which key is an absolute
84 `localmods` is a dict (or set), of which key is an absolute
83 `dotted_name_of_path()`-ed source file path of locally defined (=
85 `dotted_name_of_path()`-ed source file path of locally defined (=
84 Mercurial specific) modules.
86 Mercurial specific) modules.
85
87
86 This function assumes that module names not existing in
88 This function assumes that module names not existing in
87 `localmods` are from the Python standard library.
89 `localmods` are from the Python standard library.
88
90
89 This function returns the function, which takes `name` argument,
91 This function returns the function, which takes `name` argument,
90 and returns `(absname, dottedpath, hassubmod)` tuple if `name`
92 and returns `(absname, dottedpath, hassubmod)` tuple if `name`
91 matches against locally defined module. Otherwise, it returns
93 matches against locally defined module. Otherwise, it returns
92 False.
94 False.
93
95
94 It is assumed that `name` doesn't have `.__init__`.
96 It is assumed that `name` doesn't have `.__init__`.
95
97
96 `absname` is an absolute module name of specified `name`
98 `absname` is an absolute module name of specified `name`
97 (e.g. "hgext.convert"). This can be used to compose prefix for sub
99 (e.g. "hgext.convert"). This can be used to compose prefix for sub
98 modules or so.
100 modules or so.
99
101
100 `dottedpath` is a `dotted_name_of_path()`-ed source file path
102 `dottedpath` is a `dotted_name_of_path()`-ed source file path
101 (e.g. "hgext.convert.__init__") of `name`. This is used to look
103 (e.g. "hgext.convert.__init__") of `name`. This is used to look
102 module up in `localmods` again.
104 module up in `localmods` again.
103
105
104 `hassubmod` is whether it may have sub modules under it (for
106 `hassubmod` is whether it may have sub modules under it (for
105 convenient, even though this is also equivalent to "absname !=
107 convenient, even though this is also equivalent to "absname !=
106 dottednpath")
108 dottednpath")
107
109
108 >>> localmods = {'foo.__init__': True, 'foo.foo1': True,
110 >>> localmods = {'foo.__init__': True, 'foo.foo1': True,
109 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
111 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
110 ... 'baz.__init__': True, 'baz.baz1': True }
112 ... 'baz.__init__': True, 'baz.baz1': True }
111 >>> fromlocal = fromlocalfunc('foo.xxx', localmods)
113 >>> fromlocal = fromlocalfunc('foo.xxx', localmods)
112 >>> # relative
114 >>> # relative
113 >>> fromlocal('foo1')
115 >>> fromlocal('foo1')
114 ('foo.foo1', 'foo.foo1', False)
116 ('foo.foo1', 'foo.foo1', False)
115 >>> fromlocal('bar')
117 >>> fromlocal('bar')
116 ('foo.bar', 'foo.bar.__init__', True)
118 ('foo.bar', 'foo.bar.__init__', True)
117 >>> fromlocal('bar.bar1')
119 >>> fromlocal('bar.bar1')
118 ('foo.bar.bar1', 'foo.bar.bar1', False)
120 ('foo.bar.bar1', 'foo.bar.bar1', False)
119 >>> # absolute
121 >>> # absolute
120 >>> fromlocal('baz')
122 >>> fromlocal('baz')
121 ('baz', 'baz.__init__', True)
123 ('baz', 'baz.__init__', True)
122 >>> fromlocal('baz.baz1')
124 >>> fromlocal('baz.baz1')
123 ('baz.baz1', 'baz.baz1', False)
125 ('baz.baz1', 'baz.baz1', False)
124 >>> # unknown = maybe standard library
126 >>> # unknown = maybe standard library
125 >>> fromlocal('os')
127 >>> fromlocal('os')
126 False
128 False
127 >>> fromlocal(None, 1)
129 >>> fromlocal(None, 1)
128 ('foo', 'foo.__init__', True)
130 ('foo', 'foo.__init__', True)
129 >>> fromlocal('foo1', 1)
131 >>> fromlocal('foo1', 1)
130 ('foo.foo1', 'foo.foo1', False)
132 ('foo.foo1', 'foo.foo1', False)
131 >>> fromlocal2 = fromlocalfunc('foo.xxx.yyy', localmods)
133 >>> fromlocal2 = fromlocalfunc('foo.xxx.yyy', localmods)
132 >>> fromlocal2(None, 2)
134 >>> fromlocal2(None, 2)
133 ('foo', 'foo.__init__', True)
135 ('foo', 'foo.__init__', True)
134 >>> fromlocal2('bar2', 1)
136 >>> fromlocal2('bar2', 1)
135 False
137 False
136 >>> fromlocal2('bar', 2)
138 >>> fromlocal2('bar', 2)
137 ('foo.bar', 'foo.bar.__init__', True)
139 ('foo.bar', 'foo.bar.__init__', True)
138 """
140 """
139 prefix = '.'.join(modulename.split('.')[:-1])
141 prefix = '.'.join(modulename.split('.')[:-1])
140 if prefix:
142 if prefix:
141 prefix += '.'
143 prefix += '.'
142 def fromlocal(name, level=0):
144 def fromlocal(name, level=0):
143 # name is false value when relative imports are used.
145 # name is false value when relative imports are used.
144 if not name:
146 if not name:
145 # If relative imports are used, level must not be absolute.
147 # If relative imports are used, level must not be absolute.
146 assert level > 0
148 assert level > 0
147 candidates = ['.'.join(modulename.split('.')[:-level])]
149 candidates = ['.'.join(modulename.split('.')[:-level])]
148 else:
150 else:
149 if not level:
151 if not level:
150 # Check relative name first.
152 # Check relative name first.
151 candidates = [prefix + name, name]
153 candidates = [prefix + name, name]
152 else:
154 else:
153 candidates = ['.'.join(modulename.split('.')[:-level]) +
155 candidates = ['.'.join(modulename.split('.')[:-level]) +
154 '.' + name]
156 '.' + name]
155
157
156 for n in candidates:
158 for n in candidates:
157 if n in localmods:
159 if n in localmods:
158 return (n, n, False)
160 return (n, n, False)
159 dottedpath = n + '.__init__'
161 dottedpath = n + '.__init__'
160 if dottedpath in localmods:
162 if dottedpath in localmods:
161 return (n, dottedpath, True)
163 return (n, dottedpath, True)
162 return False
164 return False
163 return fromlocal
165 return fromlocal
164
166
165 def list_stdlib_modules():
167 def list_stdlib_modules():
166 """List the modules present in the stdlib.
168 """List the modules present in the stdlib.
167
169
168 >>> mods = set(list_stdlib_modules())
170 >>> mods = set(list_stdlib_modules())
169 >>> 'BaseHTTPServer' in mods
171 >>> 'BaseHTTPServer' in mods
170 True
172 True
171
173
172 os.path isn't really a module, so it's missing:
174 os.path isn't really a module, so it's missing:
173
175
174 >>> 'os.path' in mods
176 >>> 'os.path' in mods
175 False
177 False
176
178
177 sys requires special treatment, because it's baked into the
179 sys requires special treatment, because it's baked into the
178 interpreter, but it should still appear:
180 interpreter, but it should still appear:
179
181
180 >>> 'sys' in mods
182 >>> 'sys' in mods
181 True
183 True
182
184
183 >>> 'collections' in mods
185 >>> 'collections' in mods
184 True
186 True
185
187
186 >>> 'cStringIO' in mods
188 >>> 'cStringIO' in mods
187 True
189 True
188
190
189 >>> 'cffi' in mods
191 >>> 'cffi' in mods
190 True
192 True
191 """
193 """
192 for m in sys.builtin_module_names:
194 for m in sys.builtin_module_names:
193 yield m
195 yield m
194 # These modules only exist on windows, but we should always
196 # These modules only exist on windows, but we should always
195 # consider them stdlib.
197 # consider them stdlib.
196 for m in ['msvcrt', '_winreg']:
198 for m in ['msvcrt', '_winreg']:
197 yield m
199 yield m
198 yield 'builtins' # python3 only
200 yield 'builtins' # python3 only
199 for m in 'fcntl', 'grp', 'pwd', 'termios': # Unix only
201 for m in 'fcntl', 'grp', 'pwd', 'termios': # Unix only
200 yield m
202 yield m
201 for m in 'cPickle', 'datetime': # in Python (not C) on PyPy
203 for m in 'cPickle', 'datetime': # in Python (not C) on PyPy
202 yield m
204 yield m
203 for m in ['cffi']:
205 for m in ['cffi']:
204 yield m
206 yield m
205 stdlib_prefixes = {sys.prefix, sys.exec_prefix}
207 stdlib_prefixes = {sys.prefix, sys.exec_prefix}
206 # We need to supplement the list of prefixes for the search to work
208 # We need to supplement the list of prefixes for the search to work
207 # when run from within a virtualenv.
209 # when run from within a virtualenv.
208 for mod in (BaseHTTPServer, zlib):
210 for mod in (BaseHTTPServer, zlib):
209 try:
211 try:
210 # Not all module objects have a __file__ attribute.
212 # Not all module objects have a __file__ attribute.
211 filename = mod.__file__
213 filename = mod.__file__
212 except AttributeError:
214 except AttributeError:
213 continue
215 continue
214 dirname = os.path.dirname(filename)
216 dirname = os.path.dirname(filename)
215 for prefix in stdlib_prefixes:
217 for prefix in stdlib_prefixes:
216 if dirname.startswith(prefix):
218 if dirname.startswith(prefix):
217 # Then this directory is redundant.
219 # Then this directory is redundant.
218 break
220 break
219 else:
221 else:
220 stdlib_prefixes.add(dirname)
222 stdlib_prefixes.add(dirname)
221 for libpath in sys.path:
223 for libpath in sys.path:
222 # We want to walk everything in sys.path that starts with
224 # We want to walk everything in sys.path that starts with
223 # something in stdlib_prefixes.
225 # something in stdlib_prefixes.
224 if not any(libpath.startswith(p) for p in stdlib_prefixes):
226 if not any(libpath.startswith(p) for p in stdlib_prefixes):
225 continue
227 continue
226 for top, dirs, files in os.walk(libpath):
228 for top, dirs, files in os.walk(libpath):
227 for i, d in reversed(list(enumerate(dirs))):
229 for i, d in reversed(list(enumerate(dirs))):
228 if (not os.path.exists(os.path.join(top, d, '__init__.py'))
230 if (not os.path.exists(os.path.join(top, d, '__init__.py'))
229 or top == libpath and d in ('hgext', 'mercurial')):
231 or top == libpath and d in ('hgext', 'mercurial')):
230 del dirs[i]
232 del dirs[i]
231 for name in files:
233 for name in files:
232 if not name.endswith(('.py', '.so', '.pyc', '.pyo', '.pyd')):
234 if not name.endswith(('.py', '.so', '.pyc', '.pyo', '.pyd')):
233 continue
235 continue
234 if name.startswith('__init__.py'):
236 if name.startswith('__init__.py'):
235 full_path = top
237 full_path = top
236 else:
238 else:
237 full_path = os.path.join(top, name)
239 full_path = os.path.join(top, name)
238 rel_path = full_path[len(libpath) + 1:]
240 rel_path = full_path[len(libpath) + 1:]
239 mod = dotted_name_of_path(rel_path)
241 mod = dotted_name_of_path(rel_path)
240 yield mod
242 yield mod
241
243
242 stdlib_modules = set(list_stdlib_modules())
244 stdlib_modules = set(list_stdlib_modules())
243
245
244 def imported_modules(source, modulename, f, localmods, ignore_nested=False):
246 def imported_modules(source, modulename, f, localmods, ignore_nested=False):
245 """Given the source of a file as a string, yield the names
247 """Given the source of a file as a string, yield the names
246 imported by that file.
248 imported by that file.
247
249
248 Args:
250 Args:
249 source: The python source to examine as a string.
251 source: The python source to examine as a string.
250 modulename: of specified python source (may have `__init__`)
252 modulename: of specified python source (may have `__init__`)
251 localmods: dict of locally defined module names (may have `__init__`)
253 localmods: dict of locally defined module names (may have `__init__`)
252 ignore_nested: If true, import statements that do not start in
254 ignore_nested: If true, import statements that do not start in
253 column zero will be ignored.
255 column zero will be ignored.
254
256
255 Returns:
257 Returns:
256 A list of absolute module names imported by the given source.
258 A list of absolute module names imported by the given source.
257
259
258 >>> f = 'foo/xxx.py'
260 >>> f = 'foo/xxx.py'
259 >>> modulename = 'foo.xxx'
261 >>> modulename = 'foo.xxx'
260 >>> localmods = {'foo.__init__': True,
262 >>> localmods = {'foo.__init__': True,
261 ... 'foo.foo1': True, 'foo.foo2': True,
263 ... 'foo.foo1': True, 'foo.foo2': True,
262 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
264 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
263 ... 'baz.__init__': True, 'baz.baz1': True }
265 ... 'baz.__init__': True, 'baz.baz1': True }
264 >>> # standard library (= not locally defined ones)
266 >>> # standard library (= not locally defined ones)
265 >>> sorted(imported_modules(
267 >>> sorted(imported_modules(
266 ... 'from stdlib1 import foo, bar; import stdlib2',
268 ... 'from stdlib1 import foo, bar; import stdlib2',
267 ... modulename, f, localmods))
269 ... modulename, f, localmods))
268 []
270 []
269 >>> # relative importing
271 >>> # relative importing
270 >>> sorted(imported_modules(
272 >>> sorted(imported_modules(
271 ... 'import foo1; from bar import bar1',
273 ... 'import foo1; from bar import bar1',
272 ... modulename, f, localmods))
274 ... modulename, f, localmods))
273 ['foo.bar.bar1', 'foo.foo1']
275 ['foo.bar.bar1', 'foo.foo1']
274 >>> sorted(imported_modules(
276 >>> sorted(imported_modules(
275 ... 'from bar.bar1 import name1, name2, name3',
277 ... 'from bar.bar1 import name1, name2, name3',
276 ... modulename, f, localmods))
278 ... modulename, f, localmods))
277 ['foo.bar.bar1']
279 ['foo.bar.bar1']
278 >>> # absolute importing
280 >>> # absolute importing
279 >>> sorted(imported_modules(
281 >>> sorted(imported_modules(
280 ... 'from baz import baz1, name1',
282 ... 'from baz import baz1, name1',
281 ... modulename, f, localmods))
283 ... modulename, f, localmods))
282 ['baz.__init__', 'baz.baz1']
284 ['baz.__init__', 'baz.baz1']
283 >>> # mixed importing, even though it shouldn't be recommended
285 >>> # mixed importing, even though it shouldn't be recommended
284 >>> sorted(imported_modules(
286 >>> sorted(imported_modules(
285 ... 'import stdlib, foo1, baz',
287 ... 'import stdlib, foo1, baz',
286 ... modulename, f, localmods))
288 ... modulename, f, localmods))
287 ['baz.__init__', 'foo.foo1']
289 ['baz.__init__', 'foo.foo1']
288 >>> # ignore_nested
290 >>> # ignore_nested
289 >>> sorted(imported_modules(
291 >>> sorted(imported_modules(
290 ... '''import foo
292 ... '''import foo
291 ... def wat():
293 ... def wat():
292 ... import bar
294 ... import bar
293 ... ''', modulename, f, localmods))
295 ... ''', modulename, f, localmods))
294 ['foo.__init__', 'foo.bar.__init__']
296 ['foo.__init__', 'foo.bar.__init__']
295 >>> sorted(imported_modules(
297 >>> sorted(imported_modules(
296 ... '''import foo
298 ... '''import foo
297 ... def wat():
299 ... def wat():
298 ... import bar
300 ... import bar
299 ... ''', modulename, f, localmods, ignore_nested=True))
301 ... ''', modulename, f, localmods, ignore_nested=True))
300 ['foo.__init__']
302 ['foo.__init__']
301 """
303 """
302 fromlocal = fromlocalfunc(modulename, localmods)
304 fromlocal = fromlocalfunc(modulename, localmods)
303 for node in ast.walk(ast.parse(source, f)):
305 for node in ast.walk(ast.parse(source, f)):
304 if ignore_nested and getattr(node, 'col_offset', 0) > 0:
306 if ignore_nested and getattr(node, 'col_offset', 0) > 0:
305 continue
307 continue
306 if isinstance(node, ast.Import):
308 if isinstance(node, ast.Import):
307 for n in node.names:
309 for n in node.names:
308 found = fromlocal(n.name)
310 found = fromlocal(n.name)
309 if not found:
311 if not found:
310 # this should import standard library
312 # this should import standard library
311 continue
313 continue
312 yield found[1]
314 yield found[1]
313 elif isinstance(node, ast.ImportFrom):
315 elif isinstance(node, ast.ImportFrom):
314 found = fromlocal(node.module, node.level)
316 found = fromlocal(node.module, node.level)
315 if not found:
317 if not found:
316 # this should import standard library
318 # this should import standard library
317 continue
319 continue
318
320
319 absname, dottedpath, hassubmod = found
321 absname, dottedpath, hassubmod = found
320 if not hassubmod:
322 if not hassubmod:
321 # "dottedpath" is not a package; must be imported
323 # "dottedpath" is not a package; must be imported
322 yield dottedpath
324 yield dottedpath
323 # examination of "node.names" should be redundant
325 # examination of "node.names" should be redundant
324 # e.g.: from mercurial.node import nullid, nullrev
326 # e.g.: from mercurial.node import nullid, nullrev
325 continue
327 continue
326
328
327 modnotfound = False
329 modnotfound = False
328 prefix = absname + '.'
330 prefix = absname + '.'
329 for n in node.names:
331 for n in node.names:
330 found = fromlocal(prefix + n.name)
332 found = fromlocal(prefix + n.name)
331 if not found:
333 if not found:
332 # this should be a function or a property of "node.module"
334 # this should be a function or a property of "node.module"
333 modnotfound = True
335 modnotfound = True
334 continue
336 continue
335 yield found[1]
337 yield found[1]
336 if modnotfound:
338 if modnotfound:
337 # "dottedpath" is a package, but imported because of non-module
339 # "dottedpath" is a package, but imported because of non-module
338 # lookup
340 # lookup
339 yield dottedpath
341 yield dottedpath
340
342
341 def verify_import_convention(module, source, localmods):
343 def verify_import_convention(module, source, localmods):
342 """Verify imports match our established coding convention.
344 """Verify imports match our established coding convention.
343
345
344 We have 2 conventions: legacy and modern. The modern convention is in
346 We have 2 conventions: legacy and modern. The modern convention is in
345 effect when using absolute imports.
347 effect when using absolute imports.
346
348
347 The legacy convention only looks for mixed imports. The modern convention
349 The legacy convention only looks for mixed imports. The modern convention
348 is much more thorough.
350 is much more thorough.
349 """
351 """
350 root = ast.parse(source)
352 root = ast.parse(source)
351 absolute = usingabsolute(root)
353 absolute = usingabsolute(root)
352
354
353 if absolute:
355 if absolute:
354 return verify_modern_convention(module, root, localmods)
356 return verify_modern_convention(module, root, localmods)
355 else:
357 else:
356 return verify_stdlib_on_own_line(root)
358 return verify_stdlib_on_own_line(root)
357
359
358 def verify_modern_convention(module, root, localmods, root_col_offset=0):
360 def verify_modern_convention(module, root, localmods, root_col_offset=0):
359 """Verify a file conforms to the modern import convention rules.
361 """Verify a file conforms to the modern import convention rules.
360
362
361 The rules of the modern convention are:
363 The rules of the modern convention are:
362
364
363 * Ordering is stdlib followed by local imports. Each group is lexically
365 * Ordering is stdlib followed by local imports. Each group is lexically
364 sorted.
366 sorted.
365 * Importing multiple modules via "import X, Y" is not allowed: use
367 * Importing multiple modules via "import X, Y" is not allowed: use
366 separate import statements.
368 separate import statements.
367 * Importing multiple modules via "from X import ..." is allowed if using
369 * Importing multiple modules via "from X import ..." is allowed if using
368 parenthesis and one entry per line.
370 parenthesis and one entry per line.
369 * Only 1 relative import statement per import level ("from .", "from ..")
371 * Only 1 relative import statement per import level ("from .", "from ..")
370 is allowed.
372 is allowed.
371 * Relative imports from higher levels must occur before lower levels. e.g.
373 * Relative imports from higher levels must occur before lower levels. e.g.
372 "from .." must be before "from .".
374 "from .." must be before "from .".
373 * Imports from peer packages should use relative import (e.g. do not
375 * Imports from peer packages should use relative import (e.g. do not
374 "import mercurial.foo" from a "mercurial.*" module).
376 "import mercurial.foo" from a "mercurial.*" module).
375 * Symbols can only be imported from specific modules (see
377 * Symbols can only be imported from specific modules (see
376 `allowsymbolimports`). For other modules, first import the module then
378 `allowsymbolimports`). For other modules, first import the module then
377 assign the symbol to a module-level variable. In addition, these imports
379 assign the symbol to a module-level variable. In addition, these imports
378 must be performed before other local imports. This rule only
380 must be performed before other local imports. This rule only
379 applies to import statements outside of any blocks.
381 applies to import statements outside of any blocks.
380 * Relative imports from the standard library are not allowed.
382 * Relative imports from the standard library are not allowed.
381 * Certain modules must be aliased to alternate names to avoid aliasing
383 * Certain modules must be aliased to alternate names to avoid aliasing
382 and readability problems. See `requirealias`.
384 and readability problems. See `requirealias`.
383 """
385 """
384 topmodule = module.split('.')[0]
386 topmodule = module.split('.')[0]
385 fromlocal = fromlocalfunc(module, localmods)
387 fromlocal = fromlocalfunc(module, localmods)
386
388
387 # Whether a local/non-stdlib import has been performed.
389 # Whether a local/non-stdlib import has been performed.
388 seenlocal = None
390 seenlocal = None
389 # Whether a local/non-stdlib, non-symbol import has been seen.
391 # Whether a local/non-stdlib, non-symbol import has been seen.
390 seennonsymbollocal = False
392 seennonsymbollocal = False
391 # The last name to be imported (for sorting).
393 # The last name to be imported (for sorting).
392 lastname = None
394 lastname = None
393 laststdlib = None
395 laststdlib = None
394 # Relative import levels encountered so far.
396 # Relative import levels encountered so far.
395 seenlevels = set()
397 seenlevels = set()
396
398
397 for node, newscope in walklocal(root):
399 for node, newscope in walklocal(root):
398 def msg(fmt, *args):
400 def msg(fmt, *args):
399 return (fmt % args, node.lineno)
401 return (fmt % args, node.lineno)
400 if newscope:
402 if newscope:
401 # Check for local imports in function
403 # Check for local imports in function
402 for r in verify_modern_convention(module, node, localmods,
404 for r in verify_modern_convention(module, node, localmods,
403 node.col_offset + 4):
405 node.col_offset + 4):
404 yield r
406 yield r
405 elif isinstance(node, ast.Import):
407 elif isinstance(node, ast.Import):
406 # Disallow "import foo, bar" and require separate imports
408 # Disallow "import foo, bar" and require separate imports
407 # for each module.
409 # for each module.
408 if len(node.names) > 1:
410 if len(node.names) > 1:
409 yield msg('multiple imported names: %s',
411 yield msg('multiple imported names: %s',
410 ', '.join(n.name for n in node.names))
412 ', '.join(n.name for n in node.names))
411
413
412 name = node.names[0].name
414 name = node.names[0].name
413 asname = node.names[0].asname
415 asname = node.names[0].asname
414
416
415 stdlib = name in stdlib_modules
417 stdlib = name in stdlib_modules
416
418
417 # Ignore sorting rules on imports inside blocks.
419 # Ignore sorting rules on imports inside blocks.
418 if node.col_offset == root_col_offset:
420 if node.col_offset == root_col_offset:
419 if lastname and name < lastname and laststdlib == stdlib:
421 if lastname and name < lastname and laststdlib == stdlib:
420 yield msg('imports not lexically sorted: %s < %s',
422 yield msg('imports not lexically sorted: %s < %s',
421 name, lastname)
423 name, lastname)
422
424
423 lastname = name
425 lastname = name
424 laststdlib = stdlib
426 laststdlib = stdlib
425
427
426 # stdlib imports should be before local imports.
428 # stdlib imports should be before local imports.
427 if stdlib and seenlocal and node.col_offset == root_col_offset:
429 if stdlib and seenlocal and node.col_offset == root_col_offset:
428 yield msg('stdlib import "%s" follows local import: %s',
430 yield msg('stdlib import "%s" follows local import: %s',
429 name, seenlocal)
431 name, seenlocal)
430
432
431 if not stdlib:
433 if not stdlib:
432 seenlocal = name
434 seenlocal = name
433
435
434 # Import of sibling modules should use relative imports.
436 # Import of sibling modules should use relative imports.
435 topname = name.split('.')[0]
437 topname = name.split('.')[0]
436 if topname == topmodule:
438 if topname == topmodule:
437 yield msg('import should be relative: %s', name)
439 yield msg('import should be relative: %s', name)
438
440
439 if name in requirealias and asname != requirealias[name]:
441 if name in requirealias and asname != requirealias[name]:
440 yield msg('%s module must be "as" aliased to %s',
442 yield msg('%s module must be "as" aliased to %s',
441 name, requirealias[name])
443 name, requirealias[name])
442
444
443 elif isinstance(node, ast.ImportFrom):
445 elif isinstance(node, ast.ImportFrom):
444 # Resolve the full imported module name.
446 # Resolve the full imported module name.
445 if node.level > 0:
447 if node.level > 0:
446 fullname = '.'.join(module.split('.')[:-node.level])
448 fullname = '.'.join(module.split('.')[:-node.level])
447 if node.module:
449 if node.module:
448 fullname += '.%s' % node.module
450 fullname += '.%s' % node.module
449 else:
451 else:
450 assert node.module
452 assert node.module
451 fullname = node.module
453 fullname = node.module
452
454
453 topname = fullname.split('.')[0]
455 topname = fullname.split('.')[0]
454 if topname == topmodule:
456 if topname == topmodule:
455 yield msg('import should be relative: %s', fullname)
457 yield msg('import should be relative: %s', fullname)
456
458
457 # __future__ is special since it needs to come first and use
459 # __future__ is special since it needs to come first and use
458 # symbol import.
460 # symbol import.
459 if fullname != '__future__':
461 if fullname != '__future__':
460 if not fullname or fullname in stdlib_modules:
462 if not fullname or fullname in stdlib_modules:
461 yield msg('relative import of stdlib module')
463 yield msg('relative import of stdlib module')
462 else:
464 else:
463 seenlocal = fullname
465 seenlocal = fullname
464
466
465 # Direct symbol import is only allowed from certain modules and
467 # Direct symbol import is only allowed from certain modules and
466 # must occur before non-symbol imports.
468 # must occur before non-symbol imports.
467 found = fromlocal(node.module, node.level)
469 found = fromlocal(node.module, node.level)
468 if found and found[2]: # node.module is a package
470 if found and found[2]: # node.module is a package
469 prefix = found[0] + '.'
471 prefix = found[0] + '.'
470 symbols = (n.name for n in node.names
472 symbols = (n.name for n in node.names
471 if not fromlocal(prefix + n.name))
473 if not fromlocal(prefix + n.name))
472 else:
474 else:
473 symbols = (n.name for n in node.names)
475 symbols = (n.name for n in node.names)
474 symbols = [sym for sym in symbols if sym not in directsymbols]
476 symbols = [sym for sym in symbols if sym not in directsymbols]
475 if node.module and node.col_offset == root_col_offset:
477 if node.module and node.col_offset == root_col_offset:
476 if symbols and fullname not in allowsymbolimports:
478 if symbols and fullname not in allowsymbolimports:
477 yield msg('direct symbol import %s from %s',
479 yield msg('direct symbol import %s from %s',
478 ', '.join(symbols), fullname)
480 ', '.join(symbols), fullname)
479
481
480 if symbols and seennonsymbollocal:
482 if symbols and seennonsymbollocal:
481 yield msg('symbol import follows non-symbol import: %s',
483 yield msg('symbol import follows non-symbol import: %s',
482 fullname)
484 fullname)
483 if not symbols and fullname not in stdlib_modules:
485 if not symbols and fullname not in stdlib_modules:
484 seennonsymbollocal = True
486 seennonsymbollocal = True
485
487
486 if not node.module:
488 if not node.module:
487 assert node.level
489 assert node.level
488
490
489 # Only allow 1 group per level.
491 # Only allow 1 group per level.
490 if (node.level in seenlevels
492 if (node.level in seenlevels
491 and node.col_offset == root_col_offset):
493 and node.col_offset == root_col_offset):
492 yield msg('multiple "from %s import" statements',
494 yield msg('multiple "from %s import" statements',
493 '.' * node.level)
495 '.' * node.level)
494
496
495 # Higher-level groups come before lower-level groups.
497 # Higher-level groups come before lower-level groups.
496 if any(node.level > l for l in seenlevels):
498 if any(node.level > l for l in seenlevels):
497 yield msg('higher-level import should come first: %s',
499 yield msg('higher-level import should come first: %s',
498 fullname)
500 fullname)
499
501
500 seenlevels.add(node.level)
502 seenlevels.add(node.level)
501
503
502 # Entries in "from .X import ( ... )" lists must be lexically
504 # Entries in "from .X import ( ... )" lists must be lexically
503 # sorted.
505 # sorted.
504 lastentryname = None
506 lastentryname = None
505
507
506 for n in node.names:
508 for n in node.names:
507 if lastentryname and n.name < lastentryname:
509 if lastentryname and n.name < lastentryname:
508 yield msg('imports from %s not lexically sorted: %s < %s',
510 yield msg('imports from %s not lexically sorted: %s < %s',
509 fullname, n.name, lastentryname)
511 fullname, n.name, lastentryname)
510
512
511 lastentryname = n.name
513 lastentryname = n.name
512
514
513 if n.name in requirealias and n.asname != requirealias[n.name]:
515 if n.name in requirealias and n.asname != requirealias[n.name]:
514 yield msg('%s from %s must be "as" aliased to %s',
516 yield msg('%s from %s must be "as" aliased to %s',
515 n.name, fullname, requirealias[n.name])
517 n.name, fullname, requirealias[n.name])
516
518
517 def verify_stdlib_on_own_line(root):
519 def verify_stdlib_on_own_line(root):
518 """Given some python source, verify that stdlib imports are done
520 """Given some python source, verify that stdlib imports are done
519 in separate statements from relative local module imports.
521 in separate statements from relative local module imports.
520
522
521 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, foo')))
523 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, foo')))
522 [('mixed imports\\n stdlib: sys\\n relative: foo', 1)]
524 [('mixed imports\\n stdlib: sys\\n relative: foo', 1)]
523 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, os')))
525 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, os')))
524 []
526 []
525 >>> list(verify_stdlib_on_own_line(ast.parse('import foo, bar')))
527 >>> list(verify_stdlib_on_own_line(ast.parse('import foo, bar')))
526 []
528 []
527 """
529 """
528 for node in ast.walk(root):
530 for node in ast.walk(root):
529 if isinstance(node, ast.Import):
531 if isinstance(node, ast.Import):
530 from_stdlib = {False: [], True: []}
532 from_stdlib = {False: [], True: []}
531 for n in node.names:
533 for n in node.names:
532 from_stdlib[n.name in stdlib_modules].append(n.name)
534 from_stdlib[n.name in stdlib_modules].append(n.name)
533 if from_stdlib[True] and from_stdlib[False]:
535 if from_stdlib[True] and from_stdlib[False]:
534 yield ('mixed imports\n stdlib: %s\n relative: %s' %
536 yield ('mixed imports\n stdlib: %s\n relative: %s' %
535 (', '.join(sorted(from_stdlib[True])),
537 (', '.join(sorted(from_stdlib[True])),
536 ', '.join(sorted(from_stdlib[False]))), node.lineno)
538 ', '.join(sorted(from_stdlib[False]))), node.lineno)
537
539
538 class CircularImport(Exception):
540 class CircularImport(Exception):
539 pass
541 pass
540
542
541 def checkmod(mod, imports):
543 def checkmod(mod, imports):
542 shortest = {}
544 shortest = {}
543 visit = [[mod]]
545 visit = [[mod]]
544 while visit:
546 while visit:
545 path = visit.pop(0)
547 path = visit.pop(0)
546 for i in sorted(imports.get(path[-1], [])):
548 for i in sorted(imports.get(path[-1], [])):
547 if len(path) < shortest.get(i, 1000):
549 if len(path) < shortest.get(i, 1000):
548 shortest[i] = len(path)
550 shortest[i] = len(path)
549 if i in path:
551 if i in path:
550 if i == path[0]:
552 if i == path[0]:
551 raise CircularImport(path)
553 raise CircularImport(path)
552 continue
554 continue
553 visit.append(path + [i])
555 visit.append(path + [i])
554
556
555 def rotatecycle(cycle):
557 def rotatecycle(cycle):
556 """arrange a cycle so that the lexicographically first module listed first
558 """arrange a cycle so that the lexicographically first module listed first
557
559
558 >>> rotatecycle(['foo', 'bar'])
560 >>> rotatecycle(['foo', 'bar'])
559 ['bar', 'foo', 'bar']
561 ['bar', 'foo', 'bar']
560 """
562 """
561 lowest = min(cycle)
563 lowest = min(cycle)
562 idx = cycle.index(lowest)
564 idx = cycle.index(lowest)
563 return cycle[idx:] + cycle[:idx] + [lowest]
565 return cycle[idx:] + cycle[:idx] + [lowest]
564
566
565 def find_cycles(imports):
567 def find_cycles(imports):
566 """Find cycles in an already-loaded import graph.
568 """Find cycles in an already-loaded import graph.
567
569
568 All module names recorded in `imports` should be absolute one.
570 All module names recorded in `imports` should be absolute one.
569
571
570 >>> from __future__ import print_function
572 >>> from __future__ import print_function
571 >>> imports = {'top.foo': ['top.bar', 'os.path', 'top.qux'],
573 >>> imports = {'top.foo': ['top.bar', 'os.path', 'top.qux'],
572 ... 'top.bar': ['top.baz', 'sys'],
574 ... 'top.bar': ['top.baz', 'sys'],
573 ... 'top.baz': ['top.foo'],
575 ... 'top.baz': ['top.foo'],
574 ... 'top.qux': ['top.foo']}
576 ... 'top.qux': ['top.foo']}
575 >>> print('\\n'.join(sorted(find_cycles(imports))))
577 >>> print('\\n'.join(sorted(find_cycles(imports))))
576 top.bar -> top.baz -> top.foo -> top.bar
578 top.bar -> top.baz -> top.foo -> top.bar
577 top.foo -> top.qux -> top.foo
579 top.foo -> top.qux -> top.foo
578 """
580 """
579 cycles = set()
581 cycles = set()
580 for mod in sorted(imports.keys()):
582 for mod in sorted(imports.keys()):
581 try:
583 try:
582 checkmod(mod, imports)
584 checkmod(mod, imports)
583 except CircularImport as e:
585 except CircularImport as e:
584 cycle = e.args[0]
586 cycle = e.args[0]
585 cycles.add(" -> ".join(rotatecycle(cycle)))
587 cycles.add(" -> ".join(rotatecycle(cycle)))
586 return cycles
588 return cycles
587
589
588 def _cycle_sortkey(c):
590 def _cycle_sortkey(c):
589 return len(c), c
591 return len(c), c
590
592
591 def embedded(f, modname, src):
593 def embedded(f, modname, src):
592 """Extract embedded python code
594 """Extract embedded python code
593
595
594 >>> def test(fn, lines):
596 >>> def test(fn, lines):
595 ... for s, m, f, l in embedded(fn, "example", lines):
597 ... for s, m, f, l in embedded(fn, "example", lines):
596 ... print("%s %s %s" % (m, f, l))
598 ... print("%s %s %s" % (m, f, l))
597 ... print(repr(s))
599 ... print(repr(s))
598 >>> lines = [
600 >>> lines = [
599 ... 'comment',
601 ... 'comment',
600 ... ' >>> from __future__ import print_function',
602 ... ' >>> from __future__ import print_function',
601 ... " >>> ' multiline",
603 ... " >>> ' multiline",
602 ... " ... string'",
604 ... " ... string'",
603 ... ' ',
605 ... ' ',
604 ... 'comment',
606 ... 'comment',
605 ... ' $ cat > foo.py <<EOF',
607 ... ' $ cat > foo.py <<EOF',
606 ... ' > from __future__ import print_function',
608 ... ' > from __future__ import print_function',
607 ... ' > EOF',
609 ... ' > EOF',
608 ... ]
610 ... ]
609 >>> test("example.t", lines)
611 >>> test("example.t", lines)
610 example[2] doctest.py 2
612 example[2] doctest.py 2
611 "from __future__ import print_function\\n' multiline\\nstring'\\n"
613 "from __future__ import print_function\\n' multiline\\nstring'\\n"
612 example[7] foo.py 7
614 example[7] foo.py 7
613 'from __future__ import print_function\\n'
615 'from __future__ import print_function\\n'
614 """
616 """
615 inlinepython = 0
617 inlinepython = 0
616 shpython = 0
618 shpython = 0
617 script = []
619 script = []
618 prefix = 6
620 prefix = 6
619 t = ''
621 t = ''
620 n = 0
622 n = 0
621 for l in src:
623 for l in src:
622 n += 1
624 n += 1
623 if not l.endswith(b'\n'):
625 if not l.endswith(b'\n'):
624 l += b'\n'
626 l += b'\n'
625 if l.startswith(b' >>> '): # python inlines
627 if l.startswith(b' >>> '): # python inlines
626 if shpython:
628 if shpython:
627 print("%s:%d: Parse Error" % (f, n))
629 print("%s:%d: Parse Error" % (f, n))
628 if not inlinepython:
630 if not inlinepython:
629 # We've just entered a Python block.
631 # We've just entered a Python block.
630 inlinepython = n
632 inlinepython = n
631 t = 'doctest.py'
633 t = 'doctest.py'
632 script.append(l[prefix:])
634 script.append(l[prefix:])
633 continue
635 continue
634 if l.startswith(b' ... '): # python inlines
636 if l.startswith(b' ... '): # python inlines
635 script.append(l[prefix:])
637 script.append(l[prefix:])
636 continue
638 continue
637 cat = re.search(r"\$ \s*cat\s*>\s*(\S+\.py)\s*<<\s*EOF", l)
639 cat = re.search(r"\$ \s*cat\s*>\s*(\S+\.py)\s*<<\s*EOF", l)
638 if cat:
640 if cat:
639 if inlinepython:
641 if inlinepython:
640 yield ''.join(script), ("%s[%d]" %
642 yield ''.join(script), ("%s[%d]" %
641 (modname, inlinepython)), t, inlinepython
643 (modname, inlinepython)), t, inlinepython
642 script = []
644 script = []
643 inlinepython = 0
645 inlinepython = 0
644 shpython = n
646 shpython = n
645 t = cat.group(1)
647 t = cat.group(1)
646 continue
648 continue
647 if shpython and l.startswith(b' > '): # sh continuation
649 if shpython and l.startswith(b' > '): # sh continuation
648 if l == b' > EOF\n':
650 if l == b' > EOF\n':
649 yield ''.join(script), ("%s[%d]" %
651 yield ''.join(script), ("%s[%d]" %
650 (modname, shpython)), t, shpython
652 (modname, shpython)), t, shpython
651 script = []
653 script = []
652 shpython = 0
654 shpython = 0
653 else:
655 else:
654 script.append(l[4:])
656 script.append(l[4:])
655 continue
657 continue
656 if inlinepython and l == b' \n':
658 if inlinepython and l == b' \n':
657 yield ''.join(script), ("%s[%d]" %
659 yield ''.join(script), ("%s[%d]" %
658 (modname, inlinepython)), t, inlinepython
660 (modname, inlinepython)), t, inlinepython
659 script = []
661 script = []
660 inlinepython = 0
662 inlinepython = 0
661 continue
663 continue
662
664
663 def sources(f, modname):
665 def sources(f, modname):
664 """Yields possibly multiple sources from a filepath
666 """Yields possibly multiple sources from a filepath
665
667
666 input: filepath, modulename
668 input: filepath, modulename
667 yields: script(string), modulename, filepath, linenumber
669 yields: script(string), modulename, filepath, linenumber
668
670
669 For embedded scripts, the modulename and filepath will be different
671 For embedded scripts, the modulename and filepath will be different
670 from the function arguments. linenumber is an offset relative to
672 from the function arguments. linenumber is an offset relative to
671 the input file.
673 the input file.
672 """
674 """
673 py = False
675 py = False
674 if not f.endswith('.t'):
676 if not f.endswith('.t'):
675 with open(f) as src:
677 with open(f) as src:
676 yield src.read(), modname, f, 0
678 yield src.read(), modname, f, 0
677 py = True
679 py = True
678 if py or f.endswith('.t'):
680 if py or f.endswith('.t'):
679 with open(f) as src:
681 with open(f) as src:
680 for script, modname, t, line in embedded(f, modname, src):
682 for script, modname, t, line in embedded(f, modname, src):
681 yield script, modname, t, line
683 yield script, modname, t, line
682
684
683 def main(argv):
685 def main(argv):
684 if len(argv) < 2 or (argv[1] == '-' and len(argv) > 2):
686 if len(argv) < 2 or (argv[1] == '-' and len(argv) > 2):
685 print('Usage: %s {-|file [file] [file] ...}')
687 print('Usage: %s {-|file [file] [file] ...}')
686 return 1
688 return 1
687 if argv[1] == '-':
689 if argv[1] == '-':
688 argv = argv[:1]
690 argv = argv[:1]
689 argv.extend(l.rstrip() for l in sys.stdin.readlines())
691 argv.extend(l.rstrip() for l in sys.stdin.readlines())
690 localmods = {}
692 localmods = {}
691 used_imports = {}
693 used_imports = {}
692 any_errors = False
694 any_errors = False
693 for source_path in argv[1:]:
695 for source_path in argv[1:]:
694 modname = dotted_name_of_path(source_path)
696 modname = dotted_name_of_path(source_path)
695 localmods[modname] = source_path
697 localmods[modname] = source_path
696 for localmodname, source_path in sorted(localmods.items()):
698 for localmodname, source_path in sorted(localmods.items()):
697 for src, modname, name, line in sources(source_path, localmodname):
699 for src, modname, name, line in sources(source_path, localmodname):
698 try:
700 try:
699 used_imports[modname] = sorted(
701 used_imports[modname] = sorted(
700 imported_modules(src, modname, name, localmods,
702 imported_modules(src, modname, name, localmods,
701 ignore_nested=True))
703 ignore_nested=True))
702 for error, lineno in verify_import_convention(modname, src,
704 for error, lineno in verify_import_convention(modname, src,
703 localmods):
705 localmods):
704 any_errors = True
706 any_errors = True
705 print('%s:%d: %s' % (source_path, lineno + line, error))
707 print('%s:%d: %s' % (source_path, lineno + line, error))
706 except SyntaxError as e:
708 except SyntaxError as e:
707 print('%s:%d: SyntaxError: %s' %
709 print('%s:%d: SyntaxError: %s' %
708 (source_path, e.lineno + line, e))
710 (source_path, e.lineno + line, e))
709 cycles = find_cycles(used_imports)
711 cycles = find_cycles(used_imports)
710 if cycles:
712 if cycles:
711 firstmods = set()
713 firstmods = set()
712 for c in sorted(cycles, key=_cycle_sortkey):
714 for c in sorted(cycles, key=_cycle_sortkey):
713 first = c.split()[0]
715 first = c.split()[0]
714 # As a rough cut, ignore any cycle that starts with the
716 # As a rough cut, ignore any cycle that starts with the
715 # same module as some other cycle. Otherwise we see lots
717 # same module as some other cycle. Otherwise we see lots
716 # of cycles that are effectively duplicates.
718 # of cycles that are effectively duplicates.
717 if first in firstmods:
719 if first in firstmods:
718 continue
720 continue
719 print('Import cycle:', c)
721 print('Import cycle:', c)
720 firstmods.add(first)
722 firstmods.add(first)
721 any_errors = True
723 any_errors = True
722 return any_errors != 0
724 return any_errors != 0
723
725
724 if __name__ == '__main__':
726 if __name__ == '__main__':
725 sys.exit(int(main(sys.argv)))
727 sys.exit(int(main(sys.argv)))
1 NO CONTENT: file renamed from mercurial/demandimport.py to hgdemandimport/demandimportpy2.py
NO CONTENT: file renamed from mercurial/demandimport.py to hgdemandimport/demandimportpy2.py
@@ -1,279 +1,283
1 # __init__.py - Startup and module loading logic for Mercurial.
1 # __init__.py - Startup and module loading logic for Mercurial.
2 #
2 #
3 # Copyright 2015 Gregory Szorc <gregory.szorc@gmail.com>
3 # Copyright 2015 Gregory Szorc <gregory.szorc@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import sys
10 import sys
11
11
12 # Allow 'from mercurial import demandimport' to keep working.
13 import hgdemandimport
14 demandimport = hgdemandimport
15
12 __all__ = []
16 __all__ = []
13
17
14 # Python 3 uses a custom module loader that transforms source code between
18 # Python 3 uses a custom module loader that transforms source code between
15 # source file reading and compilation. This is done by registering a custom
19 # source file reading and compilation. This is done by registering a custom
16 # finder that changes the spec for Mercurial modules to use a custom loader.
20 # finder that changes the spec for Mercurial modules to use a custom loader.
17 if sys.version_info[0] >= 3:
21 if sys.version_info[0] >= 3:
18 import importlib
22 import importlib
19 import importlib.abc
23 import importlib.abc
20 import io
24 import io
21 import token
25 import token
22 import tokenize
26 import tokenize
23
27
24 class hgpathentryfinder(importlib.abc.MetaPathFinder):
28 class hgpathentryfinder(importlib.abc.MetaPathFinder):
25 """A sys.meta_path finder that uses a custom module loader."""
29 """A sys.meta_path finder that uses a custom module loader."""
26 def find_spec(self, fullname, path, target=None):
30 def find_spec(self, fullname, path, target=None):
27 # Only handle Mercurial-related modules.
31 # Only handle Mercurial-related modules.
28 if not fullname.startswith(('mercurial.', 'hgext.', 'hgext3rd.')):
32 if not fullname.startswith(('mercurial.', 'hgext.', 'hgext3rd.')):
29 return None
33 return None
30 # zstd is already dual-version clean, don't try and mangle it
34 # zstd is already dual-version clean, don't try and mangle it
31 if fullname.startswith('mercurial.zstd'):
35 if fullname.startswith('mercurial.zstd'):
32 return None
36 return None
33
37
34 # Try to find the module using other registered finders.
38 # Try to find the module using other registered finders.
35 spec = None
39 spec = None
36 for finder in sys.meta_path:
40 for finder in sys.meta_path:
37 if finder == self:
41 if finder == self:
38 continue
42 continue
39
43
40 spec = finder.find_spec(fullname, path, target=target)
44 spec = finder.find_spec(fullname, path, target=target)
41 if spec:
45 if spec:
42 break
46 break
43
47
44 # This is a Mercurial-related module but we couldn't find it
48 # This is a Mercurial-related module but we couldn't find it
45 # using the previously-registered finders. This likely means
49 # using the previously-registered finders. This likely means
46 # the module doesn't exist.
50 # the module doesn't exist.
47 if not spec:
51 if not spec:
48 return None
52 return None
49
53
50 # TODO need to support loaders from alternate specs, like zip
54 # TODO need to support loaders from alternate specs, like zip
51 # loaders.
55 # loaders.
52 spec.loader = hgloader(spec.name, spec.origin)
56 spec.loader = hgloader(spec.name, spec.origin)
53 return spec
57 return spec
54
58
55 def replacetokens(tokens, fullname):
59 def replacetokens(tokens, fullname):
56 """Transform a stream of tokens from raw to Python 3.
60 """Transform a stream of tokens from raw to Python 3.
57
61
58 It is called by the custom module loading machinery to rewrite
62 It is called by the custom module loading machinery to rewrite
59 source/tokens between source decoding and compilation.
63 source/tokens between source decoding and compilation.
60
64
61 Returns a generator of possibly rewritten tokens.
65 Returns a generator of possibly rewritten tokens.
62
66
63 The input token list may be mutated as part of processing. However,
67 The input token list may be mutated as part of processing. However,
64 its changes do not necessarily match the output token stream.
68 its changes do not necessarily match the output token stream.
65
69
66 REMEMBER TO CHANGE ``BYTECODEHEADER`` WHEN CHANGING THIS FUNCTION
70 REMEMBER TO CHANGE ``BYTECODEHEADER`` WHEN CHANGING THIS FUNCTION
67 OR CACHED FILES WON'T GET INVALIDATED PROPERLY.
71 OR CACHED FILES WON'T GET INVALIDATED PROPERLY.
68 """
72 """
69 futureimpline = False
73 futureimpline = False
70
74
71 # The following utility functions access the tokens list and i index of
75 # The following utility functions access the tokens list and i index of
72 # the for i, t enumerate(tokens) loop below
76 # the for i, t enumerate(tokens) loop below
73 def _isop(j, *o):
77 def _isop(j, *o):
74 """Assert that tokens[j] is an OP with one of the given values"""
78 """Assert that tokens[j] is an OP with one of the given values"""
75 try:
79 try:
76 return tokens[j].type == token.OP and tokens[j].string in o
80 return tokens[j].type == token.OP and tokens[j].string in o
77 except IndexError:
81 except IndexError:
78 return False
82 return False
79
83
80 def _findargnofcall(n):
84 def _findargnofcall(n):
81 """Find arg n of a call expression (start at 0)
85 """Find arg n of a call expression (start at 0)
82
86
83 Returns index of the first token of that argument, or None if
87 Returns index of the first token of that argument, or None if
84 there is not that many arguments.
88 there is not that many arguments.
85
89
86 Assumes that token[i + 1] is '('.
90 Assumes that token[i + 1] is '('.
87
91
88 """
92 """
89 nested = 0
93 nested = 0
90 for j in range(i + 2, len(tokens)):
94 for j in range(i + 2, len(tokens)):
91 if _isop(j, ')', ']', '}'):
95 if _isop(j, ')', ']', '}'):
92 # end of call, tuple, subscription or dict / set
96 # end of call, tuple, subscription or dict / set
93 nested -= 1
97 nested -= 1
94 if nested < 0:
98 if nested < 0:
95 return None
99 return None
96 elif n == 0:
100 elif n == 0:
97 # this is the starting position of arg
101 # this is the starting position of arg
98 return j
102 return j
99 elif _isop(j, '(', '[', '{'):
103 elif _isop(j, '(', '[', '{'):
100 nested += 1
104 nested += 1
101 elif _isop(j, ',') and nested == 0:
105 elif _isop(j, ',') and nested == 0:
102 n -= 1
106 n -= 1
103
107
104 return None
108 return None
105
109
106 def _ensureunicode(j):
110 def _ensureunicode(j):
107 """Make sure the token at j is a unicode string
111 """Make sure the token at j is a unicode string
108
112
109 This rewrites a string token to include the unicode literal prefix
113 This rewrites a string token to include the unicode literal prefix
110 so the string transformer won't add the byte prefix.
114 so the string transformer won't add the byte prefix.
111
115
112 Ignores tokens that are not strings. Assumes bounds checking has
116 Ignores tokens that are not strings. Assumes bounds checking has
113 already been done.
117 already been done.
114
118
115 """
119 """
116 st = tokens[j]
120 st = tokens[j]
117 if st.type == token.STRING and st.string.startswith(("'", '"')):
121 if st.type == token.STRING and st.string.startswith(("'", '"')):
118 tokens[j] = st._replace(string='u%s' % st.string)
122 tokens[j] = st._replace(string='u%s' % st.string)
119
123
120 for i, t in enumerate(tokens):
124 for i, t in enumerate(tokens):
121 # Convert most string literals to byte literals. String literals
125 # Convert most string literals to byte literals. String literals
122 # in Python 2 are bytes. String literals in Python 3 are unicode.
126 # in Python 2 are bytes. String literals in Python 3 are unicode.
123 # Most strings in Mercurial are bytes and unicode strings are rare.
127 # Most strings in Mercurial are bytes and unicode strings are rare.
124 # Rather than rewrite all string literals to use ``b''`` to indicate
128 # Rather than rewrite all string literals to use ``b''`` to indicate
125 # byte strings, we apply this token transformer to insert the ``b``
129 # byte strings, we apply this token transformer to insert the ``b``
126 # prefix nearly everywhere.
130 # prefix nearly everywhere.
127 if t.type == token.STRING:
131 if t.type == token.STRING:
128 s = t.string
132 s = t.string
129
133
130 # Preserve docstrings as string literals. This is inconsistent
134 # Preserve docstrings as string literals. This is inconsistent
131 # with regular unprefixed strings. However, the
135 # with regular unprefixed strings. However, the
132 # "from __future__" parsing (which allows a module docstring to
136 # "from __future__" parsing (which allows a module docstring to
133 # exist before it) doesn't properly handle the docstring if it
137 # exist before it) doesn't properly handle the docstring if it
134 # is b''' prefixed, leading to a SyntaxError. We leave all
138 # is b''' prefixed, leading to a SyntaxError. We leave all
135 # docstrings as unprefixed to avoid this. This means Mercurial
139 # docstrings as unprefixed to avoid this. This means Mercurial
136 # components touching docstrings need to handle unicode,
140 # components touching docstrings need to handle unicode,
137 # unfortunately.
141 # unfortunately.
138 if s[0:3] in ("'''", '"""'):
142 if s[0:3] in ("'''", '"""'):
139 yield t
143 yield t
140 continue
144 continue
141
145
142 # If the first character isn't a quote, it is likely a string
146 # If the first character isn't a quote, it is likely a string
143 # prefixing character (such as 'b', 'u', or 'r'. Ignore.
147 # prefixing character (such as 'b', 'u', or 'r'. Ignore.
144 if s[0] not in ("'", '"'):
148 if s[0] not in ("'", '"'):
145 yield t
149 yield t
146 continue
150 continue
147
151
148 # String literal. Prefix to make a b'' string.
152 # String literal. Prefix to make a b'' string.
149 yield t._replace(string='b%s' % t.string)
153 yield t._replace(string='b%s' % t.string)
150 continue
154 continue
151
155
152 # Insert compatibility imports at "from __future__ import" line.
156 # Insert compatibility imports at "from __future__ import" line.
153 # No '\n' should be added to preserve line numbers.
157 # No '\n' should be added to preserve line numbers.
154 if (t.type == token.NAME and t.string == 'import' and
158 if (t.type == token.NAME and t.string == 'import' and
155 all(u.type == token.NAME for u in tokens[i - 2:i]) and
159 all(u.type == token.NAME for u in tokens[i - 2:i]) and
156 [u.string for u in tokens[i - 2:i]] == ['from', '__future__']):
160 [u.string for u in tokens[i - 2:i]] == ['from', '__future__']):
157 futureimpline = True
161 futureimpline = True
158 if t.type == token.NEWLINE and futureimpline:
162 if t.type == token.NEWLINE and futureimpline:
159 futureimpline = False
163 futureimpline = False
160 if fullname == 'mercurial.pycompat':
164 if fullname == 'mercurial.pycompat':
161 yield t
165 yield t
162 continue
166 continue
163 r, c = t.start
167 r, c = t.start
164 l = (b'; from mercurial.pycompat import '
168 l = (b'; from mercurial.pycompat import '
165 b'delattr, getattr, hasattr, setattr, xrange, '
169 b'delattr, getattr, hasattr, setattr, xrange, '
166 b'open, unicode\n')
170 b'open, unicode\n')
167 for u in tokenize.tokenize(io.BytesIO(l).readline):
171 for u in tokenize.tokenize(io.BytesIO(l).readline):
168 if u.type in (tokenize.ENCODING, token.ENDMARKER):
172 if u.type in (tokenize.ENCODING, token.ENDMARKER):
169 continue
173 continue
170 yield u._replace(
174 yield u._replace(
171 start=(r, c + u.start[1]), end=(r, c + u.end[1]))
175 start=(r, c + u.start[1]), end=(r, c + u.end[1]))
172 continue
176 continue
173
177
174 # This looks like a function call.
178 # This looks like a function call.
175 if t.type == token.NAME and _isop(i + 1, '('):
179 if t.type == token.NAME and _isop(i + 1, '('):
176 fn = t.string
180 fn = t.string
177
181
178 # *attr() builtins don't accept byte strings to 2nd argument.
182 # *attr() builtins don't accept byte strings to 2nd argument.
179 if (fn in ('getattr', 'setattr', 'hasattr', 'safehasattr') and
183 if (fn in ('getattr', 'setattr', 'hasattr', 'safehasattr') and
180 not _isop(i - 1, '.')):
184 not _isop(i - 1, '.')):
181 arg1idx = _findargnofcall(1)
185 arg1idx = _findargnofcall(1)
182 if arg1idx is not None:
186 if arg1idx is not None:
183 _ensureunicode(arg1idx)
187 _ensureunicode(arg1idx)
184
188
185 # .encode() and .decode() on str/bytes/unicode don't accept
189 # .encode() and .decode() on str/bytes/unicode don't accept
186 # byte strings on Python 3.
190 # byte strings on Python 3.
187 elif fn in ('encode', 'decode') and _isop(i - 1, '.'):
191 elif fn in ('encode', 'decode') and _isop(i - 1, '.'):
188 for argn in range(2):
192 for argn in range(2):
189 argidx = _findargnofcall(argn)
193 argidx = _findargnofcall(argn)
190 if argidx is not None:
194 if argidx is not None:
191 _ensureunicode(argidx)
195 _ensureunicode(argidx)
192
196
193 # It changes iteritems/values to items/values as they are not
197 # It changes iteritems/values to items/values as they are not
194 # present in Python 3 world.
198 # present in Python 3 world.
195 elif fn in ('iteritems', 'itervalues'):
199 elif fn in ('iteritems', 'itervalues'):
196 yield t._replace(string=fn[4:])
200 yield t._replace(string=fn[4:])
197 continue
201 continue
198
202
199 # Emit unmodified token.
203 # Emit unmodified token.
200 yield t
204 yield t
201
205
202 # Header to add to bytecode files. This MUST be changed when
206 # Header to add to bytecode files. This MUST be changed when
203 # ``replacetoken`` or any mechanism that changes semantics of module
207 # ``replacetoken`` or any mechanism that changes semantics of module
204 # loading is changed. Otherwise cached bytecode may get loaded without
208 # loading is changed. Otherwise cached bytecode may get loaded without
205 # the new transformation mechanisms applied.
209 # the new transformation mechanisms applied.
206 BYTECODEHEADER = b'HG\x00\x0a'
210 BYTECODEHEADER = b'HG\x00\x0a'
207
211
208 class hgloader(importlib.machinery.SourceFileLoader):
212 class hgloader(importlib.machinery.SourceFileLoader):
209 """Custom module loader that transforms source code.
213 """Custom module loader that transforms source code.
210
214
211 When the source code is converted to a code object, we transform
215 When the source code is converted to a code object, we transform
212 certain patterns to be Python 3 compatible. This allows us to write code
216 certain patterns to be Python 3 compatible. This allows us to write code
213 that is natively Python 2 and compatible with Python 3 without
217 that is natively Python 2 and compatible with Python 3 without
214 making the code excessively ugly.
218 making the code excessively ugly.
215
219
216 We do this by transforming the token stream between parse and compile.
220 We do this by transforming the token stream between parse and compile.
217
221
218 Implementing transformations invalidates caching assumptions made
222 Implementing transformations invalidates caching assumptions made
219 by the built-in importer. The built-in importer stores a header on
223 by the built-in importer. The built-in importer stores a header on
220 saved bytecode files indicating the Python/bytecode version. If the
224 saved bytecode files indicating the Python/bytecode version. If the
221 version changes, the cached bytecode is ignored. The Mercurial
225 version changes, the cached bytecode is ignored. The Mercurial
222 transformations could change at any time. This means we need to check
226 transformations could change at any time. This means we need to check
223 that cached bytecode was generated with the current transformation
227 that cached bytecode was generated with the current transformation
224 code or there could be a mismatch between cached bytecode and what
228 code or there could be a mismatch between cached bytecode and what
225 would be generated from this class.
229 would be generated from this class.
226
230
227 We supplement the bytecode caching layer by wrapping ``get_data``
231 We supplement the bytecode caching layer by wrapping ``get_data``
228 and ``set_data``. These functions are called when the
232 and ``set_data``. These functions are called when the
229 ``SourceFileLoader`` retrieves and saves bytecode cache files,
233 ``SourceFileLoader`` retrieves and saves bytecode cache files,
230 respectively. We simply add an additional header on the file. As
234 respectively. We simply add an additional header on the file. As
231 long as the version in this file is changed when semantics change,
235 long as the version in this file is changed when semantics change,
232 cached bytecode should be invalidated when transformations change.
236 cached bytecode should be invalidated when transformations change.
233
237
234 The added header has the form ``HG<VERSION>``. That is a literal
238 The added header has the form ``HG<VERSION>``. That is a literal
235 ``HG`` with 2 binary bytes indicating the transformation version.
239 ``HG`` with 2 binary bytes indicating the transformation version.
236 """
240 """
237 def get_data(self, path):
241 def get_data(self, path):
238 data = super(hgloader, self).get_data(path)
242 data = super(hgloader, self).get_data(path)
239
243
240 if not path.endswith(tuple(importlib.machinery.BYTECODE_SUFFIXES)):
244 if not path.endswith(tuple(importlib.machinery.BYTECODE_SUFFIXES)):
241 return data
245 return data
242
246
243 # There should be a header indicating the Mercurial transformation
247 # There should be a header indicating the Mercurial transformation
244 # version. If it doesn't exist or doesn't match the current version,
248 # version. If it doesn't exist or doesn't match the current version,
245 # we raise an OSError because that is what
249 # we raise an OSError because that is what
246 # ``SourceFileLoader.get_code()`` expects when loading bytecode
250 # ``SourceFileLoader.get_code()`` expects when loading bytecode
247 # paths to indicate the cached file is "bad."
251 # paths to indicate the cached file is "bad."
248 if data[0:2] != b'HG':
252 if data[0:2] != b'HG':
249 raise OSError('no hg header')
253 raise OSError('no hg header')
250 if data[0:4] != BYTECODEHEADER:
254 if data[0:4] != BYTECODEHEADER:
251 raise OSError('hg header version mismatch')
255 raise OSError('hg header version mismatch')
252
256
253 return data[4:]
257 return data[4:]
254
258
255 def set_data(self, path, data, *args, **kwargs):
259 def set_data(self, path, data, *args, **kwargs):
256 if path.endswith(tuple(importlib.machinery.BYTECODE_SUFFIXES)):
260 if path.endswith(tuple(importlib.machinery.BYTECODE_SUFFIXES)):
257 data = BYTECODEHEADER + data
261 data = BYTECODEHEADER + data
258
262
259 return super(hgloader, self).set_data(path, data, *args, **kwargs)
263 return super(hgloader, self).set_data(path, data, *args, **kwargs)
260
264
261 def source_to_code(self, data, path):
265 def source_to_code(self, data, path):
262 """Perform token transformation before compilation."""
266 """Perform token transformation before compilation."""
263 buf = io.BytesIO(data)
267 buf = io.BytesIO(data)
264 tokens = tokenize.tokenize(buf.readline)
268 tokens = tokenize.tokenize(buf.readline)
265 data = tokenize.untokenize(replacetokens(list(tokens), self.name))
269 data = tokenize.untokenize(replacetokens(list(tokens), self.name))
266 # Python's built-in importer strips frames from exceptions raised
270 # Python's built-in importer strips frames from exceptions raised
267 # for this code. Unfortunately, that mechanism isn't extensible
271 # for this code. Unfortunately, that mechanism isn't extensible
268 # and our frame will be blamed for the import failure. There
272 # and our frame will be blamed for the import failure. There
269 # are extremely hacky ways to do frame stripping. We haven't
273 # are extremely hacky ways to do frame stripping. We haven't
270 # implemented them because they are very ugly.
274 # implemented them because they are very ugly.
271 return super(hgloader, self).source_to_code(data, path)
275 return super(hgloader, self).source_to_code(data, path)
272
276
273 # We automagically register our custom importer as a side-effect of
277 # We automagically register our custom importer as a side-effect of
274 # loading. This is necessary to ensure that any entry points are able
278 # loading. This is necessary to ensure that any entry points are able
275 # to import mercurial.* modules without having to perform this
279 # to import mercurial.* modules without having to perform this
276 # registration themselves.
280 # registration themselves.
277 if not any(isinstance(x, hgpathentryfinder) for x in sys.meta_path):
281 if not any(isinstance(x, hgpathentryfinder) for x in sys.meta_path):
278 # meta_path is used before any implicit finders and before sys.path.
282 # meta_path is used before any implicit finders and before sys.path.
279 sys.meta_path.insert(0, hgpathentryfinder())
283 sys.meta_path.insert(0, hgpathentryfinder())
@@ -1,803 +1,804
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 sys.version_info < (2, 7, 0, 'final'):
8 if sys.version_info < (2, 7, 0, 'final'):
9 raise SystemExit('Mercurial requires Python 2.7 or later.')
9 raise SystemExit('Mercurial requires Python 2.7 or later.')
10
10
11 if sys.version_info[0] >= 3:
11 if sys.version_info[0] >= 3:
12 printf = eval('print')
12 printf = eval('print')
13 libdir_escape = 'unicode_escape'
13 libdir_escape = 'unicode_escape'
14 else:
14 else:
15 libdir_escape = 'string_escape'
15 libdir_escape = 'string_escape'
16 def printf(*args, **kwargs):
16 def printf(*args, **kwargs):
17 f = kwargs.get('file', sys.stdout)
17 f = kwargs.get('file', sys.stdout)
18 end = kwargs.get('end', '\n')
18 end = kwargs.get('end', '\n')
19 f.write(b' '.join(args) + end)
19 f.write(b' '.join(args) + end)
20
20
21 # Solaris Python packaging brain damage
21 # Solaris Python packaging brain damage
22 try:
22 try:
23 import hashlib
23 import hashlib
24 sha = hashlib.sha1()
24 sha = hashlib.sha1()
25 except ImportError:
25 except ImportError:
26 try:
26 try:
27 import sha
27 import sha
28 sha.sha # silence unused import warning
28 sha.sha # silence unused import warning
29 except ImportError:
29 except ImportError:
30 raise SystemExit(
30 raise SystemExit(
31 "Couldn't import standard hashlib (incomplete Python install).")
31 "Couldn't import standard hashlib (incomplete Python install).")
32
32
33 try:
33 try:
34 import zlib
34 import zlib
35 zlib.compressobj # silence unused import warning
35 zlib.compressobj # silence unused import warning
36 except ImportError:
36 except ImportError:
37 raise SystemExit(
37 raise SystemExit(
38 "Couldn't import standard zlib (incomplete Python install).")
38 "Couldn't import standard zlib (incomplete Python install).")
39
39
40 # The base IronPython distribution (as of 2.7.1) doesn't support bz2
40 # The base IronPython distribution (as of 2.7.1) doesn't support bz2
41 isironpython = False
41 isironpython = False
42 try:
42 try:
43 isironpython = (platform.python_implementation()
43 isironpython = (platform.python_implementation()
44 .lower().find("ironpython") != -1)
44 .lower().find("ironpython") != -1)
45 except AttributeError:
45 except AttributeError:
46 pass
46 pass
47
47
48 if isironpython:
48 if isironpython:
49 sys.stderr.write("warning: IronPython detected (no bz2 support)\n")
49 sys.stderr.write("warning: IronPython detected (no bz2 support)\n")
50 else:
50 else:
51 try:
51 try:
52 import bz2
52 import bz2
53 bz2.BZ2Compressor # silence unused import warning
53 bz2.BZ2Compressor # silence unused import warning
54 except ImportError:
54 except ImportError:
55 raise SystemExit(
55 raise SystemExit(
56 "Couldn't import standard bz2 (incomplete Python install).")
56 "Couldn't import standard bz2 (incomplete Python install).")
57
57
58 ispypy = "PyPy" in sys.version
58 ispypy = "PyPy" in sys.version
59
59
60 import ctypes
60 import ctypes
61 import os, stat, subprocess, time
61 import os, stat, subprocess, time
62 import re
62 import re
63 import shutil
63 import shutil
64 import tempfile
64 import tempfile
65 from distutils import log
65 from distutils import log
66 # We have issues with setuptools on some platforms and builders. Until
66 # We have issues with setuptools on some platforms and builders. Until
67 # those are resolved, setuptools is opt-in except for platforms where
67 # those are resolved, setuptools is opt-in except for platforms where
68 # we don't have issues.
68 # we don't have issues.
69 if os.name == 'nt' or 'FORCE_SETUPTOOLS' in os.environ:
69 if os.name == 'nt' or 'FORCE_SETUPTOOLS' in os.environ:
70 from setuptools import setup
70 from setuptools import setup
71 else:
71 else:
72 from distutils.core import setup
72 from distutils.core import setup
73 from distutils.ccompiler import new_compiler
73 from distutils.ccompiler import new_compiler
74 from distutils.core import Command, Extension
74 from distutils.core import Command, Extension
75 from distutils.dist import Distribution
75 from distutils.dist import Distribution
76 from distutils.command.build import build
76 from distutils.command.build import build
77 from distutils.command.build_ext import build_ext
77 from distutils.command.build_ext import build_ext
78 from distutils.command.build_py import build_py
78 from distutils.command.build_py import build_py
79 from distutils.command.build_scripts import build_scripts
79 from distutils.command.build_scripts import build_scripts
80 from distutils.command.install_lib import install_lib
80 from distutils.command.install_lib import install_lib
81 from distutils.command.install_scripts import install_scripts
81 from distutils.command.install_scripts import install_scripts
82 from distutils.spawn import spawn, find_executable
82 from distutils.spawn import spawn, find_executable
83 from distutils import file_util
83 from distutils import file_util
84 from distutils.errors import (
84 from distutils.errors import (
85 CCompilerError,
85 CCompilerError,
86 DistutilsError,
86 DistutilsError,
87 DistutilsExecError,
87 DistutilsExecError,
88 )
88 )
89 from distutils.sysconfig import get_python_inc, get_config_var
89 from distutils.sysconfig import get_python_inc, get_config_var
90 from distutils.version import StrictVersion
90 from distutils.version import StrictVersion
91
91
92 scripts = ['hg']
92 scripts = ['hg']
93 if os.name == 'nt':
93 if os.name == 'nt':
94 # We remove hg.bat if we are able to build hg.exe.
94 # We remove hg.bat if we are able to build hg.exe.
95 scripts.append('contrib/win32/hg.bat')
95 scripts.append('contrib/win32/hg.bat')
96
96
97 def cancompile(cc, code):
97 def cancompile(cc, code):
98 tmpdir = tempfile.mkdtemp(prefix='hg-install-')
98 tmpdir = tempfile.mkdtemp(prefix='hg-install-')
99 devnull = oldstderr = None
99 devnull = oldstderr = None
100 try:
100 try:
101 fname = os.path.join(tmpdir, 'testcomp.c')
101 fname = os.path.join(tmpdir, 'testcomp.c')
102 f = open(fname, 'w')
102 f = open(fname, 'w')
103 f.write(code)
103 f.write(code)
104 f.close()
104 f.close()
105 # Redirect stderr to /dev/null to hide any error messages
105 # Redirect stderr to /dev/null to hide any error messages
106 # from the compiler.
106 # from the compiler.
107 # This will have to be changed if we ever have to check
107 # This will have to be changed if we ever have to check
108 # for a function on Windows.
108 # for a function on Windows.
109 devnull = open('/dev/null', 'w')
109 devnull = open('/dev/null', 'w')
110 oldstderr = os.dup(sys.stderr.fileno())
110 oldstderr = os.dup(sys.stderr.fileno())
111 os.dup2(devnull.fileno(), sys.stderr.fileno())
111 os.dup2(devnull.fileno(), sys.stderr.fileno())
112 objects = cc.compile([fname], output_dir=tmpdir)
112 objects = cc.compile([fname], output_dir=tmpdir)
113 cc.link_executable(objects, os.path.join(tmpdir, "a.out"))
113 cc.link_executable(objects, os.path.join(tmpdir, "a.out"))
114 return True
114 return True
115 except Exception:
115 except Exception:
116 return False
116 return False
117 finally:
117 finally:
118 if oldstderr is not None:
118 if oldstderr is not None:
119 os.dup2(oldstderr, sys.stderr.fileno())
119 os.dup2(oldstderr, sys.stderr.fileno())
120 if devnull is not None:
120 if devnull is not None:
121 devnull.close()
121 devnull.close()
122 shutil.rmtree(tmpdir)
122 shutil.rmtree(tmpdir)
123
123
124 # simplified version of distutils.ccompiler.CCompiler.has_function
124 # simplified version of distutils.ccompiler.CCompiler.has_function
125 # that actually removes its temporary files.
125 # that actually removes its temporary files.
126 def hasfunction(cc, funcname):
126 def hasfunction(cc, funcname):
127 code = 'int main(void) { %s(); }\n' % funcname
127 code = 'int main(void) { %s(); }\n' % funcname
128 return cancompile(cc, code)
128 return cancompile(cc, code)
129
129
130 def hasheader(cc, headername):
130 def hasheader(cc, headername):
131 code = '#include <%s>\nint main(void) { return 0; }\n' % headername
131 code = '#include <%s>\nint main(void) { return 0; }\n' % headername
132 return cancompile(cc, code)
132 return cancompile(cc, code)
133
133
134 # py2exe needs to be installed to work
134 # py2exe needs to be installed to work
135 try:
135 try:
136 import py2exe
136 import py2exe
137 py2exe.Distribution # silence unused import warning
137 py2exe.Distribution # silence unused import warning
138 py2exeloaded = True
138 py2exeloaded = True
139 # import py2exe's patched Distribution class
139 # import py2exe's patched Distribution class
140 from distutils.core import Distribution
140 from distutils.core import Distribution
141 except ImportError:
141 except ImportError:
142 py2exeloaded = False
142 py2exeloaded = False
143
143
144 def runcmd(cmd, env):
144 def runcmd(cmd, env):
145 if (sys.platform == 'plan9'
145 if (sys.platform == 'plan9'
146 and (sys.version_info[0] == 2 and sys.version_info[1] < 7)):
146 and (sys.version_info[0] == 2 and sys.version_info[1] < 7)):
147 # subprocess kludge to work around issues in half-baked Python
147 # subprocess kludge to work around issues in half-baked Python
148 # ports, notably bichued/python:
148 # ports, notably bichued/python:
149 _, out, err = os.popen3(cmd)
149 _, out, err = os.popen3(cmd)
150 return str(out), str(err)
150 return str(out), str(err)
151 else:
151 else:
152 p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
152 p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
153 stderr=subprocess.PIPE, env=env)
153 stderr=subprocess.PIPE, env=env)
154 out, err = p.communicate()
154 out, err = p.communicate()
155 return out, err
155 return out, err
156
156
157 def runhg(cmd, env):
157 def runhg(cmd, env):
158 out, err = runcmd(cmd, env)
158 out, err = runcmd(cmd, env)
159 # If root is executing setup.py, but the repository is owned by
159 # If root is executing setup.py, but the repository is owned by
160 # another user (as in "sudo python setup.py install") we will get
160 # another user (as in "sudo python setup.py install") we will get
161 # trust warnings since the .hg/hgrc file is untrusted. That is
161 # trust warnings since the .hg/hgrc file is untrusted. That is
162 # fine, we don't want to load it anyway. Python may warn about
162 # fine, we don't want to load it anyway. Python may warn about
163 # a missing __init__.py in mercurial/locale, we also ignore that.
163 # a missing __init__.py in mercurial/locale, we also ignore that.
164 err = [e for e in err.splitlines()
164 err = [e for e in err.splitlines()
165 if not e.startswith(b'not trusting file') \
165 if not e.startswith(b'not trusting file') \
166 and not e.startswith(b'warning: Not importing') \
166 and not e.startswith(b'warning: Not importing') \
167 and not e.startswith(b'obsolete feature not enabled')]
167 and not e.startswith(b'obsolete feature not enabled')]
168 if err:
168 if err:
169 printf("stderr from '%s':" % (' '.join(cmd)), file=sys.stderr)
169 printf("stderr from '%s':" % (' '.join(cmd)), file=sys.stderr)
170 printf(b'\n'.join([b' ' + e for e in err]), file=sys.stderr)
170 printf(b'\n'.join([b' ' + e for e in err]), file=sys.stderr)
171 return ''
171 return ''
172 return out
172 return out
173
173
174 version = ''
174 version = ''
175
175
176 # Execute hg out of this directory with a custom environment which takes care
176 # Execute hg out of this directory with a custom environment which takes care
177 # to not use any hgrc files and do no localization.
177 # to not use any hgrc files and do no localization.
178 env = {'HGMODULEPOLICY': 'py',
178 env = {'HGMODULEPOLICY': 'py',
179 'HGRCPATH': '',
179 'HGRCPATH': '',
180 'LANGUAGE': 'C',
180 'LANGUAGE': 'C',
181 'PATH': ''} # make pypi modules that use os.environ['PATH'] happy
181 'PATH': ''} # make pypi modules that use os.environ['PATH'] happy
182 if 'LD_LIBRARY_PATH' in os.environ:
182 if 'LD_LIBRARY_PATH' in os.environ:
183 env['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH']
183 env['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH']
184 if 'SystemRoot' in os.environ:
184 if 'SystemRoot' in os.environ:
185 # Copy SystemRoot into the custom environment for Python 2.6
185 # Copy SystemRoot into the custom environment for Python 2.6
186 # under Windows. Otherwise, the subprocess will fail with
186 # under Windows. Otherwise, the subprocess will fail with
187 # error 0xc0150004. See: http://bugs.python.org/issue3440
187 # error 0xc0150004. See: http://bugs.python.org/issue3440
188 env['SystemRoot'] = os.environ['SystemRoot']
188 env['SystemRoot'] = os.environ['SystemRoot']
189
189
190 if os.path.isdir('.hg'):
190 if os.path.isdir('.hg'):
191 cmd = [sys.executable, 'hg', 'log', '-r', '.', '--template', '{tags}\n']
191 cmd = [sys.executable, 'hg', 'log', '-r', '.', '--template', '{tags}\n']
192 numerictags = [t for t in runhg(cmd, env).split() if t[0].isdigit()]
192 numerictags = [t for t in runhg(cmd, env).split() if t[0].isdigit()]
193 hgid = runhg([sys.executable, 'hg', 'id', '-i'], env).strip()
193 hgid = runhg([sys.executable, 'hg', 'id', '-i'], env).strip()
194 if numerictags: # tag(s) found
194 if numerictags: # tag(s) found
195 version = numerictags[-1]
195 version = numerictags[-1]
196 if hgid.endswith('+'): # propagate the dirty status to the tag
196 if hgid.endswith('+'): # propagate the dirty status to the tag
197 version += '+'
197 version += '+'
198 else: # no tag found
198 else: # no tag found
199 ltagcmd = [sys.executable, 'hg', 'parents', '--template',
199 ltagcmd = [sys.executable, 'hg', 'parents', '--template',
200 '{latesttag}']
200 '{latesttag}']
201 ltag = runhg(ltagcmd, env)
201 ltag = runhg(ltagcmd, env)
202 changessincecmd = [sys.executable, 'hg', 'log', '-T', 'x\n', '-r',
202 changessincecmd = [sys.executable, 'hg', 'log', '-T', 'x\n', '-r',
203 "only(.,'%s')" % ltag]
203 "only(.,'%s')" % ltag]
204 changessince = len(runhg(changessincecmd, env).splitlines())
204 changessince = len(runhg(changessincecmd, env).splitlines())
205 version = '%s+%s-%s' % (ltag, changessince, hgid)
205 version = '%s+%s-%s' % (ltag, changessince, hgid)
206 if version.endswith('+'):
206 if version.endswith('+'):
207 version += time.strftime('%Y%m%d')
207 version += time.strftime('%Y%m%d')
208 elif os.path.exists('.hg_archival.txt'):
208 elif os.path.exists('.hg_archival.txt'):
209 kw = dict([[t.strip() for t in l.split(':', 1)]
209 kw = dict([[t.strip() for t in l.split(':', 1)]
210 for l in open('.hg_archival.txt')])
210 for l in open('.hg_archival.txt')])
211 if 'tag' in kw:
211 if 'tag' in kw:
212 version = kw['tag']
212 version = kw['tag']
213 elif 'latesttag' in kw:
213 elif 'latesttag' in kw:
214 if 'changessincelatesttag' in kw:
214 if 'changessincelatesttag' in kw:
215 version = '%(latesttag)s+%(changessincelatesttag)s-%(node).12s' % kw
215 version = '%(latesttag)s+%(changessincelatesttag)s-%(node).12s' % kw
216 else:
216 else:
217 version = '%(latesttag)s+%(latesttagdistance)s-%(node).12s' % kw
217 version = '%(latesttag)s+%(latesttagdistance)s-%(node).12s' % kw
218 else:
218 else:
219 version = kw.get('node', '')[:12]
219 version = kw.get('node', '')[:12]
220
220
221 if version:
221 if version:
222 with open("mercurial/__version__.py", "w") as f:
222 with open("mercurial/__version__.py", "w") as f:
223 f.write('# this file is autogenerated by setup.py\n')
223 f.write('# this file is autogenerated by setup.py\n')
224 f.write('version = "%s"\n' % version)
224 f.write('version = "%s"\n' % version)
225
225
226 try:
226 try:
227 oldpolicy = os.environ.get('HGMODULEPOLICY', None)
227 oldpolicy = os.environ.get('HGMODULEPOLICY', None)
228 os.environ['HGMODULEPOLICY'] = 'py'
228 os.environ['HGMODULEPOLICY'] = 'py'
229 from mercurial import __version__
229 from mercurial import __version__
230 version = __version__.version
230 version = __version__.version
231 except ImportError:
231 except ImportError:
232 version = 'unknown'
232 version = 'unknown'
233 finally:
233 finally:
234 if oldpolicy is None:
234 if oldpolicy is None:
235 del os.environ['HGMODULEPOLICY']
235 del os.environ['HGMODULEPOLICY']
236 else:
236 else:
237 os.environ['HGMODULEPOLICY'] = oldpolicy
237 os.environ['HGMODULEPOLICY'] = oldpolicy
238
238
239 class hgbuild(build):
239 class hgbuild(build):
240 # Insert hgbuildmo first so that files in mercurial/locale/ are found
240 # Insert hgbuildmo first so that files in mercurial/locale/ are found
241 # when build_py is run next.
241 # when build_py is run next.
242 sub_commands = [('build_mo', None)] + build.sub_commands
242 sub_commands = [('build_mo', None)] + build.sub_commands
243
243
244 class hgbuildmo(build):
244 class hgbuildmo(build):
245
245
246 description = "build translations (.mo files)"
246 description = "build translations (.mo files)"
247
247
248 def run(self):
248 def run(self):
249 if not find_executable('msgfmt'):
249 if not find_executable('msgfmt'):
250 self.warn("could not find msgfmt executable, no translations "
250 self.warn("could not find msgfmt executable, no translations "
251 "will be built")
251 "will be built")
252 return
252 return
253
253
254 podir = 'i18n'
254 podir = 'i18n'
255 if not os.path.isdir(podir):
255 if not os.path.isdir(podir):
256 self.warn("could not find %s/ directory" % podir)
256 self.warn("could not find %s/ directory" % podir)
257 return
257 return
258
258
259 join = os.path.join
259 join = os.path.join
260 for po in os.listdir(podir):
260 for po in os.listdir(podir):
261 if not po.endswith('.po'):
261 if not po.endswith('.po'):
262 continue
262 continue
263 pofile = join(podir, po)
263 pofile = join(podir, po)
264 modir = join('locale', po[:-3], 'LC_MESSAGES')
264 modir = join('locale', po[:-3], 'LC_MESSAGES')
265 mofile = join(modir, 'hg.mo')
265 mofile = join(modir, 'hg.mo')
266 mobuildfile = join('mercurial', mofile)
266 mobuildfile = join('mercurial', mofile)
267 cmd = ['msgfmt', '-v', '-o', mobuildfile, pofile]
267 cmd = ['msgfmt', '-v', '-o', mobuildfile, pofile]
268 if sys.platform != 'sunos5':
268 if sys.platform != 'sunos5':
269 # msgfmt on Solaris does not know about -c
269 # msgfmt on Solaris does not know about -c
270 cmd.append('-c')
270 cmd.append('-c')
271 self.mkpath(join('mercurial', modir))
271 self.mkpath(join('mercurial', modir))
272 self.make_file([pofile], mobuildfile, spawn, (cmd,))
272 self.make_file([pofile], mobuildfile, spawn, (cmd,))
273
273
274
274
275 class hgdist(Distribution):
275 class hgdist(Distribution):
276 pure = False
276 pure = False
277 cffi = ispypy
277 cffi = ispypy
278
278
279 global_options = Distribution.global_options + \
279 global_options = Distribution.global_options + \
280 [('pure', None, "use pure (slow) Python "
280 [('pure', None, "use pure (slow) Python "
281 "code instead of C extensions"),
281 "code instead of C extensions"),
282 ]
282 ]
283
283
284 def has_ext_modules(self):
284 def has_ext_modules(self):
285 # self.ext_modules is emptied in hgbuildpy.finalize_options which is
285 # self.ext_modules is emptied in hgbuildpy.finalize_options which is
286 # too late for some cases
286 # too late for some cases
287 return not self.pure and Distribution.has_ext_modules(self)
287 return not self.pure and Distribution.has_ext_modules(self)
288
288
289 # This is ugly as a one-liner. So use a variable.
289 # This is ugly as a one-liner. So use a variable.
290 buildextnegops = dict(getattr(build_ext, 'negative_options', {}))
290 buildextnegops = dict(getattr(build_ext, 'negative_options', {}))
291 buildextnegops['no-zstd'] = 'zstd'
291 buildextnegops['no-zstd'] = 'zstd'
292
292
293 class hgbuildext(build_ext):
293 class hgbuildext(build_ext):
294 user_options = build_ext.user_options + [
294 user_options = build_ext.user_options + [
295 ('zstd', None, 'compile zstd bindings [default]'),
295 ('zstd', None, 'compile zstd bindings [default]'),
296 ('no-zstd', None, 'do not compile zstd bindings'),
296 ('no-zstd', None, 'do not compile zstd bindings'),
297 ]
297 ]
298
298
299 boolean_options = build_ext.boolean_options + ['zstd']
299 boolean_options = build_ext.boolean_options + ['zstd']
300 negative_opt = buildextnegops
300 negative_opt = buildextnegops
301
301
302 def initialize_options(self):
302 def initialize_options(self):
303 self.zstd = True
303 self.zstd = True
304 return build_ext.initialize_options(self)
304 return build_ext.initialize_options(self)
305
305
306 def build_extensions(self):
306 def build_extensions(self):
307 # Filter out zstd if disabled via argument.
307 # Filter out zstd if disabled via argument.
308 if not self.zstd:
308 if not self.zstd:
309 self.extensions = [e for e in self.extensions
309 self.extensions = [e for e in self.extensions
310 if e.name != 'mercurial.zstd']
310 if e.name != 'mercurial.zstd']
311
311
312 return build_ext.build_extensions(self)
312 return build_ext.build_extensions(self)
313
313
314 def build_extension(self, ext):
314 def build_extension(self, ext):
315 try:
315 try:
316 build_ext.build_extension(self, ext)
316 build_ext.build_extension(self, ext)
317 except CCompilerError:
317 except CCompilerError:
318 if not getattr(ext, 'optional', False):
318 if not getattr(ext, 'optional', False):
319 raise
319 raise
320 log.warn("Failed to build optional extension '%s' (skipping)",
320 log.warn("Failed to build optional extension '%s' (skipping)",
321 ext.name)
321 ext.name)
322
322
323 class hgbuildscripts(build_scripts):
323 class hgbuildscripts(build_scripts):
324 def run(self):
324 def run(self):
325 if os.name != 'nt' or self.distribution.pure:
325 if os.name != 'nt' or self.distribution.pure:
326 return build_scripts.run(self)
326 return build_scripts.run(self)
327
327
328 exebuilt = False
328 exebuilt = False
329 try:
329 try:
330 self.run_command('build_hgexe')
330 self.run_command('build_hgexe')
331 exebuilt = True
331 exebuilt = True
332 except (DistutilsError, CCompilerError):
332 except (DistutilsError, CCompilerError):
333 log.warn('failed to build optional hg.exe')
333 log.warn('failed to build optional hg.exe')
334
334
335 if exebuilt:
335 if exebuilt:
336 # Copying hg.exe to the scripts build directory ensures it is
336 # Copying hg.exe to the scripts build directory ensures it is
337 # installed by the install_scripts command.
337 # installed by the install_scripts command.
338 hgexecommand = self.get_finalized_command('build_hgexe')
338 hgexecommand = self.get_finalized_command('build_hgexe')
339 dest = os.path.join(self.build_dir, 'hg.exe')
339 dest = os.path.join(self.build_dir, 'hg.exe')
340 self.mkpath(self.build_dir)
340 self.mkpath(self.build_dir)
341 self.copy_file(hgexecommand.hgexepath, dest)
341 self.copy_file(hgexecommand.hgexepath, dest)
342
342
343 # Remove hg.bat because it is redundant with hg.exe.
343 # Remove hg.bat because it is redundant with hg.exe.
344 self.scripts.remove('contrib/win32/hg.bat')
344 self.scripts.remove('contrib/win32/hg.bat')
345
345
346 return build_scripts.run(self)
346 return build_scripts.run(self)
347
347
348 class hgbuildpy(build_py):
348 class hgbuildpy(build_py):
349 def finalize_options(self):
349 def finalize_options(self):
350 build_py.finalize_options(self)
350 build_py.finalize_options(self)
351
351
352 if self.distribution.pure:
352 if self.distribution.pure:
353 self.distribution.ext_modules = []
353 self.distribution.ext_modules = []
354 elif self.distribution.cffi:
354 elif self.distribution.cffi:
355 from mercurial.cffi import (
355 from mercurial.cffi import (
356 bdiff,
356 bdiff,
357 mpatch,
357 mpatch,
358 )
358 )
359 exts = [mpatch.ffi.distutils_extension(),
359 exts = [mpatch.ffi.distutils_extension(),
360 bdiff.ffi.distutils_extension()]
360 bdiff.ffi.distutils_extension()]
361 # cffi modules go here
361 # cffi modules go here
362 if sys.platform == 'darwin':
362 if sys.platform == 'darwin':
363 from mercurial.cffi import osutil
363 from mercurial.cffi import osutil
364 exts.append(osutil.ffi.distutils_extension())
364 exts.append(osutil.ffi.distutils_extension())
365 self.distribution.ext_modules = exts
365 self.distribution.ext_modules = exts
366 else:
366 else:
367 h = os.path.join(get_python_inc(), 'Python.h')
367 h = os.path.join(get_python_inc(), 'Python.h')
368 if not os.path.exists(h):
368 if not os.path.exists(h):
369 raise SystemExit('Python headers are required to build '
369 raise SystemExit('Python headers are required to build '
370 'Mercurial but weren\'t found in %s' % h)
370 'Mercurial but weren\'t found in %s' % h)
371
371
372 def run(self):
372 def run(self):
373 if self.distribution.pure:
373 if self.distribution.pure:
374 modulepolicy = 'py'
374 modulepolicy = 'py'
375 elif self.build_lib == '.':
375 elif self.build_lib == '.':
376 # in-place build should run without rebuilding C extensions
376 # in-place build should run without rebuilding C extensions
377 modulepolicy = 'allow'
377 modulepolicy = 'allow'
378 else:
378 else:
379 modulepolicy = 'c'
379 modulepolicy = 'c'
380 with open("mercurial/__modulepolicy__.py", "w") as f:
380 with open("mercurial/__modulepolicy__.py", "w") as f:
381 f.write('# this file is autogenerated by setup.py\n')
381 f.write('# this file is autogenerated by setup.py\n')
382 f.write('modulepolicy = b"%s"\n' % modulepolicy)
382 f.write('modulepolicy = b"%s"\n' % modulepolicy)
383
383
384 build_py.run(self)
384 build_py.run(self)
385
385
386 class buildhgextindex(Command):
386 class buildhgextindex(Command):
387 description = 'generate prebuilt index of hgext (for frozen package)'
387 description = 'generate prebuilt index of hgext (for frozen package)'
388 user_options = []
388 user_options = []
389 _indexfilename = 'hgext/__index__.py'
389 _indexfilename = 'hgext/__index__.py'
390
390
391 def initialize_options(self):
391 def initialize_options(self):
392 pass
392 pass
393
393
394 def finalize_options(self):
394 def finalize_options(self):
395 pass
395 pass
396
396
397 def run(self):
397 def run(self):
398 if os.path.exists(self._indexfilename):
398 if os.path.exists(self._indexfilename):
399 with open(self._indexfilename, 'w') as f:
399 with open(self._indexfilename, 'w') as f:
400 f.write('# empty\n')
400 f.write('# empty\n')
401
401
402 # here no extension enabled, disabled() lists up everything
402 # here no extension enabled, disabled() lists up everything
403 code = ('import pprint; from mercurial import extensions; '
403 code = ('import pprint; from mercurial import extensions; '
404 'pprint.pprint(extensions.disabled())')
404 'pprint.pprint(extensions.disabled())')
405 out, err = runcmd([sys.executable, '-c', code], env)
405 out, err = runcmd([sys.executable, '-c', code], env)
406 if err:
406 if err:
407 raise DistutilsExecError(err)
407 raise DistutilsExecError(err)
408
408
409 with open(self._indexfilename, 'w') as f:
409 with open(self._indexfilename, 'w') as f:
410 f.write('# this file is autogenerated by setup.py\n')
410 f.write('# this file is autogenerated by setup.py\n')
411 f.write('docs = ')
411 f.write('docs = ')
412 f.write(out)
412 f.write(out)
413
413
414 class buildhgexe(build_ext):
414 class buildhgexe(build_ext):
415 description = 'compile hg.exe from mercurial/exewrapper.c'
415 description = 'compile hg.exe from mercurial/exewrapper.c'
416
416
417 def build_extensions(self):
417 def build_extensions(self):
418 if os.name != 'nt':
418 if os.name != 'nt':
419 return
419 return
420 if isinstance(self.compiler, HackedMingw32CCompiler):
420 if isinstance(self.compiler, HackedMingw32CCompiler):
421 self.compiler.compiler_so = self.compiler.compiler # no -mdll
421 self.compiler.compiler_so = self.compiler.compiler # no -mdll
422 self.compiler.dll_libraries = [] # no -lmsrvc90
422 self.compiler.dll_libraries = [] # no -lmsrvc90
423
423
424 # Different Python installs can have different Python library
424 # Different Python installs can have different Python library
425 # names. e.g. the official CPython distribution uses pythonXY.dll
425 # names. e.g. the official CPython distribution uses pythonXY.dll
426 # and MinGW uses libpythonX.Y.dll.
426 # and MinGW uses libpythonX.Y.dll.
427 _kernel32 = ctypes.windll.kernel32
427 _kernel32 = ctypes.windll.kernel32
428 _kernel32.GetModuleFileNameA.argtypes = [ctypes.c_void_p,
428 _kernel32.GetModuleFileNameA.argtypes = [ctypes.c_void_p,
429 ctypes.c_void_p,
429 ctypes.c_void_p,
430 ctypes.c_ulong]
430 ctypes.c_ulong]
431 _kernel32.GetModuleFileNameA.restype = ctypes.c_ulong
431 _kernel32.GetModuleFileNameA.restype = ctypes.c_ulong
432 size = 1000
432 size = 1000
433 buf = ctypes.create_string_buffer(size + 1)
433 buf = ctypes.create_string_buffer(size + 1)
434 filelen = _kernel32.GetModuleFileNameA(sys.dllhandle, ctypes.byref(buf),
434 filelen = _kernel32.GetModuleFileNameA(sys.dllhandle, ctypes.byref(buf),
435 size)
435 size)
436
436
437 if filelen > 0 and filelen != size:
437 if filelen > 0 and filelen != size:
438 dllbasename = os.path.basename(buf.value)
438 dllbasename = os.path.basename(buf.value)
439 if not dllbasename.lower().endswith('.dll'):
439 if not dllbasename.lower().endswith('.dll'):
440 raise SystemExit('Python DLL does not end with .dll: %s' %
440 raise SystemExit('Python DLL does not end with .dll: %s' %
441 dllbasename)
441 dllbasename)
442 pythonlib = dllbasename[:-4]
442 pythonlib = dllbasename[:-4]
443 else:
443 else:
444 log.warn('could not determine Python DLL filename; '
444 log.warn('could not determine Python DLL filename; '
445 'assuming pythonXY')
445 'assuming pythonXY')
446
446
447 hv = sys.hexversion
447 hv = sys.hexversion
448 pythonlib = 'python%d%d' % (hv >> 24, (hv >> 16) & 0xff)
448 pythonlib = 'python%d%d' % (hv >> 24, (hv >> 16) & 0xff)
449
449
450 log.info('using %s as Python library name' % pythonlib)
450 log.info('using %s as Python library name' % pythonlib)
451 with open('mercurial/hgpythonlib.h', 'wb') as f:
451 with open('mercurial/hgpythonlib.h', 'wb') as f:
452 f.write('/* this file is autogenerated by setup.py */\n')
452 f.write('/* this file is autogenerated by setup.py */\n')
453 f.write('#define HGPYTHONLIB "%s"\n' % pythonlib)
453 f.write('#define HGPYTHONLIB "%s"\n' % pythonlib)
454 objects = self.compiler.compile(['mercurial/exewrapper.c'],
454 objects = self.compiler.compile(['mercurial/exewrapper.c'],
455 output_dir=self.build_temp)
455 output_dir=self.build_temp)
456 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
456 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
457 target = os.path.join(dir, 'hg')
457 target = os.path.join(dir, 'hg')
458 self.compiler.link_executable(objects, target,
458 self.compiler.link_executable(objects, target,
459 libraries=[],
459 libraries=[],
460 output_dir=self.build_temp)
460 output_dir=self.build_temp)
461
461
462 @property
462 @property
463 def hgexepath(self):
463 def hgexepath(self):
464 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
464 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
465 return os.path.join(self.build_temp, dir, 'hg.exe')
465 return os.path.join(self.build_temp, dir, 'hg.exe')
466
466
467 class hginstalllib(install_lib):
467 class hginstalllib(install_lib):
468 '''
468 '''
469 This is a specialization of install_lib that replaces the copy_file used
469 This is a specialization of install_lib that replaces the copy_file used
470 there so that it supports setting the mode of files after copying them,
470 there so that it supports setting the mode of files after copying them,
471 instead of just preserving the mode that the files originally had. If your
471 instead of just preserving the mode that the files originally had. If your
472 system has a umask of something like 027, preserving the permissions when
472 system has a umask of something like 027, preserving the permissions when
473 copying will lead to a broken install.
473 copying will lead to a broken install.
474
474
475 Note that just passing keep_permissions=False to copy_file would be
475 Note that just passing keep_permissions=False to copy_file would be
476 insufficient, as it might still be applying a umask.
476 insufficient, as it might still be applying a umask.
477 '''
477 '''
478
478
479 def run(self):
479 def run(self):
480 realcopyfile = file_util.copy_file
480 realcopyfile = file_util.copy_file
481 def copyfileandsetmode(*args, **kwargs):
481 def copyfileandsetmode(*args, **kwargs):
482 src, dst = args[0], args[1]
482 src, dst = args[0], args[1]
483 dst, copied = realcopyfile(*args, **kwargs)
483 dst, copied = realcopyfile(*args, **kwargs)
484 if copied:
484 if copied:
485 st = os.stat(src)
485 st = os.stat(src)
486 # Persist executable bit (apply it to group and other if user
486 # Persist executable bit (apply it to group and other if user
487 # has it)
487 # has it)
488 if st[stat.ST_MODE] & stat.S_IXUSR:
488 if st[stat.ST_MODE] & stat.S_IXUSR:
489 setmode = int('0755', 8)
489 setmode = int('0755', 8)
490 else:
490 else:
491 setmode = int('0644', 8)
491 setmode = int('0644', 8)
492 m = stat.S_IMODE(st[stat.ST_MODE])
492 m = stat.S_IMODE(st[stat.ST_MODE])
493 m = (m & ~int('0777', 8)) | setmode
493 m = (m & ~int('0777', 8)) | setmode
494 os.chmod(dst, m)
494 os.chmod(dst, m)
495 file_util.copy_file = copyfileandsetmode
495 file_util.copy_file = copyfileandsetmode
496 try:
496 try:
497 install_lib.run(self)
497 install_lib.run(self)
498 finally:
498 finally:
499 file_util.copy_file = realcopyfile
499 file_util.copy_file = realcopyfile
500
500
501 class hginstallscripts(install_scripts):
501 class hginstallscripts(install_scripts):
502 '''
502 '''
503 This is a specialization of install_scripts that replaces the @LIBDIR@ with
503 This is a specialization of install_scripts that replaces the @LIBDIR@ with
504 the configured directory for modules. If possible, the path is made relative
504 the configured directory for modules. If possible, the path is made relative
505 to the directory for scripts.
505 to the directory for scripts.
506 '''
506 '''
507
507
508 def initialize_options(self):
508 def initialize_options(self):
509 install_scripts.initialize_options(self)
509 install_scripts.initialize_options(self)
510
510
511 self.install_lib = None
511 self.install_lib = None
512
512
513 def finalize_options(self):
513 def finalize_options(self):
514 install_scripts.finalize_options(self)
514 install_scripts.finalize_options(self)
515 self.set_undefined_options('install',
515 self.set_undefined_options('install',
516 ('install_lib', 'install_lib'))
516 ('install_lib', 'install_lib'))
517
517
518 def run(self):
518 def run(self):
519 install_scripts.run(self)
519 install_scripts.run(self)
520
520
521 # It only makes sense to replace @LIBDIR@ with the install path if
521 # It only makes sense to replace @LIBDIR@ with the install path if
522 # the install path is known. For wheels, the logic below calculates
522 # the install path is known. For wheels, the logic below calculates
523 # the libdir to be "../..". This is because the internal layout of a
523 # the libdir to be "../..". This is because the internal layout of a
524 # wheel archive looks like:
524 # wheel archive looks like:
525 #
525 #
526 # mercurial-3.6.1.data/scripts/hg
526 # mercurial-3.6.1.data/scripts/hg
527 # mercurial/__init__.py
527 # mercurial/__init__.py
528 #
528 #
529 # When installing wheels, the subdirectories of the "<pkg>.data"
529 # When installing wheels, the subdirectories of the "<pkg>.data"
530 # directory are translated to system local paths and files therein
530 # directory are translated to system local paths and files therein
531 # are copied in place. The mercurial/* files are installed into the
531 # are copied in place. The mercurial/* files are installed into the
532 # site-packages directory. However, the site-packages directory
532 # site-packages directory. However, the site-packages directory
533 # isn't known until wheel install time. This means we have no clue
533 # isn't known until wheel install time. This means we have no clue
534 # at wheel generation time what the installed site-packages directory
534 # at wheel generation time what the installed site-packages directory
535 # will be. And, wheels don't appear to provide the ability to register
535 # will be. And, wheels don't appear to provide the ability to register
536 # custom code to run during wheel installation. This all means that
536 # custom code to run during wheel installation. This all means that
537 # we can't reliably set the libdir in wheels: the default behavior
537 # we can't reliably set the libdir in wheels: the default behavior
538 # of looking in sys.path must do.
538 # of looking in sys.path must do.
539
539
540 if (os.path.splitdrive(self.install_dir)[0] !=
540 if (os.path.splitdrive(self.install_dir)[0] !=
541 os.path.splitdrive(self.install_lib)[0]):
541 os.path.splitdrive(self.install_lib)[0]):
542 # can't make relative paths from one drive to another, so use an
542 # can't make relative paths from one drive to another, so use an
543 # absolute path instead
543 # absolute path instead
544 libdir = self.install_lib
544 libdir = self.install_lib
545 else:
545 else:
546 common = os.path.commonprefix((self.install_dir, self.install_lib))
546 common = os.path.commonprefix((self.install_dir, self.install_lib))
547 rest = self.install_dir[len(common):]
547 rest = self.install_dir[len(common):]
548 uplevel = len([n for n in os.path.split(rest) if n])
548 uplevel = len([n for n in os.path.split(rest) if n])
549
549
550 libdir = uplevel * ('..' + os.sep) + self.install_lib[len(common):]
550 libdir = uplevel * ('..' + os.sep) + self.install_lib[len(common):]
551
551
552 for outfile in self.outfiles:
552 for outfile in self.outfiles:
553 with open(outfile, 'rb') as fp:
553 with open(outfile, 'rb') as fp:
554 data = fp.read()
554 data = fp.read()
555
555
556 # skip binary files
556 # skip binary files
557 if b'\0' in data:
557 if b'\0' in data:
558 continue
558 continue
559
559
560 # During local installs, the shebang will be rewritten to the final
560 # During local installs, the shebang will be rewritten to the final
561 # install path. During wheel packaging, the shebang has a special
561 # install path. During wheel packaging, the shebang has a special
562 # value.
562 # value.
563 if data.startswith(b'#!python'):
563 if data.startswith(b'#!python'):
564 log.info('not rewriting @LIBDIR@ in %s because install path '
564 log.info('not rewriting @LIBDIR@ in %s because install path '
565 'not known' % outfile)
565 'not known' % outfile)
566 continue
566 continue
567
567
568 data = data.replace(b'@LIBDIR@', libdir.encode(libdir_escape))
568 data = data.replace(b'@LIBDIR@', libdir.encode(libdir_escape))
569 with open(outfile, 'wb') as fp:
569 with open(outfile, 'wb') as fp:
570 fp.write(data)
570 fp.write(data)
571
571
572 cmdclass = {'build': hgbuild,
572 cmdclass = {'build': hgbuild,
573 'build_mo': hgbuildmo,
573 'build_mo': hgbuildmo,
574 'build_ext': hgbuildext,
574 'build_ext': hgbuildext,
575 'build_py': hgbuildpy,
575 'build_py': hgbuildpy,
576 'build_scripts': hgbuildscripts,
576 'build_scripts': hgbuildscripts,
577 'build_hgextindex': buildhgextindex,
577 'build_hgextindex': buildhgextindex,
578 'install_lib': hginstalllib,
578 'install_lib': hginstalllib,
579 'install_scripts': hginstallscripts,
579 'install_scripts': hginstallscripts,
580 'build_hgexe': buildhgexe,
580 'build_hgexe': buildhgexe,
581 }
581 }
582
582
583 packages = ['mercurial',
583 packages = ['mercurial',
584 'mercurial.cext',
584 'mercurial.cext',
585 'mercurial.hgweb',
585 'mercurial.hgweb',
586 'mercurial.httpclient',
586 'mercurial.httpclient',
587 'mercurial.pure',
587 'mercurial.pure',
588 'hgext', 'hgext.convert', 'hgext.fsmonitor',
588 'hgext', 'hgext.convert', 'hgext.fsmonitor',
589 'hgext.fsmonitor.pywatchman', 'hgext.highlight',
589 'hgext.fsmonitor.pywatchman', 'hgext.highlight',
590 'hgext.largefiles', 'hgext.zeroconf', 'hgext3rd']
590 'hgext.largefiles', 'hgext.zeroconf', 'hgext3rd',
591 'hgdemandimport']
591
592
592 common_depends = ['mercurial/bitmanipulation.h',
593 common_depends = ['mercurial/bitmanipulation.h',
593 'mercurial/compat.h',
594 'mercurial/compat.h',
594 'mercurial/cext/util.h']
595 'mercurial/cext/util.h']
595 common_include_dirs = ['mercurial']
596 common_include_dirs = ['mercurial']
596
597
597 osutil_cflags = []
598 osutil_cflags = []
598 osutil_ldflags = []
599 osutil_ldflags = []
599
600
600 # platform specific macros
601 # platform specific macros
601 for plat, func in [('bsd', 'setproctitle')]:
602 for plat, func in [('bsd', 'setproctitle')]:
602 if re.search(plat, sys.platform) and hasfunction(new_compiler(), func):
603 if re.search(plat, sys.platform) and hasfunction(new_compiler(), func):
603 osutil_cflags.append('-DHAVE_%s' % func.upper())
604 osutil_cflags.append('-DHAVE_%s' % func.upper())
604
605
605 for plat, macro, code in [
606 for plat, macro, code in [
606 ('bsd|darwin', 'BSD_STATFS', '''
607 ('bsd|darwin', 'BSD_STATFS', '''
607 #include <sys/param.h>
608 #include <sys/param.h>
608 #include <sys/mount.h>
609 #include <sys/mount.h>
609 int main() { struct statfs s; return sizeof(s.f_fstypename); }
610 int main() { struct statfs s; return sizeof(s.f_fstypename); }
610 '''),
611 '''),
611 ('linux', 'LINUX_STATFS', '''
612 ('linux', 'LINUX_STATFS', '''
612 #include <linux/magic.h>
613 #include <linux/magic.h>
613 #include <sys/vfs.h>
614 #include <sys/vfs.h>
614 int main() { struct statfs s; return sizeof(s.f_type); }
615 int main() { struct statfs s; return sizeof(s.f_type); }
615 '''),
616 '''),
616 ]:
617 ]:
617 if re.search(plat, sys.platform) and cancompile(new_compiler(), code):
618 if re.search(plat, sys.platform) and cancompile(new_compiler(), code):
618 osutil_cflags.append('-DHAVE_%s' % macro)
619 osutil_cflags.append('-DHAVE_%s' % macro)
619
620
620 if sys.platform == 'darwin':
621 if sys.platform == 'darwin':
621 osutil_ldflags += ['-framework', 'ApplicationServices']
622 osutil_ldflags += ['-framework', 'ApplicationServices']
622
623
623 extmodules = [
624 extmodules = [
624 Extension('mercurial.cext.base85', ['mercurial/cext/base85.c'],
625 Extension('mercurial.cext.base85', ['mercurial/cext/base85.c'],
625 include_dirs=common_include_dirs,
626 include_dirs=common_include_dirs,
626 depends=common_depends),
627 depends=common_depends),
627 Extension('mercurial.cext.bdiff', ['mercurial/bdiff.c',
628 Extension('mercurial.cext.bdiff', ['mercurial/bdiff.c',
628 'mercurial/cext/bdiff.c'],
629 'mercurial/cext/bdiff.c'],
629 include_dirs=common_include_dirs,
630 include_dirs=common_include_dirs,
630 depends=common_depends + ['mercurial/bdiff.h']),
631 depends=common_depends + ['mercurial/bdiff.h']),
631 Extension('mercurial.cext.diffhelpers', ['mercurial/cext/diffhelpers.c'],
632 Extension('mercurial.cext.diffhelpers', ['mercurial/cext/diffhelpers.c'],
632 include_dirs=common_include_dirs,
633 include_dirs=common_include_dirs,
633 depends=common_depends),
634 depends=common_depends),
634 Extension('mercurial.cext.mpatch', ['mercurial/mpatch.c',
635 Extension('mercurial.cext.mpatch', ['mercurial/mpatch.c',
635 'mercurial/cext/mpatch.c'],
636 'mercurial/cext/mpatch.c'],
636 include_dirs=common_include_dirs,
637 include_dirs=common_include_dirs,
637 depends=common_depends),
638 depends=common_depends),
638 Extension('mercurial.cext.parsers', ['mercurial/cext/dirs.c',
639 Extension('mercurial.cext.parsers', ['mercurial/cext/dirs.c',
639 'mercurial/cext/manifest.c',
640 'mercurial/cext/manifest.c',
640 'mercurial/cext/parsers.c',
641 'mercurial/cext/parsers.c',
641 'mercurial/cext/pathencode.c',
642 'mercurial/cext/pathencode.c',
642 'mercurial/cext/revlog.c'],
643 'mercurial/cext/revlog.c'],
643 include_dirs=common_include_dirs,
644 include_dirs=common_include_dirs,
644 depends=common_depends),
645 depends=common_depends),
645 Extension('mercurial.cext.osutil', ['mercurial/cext/osutil.c'],
646 Extension('mercurial.cext.osutil', ['mercurial/cext/osutil.c'],
646 include_dirs=common_include_dirs,
647 include_dirs=common_include_dirs,
647 extra_compile_args=osutil_cflags,
648 extra_compile_args=osutil_cflags,
648 extra_link_args=osutil_ldflags,
649 extra_link_args=osutil_ldflags,
649 depends=common_depends),
650 depends=common_depends),
650 Extension('hgext.fsmonitor.pywatchman.bser',
651 Extension('hgext.fsmonitor.pywatchman.bser',
651 ['hgext/fsmonitor/pywatchman/bser.c']),
652 ['hgext/fsmonitor/pywatchman/bser.c']),
652 ]
653 ]
653
654
654 sys.path.insert(0, 'contrib/python-zstandard')
655 sys.path.insert(0, 'contrib/python-zstandard')
655 import setup_zstd
656 import setup_zstd
656 extmodules.append(setup_zstd.get_c_extension(name='mercurial.zstd'))
657 extmodules.append(setup_zstd.get_c_extension(name='mercurial.zstd'))
657
658
658 try:
659 try:
659 from distutils import cygwinccompiler
660 from distutils import cygwinccompiler
660
661
661 # the -mno-cygwin option has been deprecated for years
662 # the -mno-cygwin option has been deprecated for years
662 compiler = cygwinccompiler.Mingw32CCompiler
663 compiler = cygwinccompiler.Mingw32CCompiler
663
664
664 class HackedMingw32CCompiler(cygwinccompiler.Mingw32CCompiler):
665 class HackedMingw32CCompiler(cygwinccompiler.Mingw32CCompiler):
665 def __init__(self, *args, **kwargs):
666 def __init__(self, *args, **kwargs):
666 compiler.__init__(self, *args, **kwargs)
667 compiler.__init__(self, *args, **kwargs)
667 for i in 'compiler compiler_so linker_exe linker_so'.split():
668 for i in 'compiler compiler_so linker_exe linker_so'.split():
668 try:
669 try:
669 getattr(self, i).remove('-mno-cygwin')
670 getattr(self, i).remove('-mno-cygwin')
670 except ValueError:
671 except ValueError:
671 pass
672 pass
672
673
673 cygwinccompiler.Mingw32CCompiler = HackedMingw32CCompiler
674 cygwinccompiler.Mingw32CCompiler = HackedMingw32CCompiler
674 except ImportError:
675 except ImportError:
675 # the cygwinccompiler package is not available on some Python
676 # the cygwinccompiler package is not available on some Python
676 # distributions like the ones from the optware project for Synology
677 # distributions like the ones from the optware project for Synology
677 # DiskStation boxes
678 # DiskStation boxes
678 class HackedMingw32CCompiler(object):
679 class HackedMingw32CCompiler(object):
679 pass
680 pass
680
681
681 packagedata = {'mercurial': ['locale/*/LC_MESSAGES/hg.mo',
682 packagedata = {'mercurial': ['locale/*/LC_MESSAGES/hg.mo',
682 'help/*.txt',
683 'help/*.txt',
683 'help/internals/*.txt',
684 'help/internals/*.txt',
684 'default.d/*.rc',
685 'default.d/*.rc',
685 'dummycert.pem']}
686 'dummycert.pem']}
686
687
687 def ordinarypath(p):
688 def ordinarypath(p):
688 return p and p[0] != '.' and p[-1] != '~'
689 return p and p[0] != '.' and p[-1] != '~'
689
690
690 for root in ('templates',):
691 for root in ('templates',):
691 for curdir, dirs, files in os.walk(os.path.join('mercurial', root)):
692 for curdir, dirs, files in os.walk(os.path.join('mercurial', root)):
692 curdir = curdir.split(os.sep, 1)[1]
693 curdir = curdir.split(os.sep, 1)[1]
693 dirs[:] = filter(ordinarypath, dirs)
694 dirs[:] = filter(ordinarypath, dirs)
694 for f in filter(ordinarypath, files):
695 for f in filter(ordinarypath, files):
695 f = os.path.join(curdir, f)
696 f = os.path.join(curdir, f)
696 packagedata['mercurial'].append(f)
697 packagedata['mercurial'].append(f)
697
698
698 datafiles = []
699 datafiles = []
699
700
700 # distutils expects version to be str/unicode. Converting it to
701 # distutils expects version to be str/unicode. Converting it to
701 # unicode on Python 2 still works because it won't contain any
702 # unicode on Python 2 still works because it won't contain any
702 # non-ascii bytes and will be implicitly converted back to bytes
703 # non-ascii bytes and will be implicitly converted back to bytes
703 # when operated on.
704 # when operated on.
704 assert isinstance(version, bytes)
705 assert isinstance(version, bytes)
705 setupversion = version.decode('ascii')
706 setupversion = version.decode('ascii')
706
707
707 extra = {}
708 extra = {}
708
709
709 if py2exeloaded:
710 if py2exeloaded:
710 extra['console'] = [
711 extra['console'] = [
711 {'script':'hg',
712 {'script':'hg',
712 'copyright':'Copyright (C) 2005-2017 Matt Mackall and others',
713 'copyright':'Copyright (C) 2005-2017 Matt Mackall and others',
713 'product_version':version}]
714 'product_version':version}]
714 # sub command of 'build' because 'py2exe' does not handle sub_commands
715 # sub command of 'build' because 'py2exe' does not handle sub_commands
715 build.sub_commands.insert(0, ('build_hgextindex', None))
716 build.sub_commands.insert(0, ('build_hgextindex', None))
716 # put dlls in sub directory so that they won't pollute PATH
717 # put dlls in sub directory so that they won't pollute PATH
717 extra['zipfile'] = 'lib/library.zip'
718 extra['zipfile'] = 'lib/library.zip'
718
719
719 if os.name == 'nt':
720 if os.name == 'nt':
720 # Windows binary file versions for exe/dll files must have the
721 # Windows binary file versions for exe/dll files must have the
721 # form W.X.Y.Z, where W,X,Y,Z are numbers in the range 0..65535
722 # form W.X.Y.Z, where W,X,Y,Z are numbers in the range 0..65535
722 setupversion = version.split('+', 1)[0]
723 setupversion = version.split('+', 1)[0]
723
724
724 if sys.platform == 'darwin' and os.path.exists('/usr/bin/xcodebuild'):
725 if sys.platform == 'darwin' and os.path.exists('/usr/bin/xcodebuild'):
725 version = runcmd(['/usr/bin/xcodebuild', '-version'], {})[0].splitlines()
726 version = runcmd(['/usr/bin/xcodebuild', '-version'], {})[0].splitlines()
726 if version:
727 if version:
727 version = version[0]
728 version = version[0]
728 if sys.version_info[0] == 3:
729 if sys.version_info[0] == 3:
729 version = version.decode('utf-8')
730 version = version.decode('utf-8')
730 xcode4 = (version.startswith('Xcode') and
731 xcode4 = (version.startswith('Xcode') and
731 StrictVersion(version.split()[1]) >= StrictVersion('4.0'))
732 StrictVersion(version.split()[1]) >= StrictVersion('4.0'))
732 xcode51 = re.match(r'^Xcode\s+5\.1', version) is not None
733 xcode51 = re.match(r'^Xcode\s+5\.1', version) is not None
733 else:
734 else:
734 # xcodebuild returns empty on OS X Lion with XCode 4.3 not
735 # xcodebuild returns empty on OS X Lion with XCode 4.3 not
735 # installed, but instead with only command-line tools. Assume
736 # installed, but instead with only command-line tools. Assume
736 # that only happens on >= Lion, thus no PPC support.
737 # that only happens on >= Lion, thus no PPC support.
737 xcode4 = True
738 xcode4 = True
738 xcode51 = False
739 xcode51 = False
739
740
740 # XCode 4.0 dropped support for ppc architecture, which is hardcoded in
741 # XCode 4.0 dropped support for ppc architecture, which is hardcoded in
741 # distutils.sysconfig
742 # distutils.sysconfig
742 if xcode4:
743 if xcode4:
743 os.environ['ARCHFLAGS'] = ''
744 os.environ['ARCHFLAGS'] = ''
744
745
745 # XCode 5.1 changes clang such that it now fails to compile if the
746 # XCode 5.1 changes clang such that it now fails to compile if the
746 # -mno-fused-madd flag is passed, but the version of Python shipped with
747 # -mno-fused-madd flag is passed, but the version of Python shipped with
747 # OS X 10.9 Mavericks includes this flag. This causes problems in all
748 # OS X 10.9 Mavericks includes this flag. This causes problems in all
748 # C extension modules, and a bug has been filed upstream at
749 # C extension modules, and a bug has been filed upstream at
749 # http://bugs.python.org/issue21244. We also need to patch this here
750 # http://bugs.python.org/issue21244. We also need to patch this here
750 # so Mercurial can continue to compile in the meantime.
751 # so Mercurial can continue to compile in the meantime.
751 if xcode51:
752 if xcode51:
752 cflags = get_config_var('CFLAGS')
753 cflags = get_config_var('CFLAGS')
753 if cflags and re.search(r'-mno-fused-madd\b', cflags) is not None:
754 if cflags and re.search(r'-mno-fused-madd\b', cflags) is not None:
754 os.environ['CFLAGS'] = (
755 os.environ['CFLAGS'] = (
755 os.environ.get('CFLAGS', '') + ' -Qunused-arguments')
756 os.environ.get('CFLAGS', '') + ' -Qunused-arguments')
756
757
757 setup(name='mercurial',
758 setup(name='mercurial',
758 version=setupversion,
759 version=setupversion,
759 author='Matt Mackall and many others',
760 author='Matt Mackall and many others',
760 author_email='mercurial@mercurial-scm.org',
761 author_email='mercurial@mercurial-scm.org',
761 url='https://mercurial-scm.org/',
762 url='https://mercurial-scm.org/',
762 download_url='https://mercurial-scm.org/release/',
763 download_url='https://mercurial-scm.org/release/',
763 description=('Fast scalable distributed SCM (revision control, version '
764 description=('Fast scalable distributed SCM (revision control, version '
764 'control) system'),
765 'control) system'),
765 long_description=('Mercurial is a distributed SCM tool written in Python.'
766 long_description=('Mercurial is a distributed SCM tool written in Python.'
766 ' It is used by a number of large projects that require'
767 ' It is used by a number of large projects that require'
767 ' fast, reliable distributed revision control, such as '
768 ' fast, reliable distributed revision control, such as '
768 'Mozilla.'),
769 'Mozilla.'),
769 license='GNU GPLv2 or any later version',
770 license='GNU GPLv2 or any later version',
770 classifiers=[
771 classifiers=[
771 'Development Status :: 6 - Mature',
772 'Development Status :: 6 - Mature',
772 'Environment :: Console',
773 'Environment :: Console',
773 'Intended Audience :: Developers',
774 'Intended Audience :: Developers',
774 'Intended Audience :: System Administrators',
775 'Intended Audience :: System Administrators',
775 'License :: OSI Approved :: GNU General Public License (GPL)',
776 'License :: OSI Approved :: GNU General Public License (GPL)',
776 'Natural Language :: Danish',
777 'Natural Language :: Danish',
777 'Natural Language :: English',
778 'Natural Language :: English',
778 'Natural Language :: German',
779 'Natural Language :: German',
779 'Natural Language :: Italian',
780 'Natural Language :: Italian',
780 'Natural Language :: Japanese',
781 'Natural Language :: Japanese',
781 'Natural Language :: Portuguese (Brazilian)',
782 'Natural Language :: Portuguese (Brazilian)',
782 'Operating System :: Microsoft :: Windows',
783 'Operating System :: Microsoft :: Windows',
783 'Operating System :: OS Independent',
784 'Operating System :: OS Independent',
784 'Operating System :: POSIX',
785 'Operating System :: POSIX',
785 'Programming Language :: C',
786 'Programming Language :: C',
786 'Programming Language :: Python',
787 'Programming Language :: Python',
787 'Topic :: Software Development :: Version Control',
788 'Topic :: Software Development :: Version Control',
788 ],
789 ],
789 scripts=scripts,
790 scripts=scripts,
790 packages=packages,
791 packages=packages,
791 ext_modules=extmodules,
792 ext_modules=extmodules,
792 data_files=datafiles,
793 data_files=datafiles,
793 package_data=packagedata,
794 package_data=packagedata,
794 cmdclass=cmdclass,
795 cmdclass=cmdclass,
795 distclass=hgdist,
796 distclass=hgdist,
796 options={'py2exe': {'packages': ['hgext', 'email']},
797 options={'py2exe': {'packages': ['hgdemandimport', 'hgext', 'email']},
797 'bdist_mpkg': {'zipdist': False,
798 'bdist_mpkg': {'zipdist': False,
798 'license': 'COPYING',
799 'license': 'COPYING',
799 'readme': 'contrib/macosx/Readme.html',
800 'readme': 'contrib/macosx/Readme.html',
800 'welcome': 'contrib/macosx/Welcome.html',
801 'welcome': 'contrib/macosx/Welcome.html',
801 },
802 },
802 },
803 },
803 **extra)
804 **extra)
General Comments 0
You need to be logged in to leave comments. Login now