##// END OF EJS Templates
thirdparty: vendor tomli...
Raphaël Gomès -
r51654:2c34c9b6 default
parent child Browse files
Show More
@@ -0,0 +1,21 b''
1 MIT License
2
3 Copyright (c) 2021 Taneli Hukkinen
4
5 Permission is hereby granted, free of charge, to any person obtaining a copy
6 of this software and associated documentation files (the "Software"), to deal
7 in the Software without restriction, including without limitation the rights
8 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 copies of the Software, and to permit persons to whom the Software is
10 furnished to do so, subject to the following conditions:
11
12 The above copyright notice and this permission notice shall be included in all
13 copies or substantial portions of the Software.
14
15 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 SOFTWARE.
@@ -0,0 +1,182 b''
1 [![Build Status](https://github.com/hukkin/tomli/workflows/Tests/badge.svg?branch=master)](https://github.com/hukkin/tomli/actions?query=workflow%3ATests+branch%3Amaster+event%3Apush)
2 [![codecov.io](https://codecov.io/gh/hukkin/tomli/branch/master/graph/badge.svg)](https://codecov.io/gh/hukkin/tomli)
3 [![PyPI version](https://img.shields.io/pypi/v/tomli)](https://pypi.org/project/tomli)
4
5 # Tomli
6
7 > A lil' TOML parser
8
9 **Table of Contents** *generated with [mdformat-toc](https://github.com/hukkin/mdformat-toc)*
10
11 <!-- mdformat-toc start --slug=github --maxlevel=6 --minlevel=2 -->
12
13 - [Intro](#intro)
14 - [Installation](#installation)
15 - [Usage](#usage)
16 - [Parse a TOML string](#parse-a-toml-string)
17 - [Parse a TOML file](#parse-a-toml-file)
18 - [Handle invalid TOML](#handle-invalid-toml)
19 - [Construct `decimal.Decimal`s from TOML floats](#construct-decimaldecimals-from-toml-floats)
20 - [FAQ](#faq)
21 - [Why this parser?](#why-this-parser)
22 - [Is comment preserving round-trip parsing supported?](#is-comment-preserving-round-trip-parsing-supported)
23 - [Is there a `dumps`, `write` or `encode` function?](#is-there-a-dumps-write-or-encode-function)
24 - [How do TOML types map into Python types?](#how-do-toml-types-map-into-python-types)
25 - [Performance](#performance)
26
27 <!-- mdformat-toc end -->
28
29 ## Intro<a name="intro"></a>
30
31 Tomli is a Python library for parsing [TOML](https://toml.io).
32 Tomli is fully compatible with [TOML v1.0.0](https://toml.io/en/v1.0.0).
33
34 ## Installation<a name="installation"></a>
35
36 ```bash
37 pip install tomli
38 ```
39
40 ## Usage<a name="usage"></a>
41
42 ### Parse a TOML string<a name="parse-a-toml-string"></a>
43
44 ```python
45 import tomli
46
47 toml_str = """
48 gretzky = 99
49
50 [kurri]
51 jari = 17
52 """
53
54 toml_dict = tomli.loads(toml_str)
55 assert toml_dict == {"gretzky": 99, "kurri": {"jari": 17}}
56 ```
57
58 ### Parse a TOML file<a name="parse-a-toml-file"></a>
59
60 ```python
61 import tomli
62
63 with open("path_to_file/conf.toml", "rb") as f:
64 toml_dict = tomli.load(f)
65 ```
66
67 The file must be opened in binary mode (with the `"rb"` flag).
68 Binary mode will enforce decoding the file as UTF-8 with universal newlines disabled,
69 both of which are required to correctly parse TOML.
70 Support for text file objects is deprecated for removal in the next major release.
71
72 ### Handle invalid TOML<a name="handle-invalid-toml"></a>
73
74 ```python
75 import tomli
76
77 try:
78 toml_dict = tomli.loads("]] this is invalid TOML [[")
79 except tomli.TOMLDecodeError:
80 print("Yep, definitely not valid.")
81 ```
82
83 Note that while the `TOMLDecodeError` type is public API, error messages of raised instances of it are not.
84 Error messages should not be assumed to stay constant across Tomli versions.
85
86 ### Construct `decimal.Decimal`s from TOML floats<a name="construct-decimaldecimals-from-toml-floats"></a>
87
88 ```python
89 from decimal import Decimal
90 import tomli
91
92 toml_dict = tomli.loads("precision-matters = 0.982492", parse_float=Decimal)
93 assert toml_dict["precision-matters"] == Decimal("0.982492")
94 ```
95
96 Note that `decimal.Decimal` can be replaced with another callable that converts a TOML float from string to a Python type.
97 The `decimal.Decimal` is, however, a practical choice for use cases where float inaccuracies can not be tolerated.
98
99 Illegal types include `dict`, `list`, and anything that has the `append` attribute.
100 Parsing floats into an illegal type results in undefined behavior.
101
102 ## FAQ<a name="faq"></a>
103
104 ### Why this parser?<a name="why-this-parser"></a>
105
106 - it's lil'
107 - pure Python with zero dependencies
108 - the fastest pure Python parser [\*](#performance):
109 15x as fast as [tomlkit](https://pypi.org/project/tomlkit/),
110 2.4x as fast as [toml](https://pypi.org/project/toml/)
111 - outputs [basic data types](#how-do-toml-types-map-into-python-types) only
112 - 100% spec compliant: passes all tests in
113 [a test set](https://github.com/toml-lang/compliance/pull/8)
114 soon to be merged to the official
115 [compliance tests for TOML](https://github.com/toml-lang/compliance)
116 repository
117 - thoroughly tested: 100% branch coverage
118
119 ### Is comment preserving round-trip parsing supported?<a name="is-comment-preserving-round-trip-parsing-supported"></a>
120
121 No.
122
123 The `tomli.loads` function returns a plain `dict` that is populated with builtin types and types from the standard library only.
124 Preserving comments requires a custom type to be returned so will not be supported,
125 at least not by the `tomli.loads` and `tomli.load` functions.
126
127 Look into [TOML Kit](https://github.com/sdispater/tomlkit) if preservation of style is what you need.
128
129 ### Is there a `dumps`, `write` or `encode` function?<a name="is-there-a-dumps-write-or-encode-function"></a>
130
131 [Tomli-W](https://github.com/hukkin/tomli-w) is the write-only counterpart of Tomli, providing `dump` and `dumps` functions.
132
133 The core library does not include write capability, as most TOML use cases are read-only, and Tomli intends to be minimal.
134
135 ### How do TOML types map into Python types?<a name="how-do-toml-types-map-into-python-types"></a>
136
137 | TOML type | Python type | Details |
138 | ---------------- | ------------------- | ------------------------------------------------------------ |
139 | Document Root | `dict` | |
140 | Key | `str` | |
141 | String | `str` | |
142 | Integer | `int` | |
143 | Float | `float` | |
144 | Boolean | `bool` | |
145 | Offset Date-Time | `datetime.datetime` | `tzinfo` attribute set to an instance of `datetime.timezone` |
146 | Local Date-Time | `datetime.datetime` | `tzinfo` attribute set to `None` |
147 | Local Date | `datetime.date` | |
148 | Local Time | `datetime.time` | |
149 | Array | `list` | |
150 | Table | `dict` | |
151 | Inline Table | `dict` | |
152
153 ## Performance<a name="performance"></a>
154
155 The `benchmark/` folder in this repository contains a performance benchmark for comparing the various Python TOML parsers.
156 The benchmark can be run with `tox -e benchmark-pypi`.
157 Running the benchmark on my personal computer output the following:
158
159 ```console
160 foo@bar:~/dev/tomli$ tox -e benchmark-pypi
161 benchmark-pypi installed: attrs==19.3.0,click==7.1.2,pytomlpp==1.0.2,qtoml==0.3.0,rtoml==0.7.0,toml==0.10.2,tomli==1.1.0,tomlkit==0.7.2
162 benchmark-pypi run-test-pre: PYTHONHASHSEED='2658546909'
163 benchmark-pypi run-test: commands[0] | python -c 'import datetime; print(datetime.date.today())'
164 2021-07-23
165 benchmark-pypi run-test: commands[1] | python --version
166 Python 3.8.10
167 benchmark-pypi run-test: commands[2] | python benchmark/run.py
168 Parsing data.toml 5000 times:
169 ------------------------------------------------------
170 parser | exec time | performance (more is better)
171 -----------+------------+-----------------------------
172 rtoml | 0.901 s | baseline (100%)
173 pytomlpp | 1.08 s | 83.15%
174 tomli | 3.89 s | 23.15%
175 toml | 9.36 s | 9.63%
176 qtoml | 11.5 s | 7.82%
177 tomlkit | 56.8 s | 1.59%
178 ```
179
180 The parsers are ordered from fastest to slowest, using the fastest parser as baseline.
181 Tomli performed the best out of all pure Python TOML parsers,
182 losing only to pytomlpp (wraps C++) and rtoml (wraps Rust).
@@ -0,0 +1,9 b''
1 """A lil' TOML parser."""
2
3 __all__ = ("loads", "load", "TOMLDecodeError")
4 __version__ = "1.2.3" # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT
5
6 from ._parser import TOMLDecodeError, load, loads
7
8 # Pretend this exception was created here.
9 TOMLDecodeError.__module__ = "tomli"
This diff has been collapsed as it changes many lines, (663 lines changed) Show them Hide them
@@ -0,0 +1,663 b''
1 import string
2 from types import MappingProxyType
3 from typing import Any, BinaryIO, Dict, FrozenSet, Iterable, NamedTuple, Optional, Tuple
4 import warnings
5
6 from ._re import (
7 RE_DATETIME,
8 RE_LOCALTIME,
9 RE_NUMBER,
10 match_to_datetime,
11 match_to_localtime,
12 match_to_number,
13 )
14 from ._types import Key, ParseFloat, Pos
15
16 ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127))
17
18 # Neither of these sets include quotation mark or backslash. They are
19 # currently handled as separate cases in the parser functions.
20 ILLEGAL_BASIC_STR_CHARS = ASCII_CTRL - frozenset("\t")
21 ILLEGAL_MULTILINE_BASIC_STR_CHARS = ASCII_CTRL - frozenset("\t\n")
22
23 ILLEGAL_LITERAL_STR_CHARS = ILLEGAL_BASIC_STR_CHARS
24 ILLEGAL_MULTILINE_LITERAL_STR_CHARS = ILLEGAL_MULTILINE_BASIC_STR_CHARS
25
26 ILLEGAL_COMMENT_CHARS = ILLEGAL_BASIC_STR_CHARS
27
28 TOML_WS = frozenset(" \t")
29 TOML_WS_AND_NEWLINE = TOML_WS | frozenset("\n")
30 BARE_KEY_CHARS = frozenset(string.ascii_letters + string.digits + "-_")
31 KEY_INITIAL_CHARS = BARE_KEY_CHARS | frozenset("\"'")
32 HEXDIGIT_CHARS = frozenset(string.hexdigits)
33
34 BASIC_STR_ESCAPE_REPLACEMENTS = MappingProxyType(
35 {
36 "\\b": "\u0008", # backspace
37 "\\t": "\u0009", # tab
38 "\\n": "\u000A", # linefeed
39 "\\f": "\u000C", # form feed
40 "\\r": "\u000D", # carriage return
41 '\\"': "\u0022", # quote
42 "\\\\": "\u005C", # backslash
43 }
44 )
45
46
47 class TOMLDecodeError(ValueError):
48 """An error raised if a document is not valid TOML."""
49
50
51 def load(fp: BinaryIO, *, parse_float: ParseFloat = float) -> Dict[str, Any]:
52 """Parse TOML from a binary file object."""
53 s_bytes = fp.read()
54 try:
55 s = s_bytes.decode()
56 except AttributeError:
57 warnings.warn(
58 "Text file object support is deprecated in favor of binary file objects."
59 ' Use `open("foo.toml", "rb")` to open the file in binary mode.',
60 DeprecationWarning,
61 stacklevel=2,
62 )
63 s = s_bytes # type: ignore[assignment]
64 return loads(s, parse_float=parse_float)
65
66
67 def loads(s: str, *, parse_float: ParseFloat = float) -> Dict[str, Any]: # noqa: C901
68 """Parse TOML from a string."""
69
70 # The spec allows converting "\r\n" to "\n", even in string
71 # literals. Let's do so to simplify parsing.
72 src = s.replace("\r\n", "\n")
73 pos = 0
74 out = Output(NestedDict(), Flags())
75 header: Key = ()
76
77 # Parse one statement at a time
78 # (typically means one line in TOML source)
79 while True:
80 # 1. Skip line leading whitespace
81 pos = skip_chars(src, pos, TOML_WS)
82
83 # 2. Parse rules. Expect one of the following:
84 # - end of file
85 # - end of line
86 # - comment
87 # - key/value pair
88 # - append dict to list (and move to its namespace)
89 # - create dict (and move to its namespace)
90 # Skip trailing whitespace when applicable.
91 try:
92 char = src[pos]
93 except IndexError:
94 break
95 if char == "\n":
96 pos += 1
97 continue
98 if char in KEY_INITIAL_CHARS:
99 pos = key_value_rule(src, pos, out, header, parse_float)
100 pos = skip_chars(src, pos, TOML_WS)
101 elif char == "[":
102 try:
103 second_char: Optional[str] = src[pos + 1]
104 except IndexError:
105 second_char = None
106 if second_char == "[":
107 pos, header = create_list_rule(src, pos, out)
108 else:
109 pos, header = create_dict_rule(src, pos, out)
110 pos = skip_chars(src, pos, TOML_WS)
111 elif char != "#":
112 raise suffixed_err(src, pos, "Invalid statement")
113
114 # 3. Skip comment
115 pos = skip_comment(src, pos)
116
117 # 4. Expect end of line or end of file
118 try:
119 char = src[pos]
120 except IndexError:
121 break
122 if char != "\n":
123 raise suffixed_err(
124 src, pos, "Expected newline or end of document after a statement"
125 )
126 pos += 1
127
128 return out.data.dict
129
130
131 class Flags:
132 """Flags that map to parsed keys/namespaces."""
133
134 # Marks an immutable namespace (inline array or inline table).
135 FROZEN = 0
136 # Marks a nest that has been explicitly created and can no longer
137 # be opened using the "[table]" syntax.
138 EXPLICIT_NEST = 1
139
140 def __init__(self) -> None:
141 self._flags: Dict[str, dict] = {}
142
143 def unset_all(self, key: Key) -> None:
144 cont = self._flags
145 for k in key[:-1]:
146 if k not in cont:
147 return
148 cont = cont[k]["nested"]
149 cont.pop(key[-1], None)
150
151 def set_for_relative_key(self, head_key: Key, rel_key: Key, flag: int) -> None:
152 cont = self._flags
153 for k in head_key:
154 if k not in cont:
155 cont[k] = {"flags": set(), "recursive_flags": set(), "nested": {}}
156 cont = cont[k]["nested"]
157 for k in rel_key:
158 if k in cont:
159 cont[k]["flags"].add(flag)
160 else:
161 cont[k] = {"flags": {flag}, "recursive_flags": set(), "nested": {}}
162 cont = cont[k]["nested"]
163
164 def set(self, key: Key, flag: int, *, recursive: bool) -> None: # noqa: A003
165 cont = self._flags
166 key_parent, key_stem = key[:-1], key[-1]
167 for k in key_parent:
168 if k not in cont:
169 cont[k] = {"flags": set(), "recursive_flags": set(), "nested": {}}
170 cont = cont[k]["nested"]
171 if key_stem not in cont:
172 cont[key_stem] = {"flags": set(), "recursive_flags": set(), "nested": {}}
173 cont[key_stem]["recursive_flags" if recursive else "flags"].add(flag)
174
175 def is_(self, key: Key, flag: int) -> bool:
176 if not key:
177 return False # document root has no flags
178 cont = self._flags
179 for k in key[:-1]:
180 if k not in cont:
181 return False
182 inner_cont = cont[k]
183 if flag in inner_cont["recursive_flags"]:
184 return True
185 cont = inner_cont["nested"]
186 key_stem = key[-1]
187 if key_stem in cont:
188 cont = cont[key_stem]
189 return flag in cont["flags"] or flag in cont["recursive_flags"]
190 return False
191
192
193 class NestedDict:
194 def __init__(self) -> None:
195 # The parsed content of the TOML document
196 self.dict: Dict[str, Any] = {}
197
198 def get_or_create_nest(
199 self,
200 key: Key,
201 *,
202 access_lists: bool = True,
203 ) -> dict:
204 cont: Any = self.dict
205 for k in key:
206 if k not in cont:
207 cont[k] = {}
208 cont = cont[k]
209 if access_lists and isinstance(cont, list):
210 cont = cont[-1]
211 if not isinstance(cont, dict):
212 raise KeyError("There is no nest behind this key")
213 return cont
214
215 def append_nest_to_list(self, key: Key) -> None:
216 cont = self.get_or_create_nest(key[:-1])
217 last_key = key[-1]
218 if last_key in cont:
219 list_ = cont[last_key]
220 try:
221 list_.append({})
222 except AttributeError:
223 raise KeyError("An object other than list found behind this key")
224 else:
225 cont[last_key] = [{}]
226
227
228 class Output(NamedTuple):
229 data: NestedDict
230 flags: Flags
231
232
233 def skip_chars(src: str, pos: Pos, chars: Iterable[str]) -> Pos:
234 try:
235 while src[pos] in chars:
236 pos += 1
237 except IndexError:
238 pass
239 return pos
240
241
242 def skip_until(
243 src: str,
244 pos: Pos,
245 expect: str,
246 *,
247 error_on: FrozenSet[str],
248 error_on_eof: bool,
249 ) -> Pos:
250 try:
251 new_pos = src.index(expect, pos)
252 except ValueError:
253 new_pos = len(src)
254 if error_on_eof:
255 raise suffixed_err(src, new_pos, f"Expected {expect!r}") from None
256
257 if not error_on.isdisjoint(src[pos:new_pos]):
258 while src[pos] not in error_on:
259 pos += 1
260 raise suffixed_err(src, pos, f"Found invalid character {src[pos]!r}")
261 return new_pos
262
263
264 def skip_comment(src: str, pos: Pos) -> Pos:
265 try:
266 char: Optional[str] = src[pos]
267 except IndexError:
268 char = None
269 if char == "#":
270 return skip_until(
271 src, pos + 1, "\n", error_on=ILLEGAL_COMMENT_CHARS, error_on_eof=False
272 )
273 return pos
274
275
276 def skip_comments_and_array_ws(src: str, pos: Pos) -> Pos:
277 while True:
278 pos_before_skip = pos
279 pos = skip_chars(src, pos, TOML_WS_AND_NEWLINE)
280 pos = skip_comment(src, pos)
281 if pos == pos_before_skip:
282 return pos
283
284
285 def create_dict_rule(src: str, pos: Pos, out: Output) -> Tuple[Pos, Key]:
286 pos += 1 # Skip "["
287 pos = skip_chars(src, pos, TOML_WS)
288 pos, key = parse_key(src, pos)
289
290 if out.flags.is_(key, Flags.EXPLICIT_NEST) or out.flags.is_(key, Flags.FROZEN):
291 raise suffixed_err(src, pos, f"Can not declare {key} twice")
292 out.flags.set(key, Flags.EXPLICIT_NEST, recursive=False)
293 try:
294 out.data.get_or_create_nest(key)
295 except KeyError:
296 raise suffixed_err(src, pos, "Can not overwrite a value") from None
297
298 if not src.startswith("]", pos):
299 raise suffixed_err(src, pos, 'Expected "]" at the end of a table declaration')
300 return pos + 1, key
301
302
303 def create_list_rule(src: str, pos: Pos, out: Output) -> Tuple[Pos, Key]:
304 pos += 2 # Skip "[["
305 pos = skip_chars(src, pos, TOML_WS)
306 pos, key = parse_key(src, pos)
307
308 if out.flags.is_(key, Flags.FROZEN):
309 raise suffixed_err(src, pos, f"Can not mutate immutable namespace {key}")
310 # Free the namespace now that it points to another empty list item...
311 out.flags.unset_all(key)
312 # ...but this key precisely is still prohibited from table declaration
313 out.flags.set(key, Flags.EXPLICIT_NEST, recursive=False)
314 try:
315 out.data.append_nest_to_list(key)
316 except KeyError:
317 raise suffixed_err(src, pos, "Can not overwrite a value") from None
318
319 if not src.startswith("]]", pos):
320 raise suffixed_err(src, pos, 'Expected "]]" at the end of an array declaration')
321 return pos + 2, key
322
323
324 def key_value_rule(
325 src: str, pos: Pos, out: Output, header: Key, parse_float: ParseFloat
326 ) -> Pos:
327 pos, key, value = parse_key_value_pair(src, pos, parse_float)
328 key_parent, key_stem = key[:-1], key[-1]
329 abs_key_parent = header + key_parent
330
331 if out.flags.is_(abs_key_parent, Flags.FROZEN):
332 raise suffixed_err(
333 src, pos, f"Can not mutate immutable namespace {abs_key_parent}"
334 )
335 # Containers in the relative path can't be opened with the table syntax after this
336 out.flags.set_for_relative_key(header, key, Flags.EXPLICIT_NEST)
337 try:
338 nest = out.data.get_or_create_nest(abs_key_parent)
339 except KeyError:
340 raise suffixed_err(src, pos, "Can not overwrite a value") from None
341 if key_stem in nest:
342 raise suffixed_err(src, pos, "Can not overwrite a value")
343 # Mark inline table and array namespaces recursively immutable
344 if isinstance(value, (dict, list)):
345 out.flags.set(header + key, Flags.FROZEN, recursive=True)
346 nest[key_stem] = value
347 return pos
348
349
350 def parse_key_value_pair(
351 src: str, pos: Pos, parse_float: ParseFloat
352 ) -> Tuple[Pos, Key, Any]:
353 pos, key = parse_key(src, pos)
354 try:
355 char: Optional[str] = src[pos]
356 except IndexError:
357 char = None
358 if char != "=":
359 raise suffixed_err(src, pos, 'Expected "=" after a key in a key/value pair')
360 pos += 1
361 pos = skip_chars(src, pos, TOML_WS)
362 pos, value = parse_value(src, pos, parse_float)
363 return pos, key, value
364
365
366 def parse_key(src: str, pos: Pos) -> Tuple[Pos, Key]:
367 pos, key_part = parse_key_part(src, pos)
368 key: Key = (key_part,)
369 pos = skip_chars(src, pos, TOML_WS)
370 while True:
371 try:
372 char: Optional[str] = src[pos]
373 except IndexError:
374 char = None
375 if char != ".":
376 return pos, key
377 pos += 1
378 pos = skip_chars(src, pos, TOML_WS)
379 pos, key_part = parse_key_part(src, pos)
380 key += (key_part,)
381 pos = skip_chars(src, pos, TOML_WS)
382
383
384 def parse_key_part(src: str, pos: Pos) -> Tuple[Pos, str]:
385 try:
386 char: Optional[str] = src[pos]
387 except IndexError:
388 char = None
389 if char in BARE_KEY_CHARS:
390 start_pos = pos
391 pos = skip_chars(src, pos, BARE_KEY_CHARS)
392 return pos, src[start_pos:pos]
393 if char == "'":
394 return parse_literal_str(src, pos)
395 if char == '"':
396 return parse_one_line_basic_str(src, pos)
397 raise suffixed_err(src, pos, "Invalid initial character for a key part")
398
399
400 def parse_one_line_basic_str(src: str, pos: Pos) -> Tuple[Pos, str]:
401 pos += 1
402 return parse_basic_str(src, pos, multiline=False)
403
404
405 def parse_array(src: str, pos: Pos, parse_float: ParseFloat) -> Tuple[Pos, list]:
406 pos += 1
407 array: list = []
408
409 pos = skip_comments_and_array_ws(src, pos)
410 if src.startswith("]", pos):
411 return pos + 1, array
412 while True:
413 pos, val = parse_value(src, pos, parse_float)
414 array.append(val)
415 pos = skip_comments_and_array_ws(src, pos)
416
417 c = src[pos : pos + 1]
418 if c == "]":
419 return pos + 1, array
420 if c != ",":
421 raise suffixed_err(src, pos, "Unclosed array")
422 pos += 1
423
424 pos = skip_comments_and_array_ws(src, pos)
425 if src.startswith("]", pos):
426 return pos + 1, array
427
428
429 def parse_inline_table(src: str, pos: Pos, parse_float: ParseFloat) -> Tuple[Pos, dict]:
430 pos += 1
431 nested_dict = NestedDict()
432 flags = Flags()
433
434 pos = skip_chars(src, pos, TOML_WS)
435 if src.startswith("}", pos):
436 return pos + 1, nested_dict.dict
437 while True:
438 pos, key, value = parse_key_value_pair(src, pos, parse_float)
439 key_parent, key_stem = key[:-1], key[-1]
440 if flags.is_(key, Flags.FROZEN):
441 raise suffixed_err(src, pos, f"Can not mutate immutable namespace {key}")
442 try:
443 nest = nested_dict.get_or_create_nest(key_parent, access_lists=False)
444 except KeyError:
445 raise suffixed_err(src, pos, "Can not overwrite a value") from None
446 if key_stem in nest:
447 raise suffixed_err(src, pos, f"Duplicate inline table key {key_stem!r}")
448 nest[key_stem] = value
449 pos = skip_chars(src, pos, TOML_WS)
450 c = src[pos : pos + 1]
451 if c == "}":
452 return pos + 1, nested_dict.dict
453 if c != ",":
454 raise suffixed_err(src, pos, "Unclosed inline table")
455 if isinstance(value, (dict, list)):
456 flags.set(key, Flags.FROZEN, recursive=True)
457 pos += 1
458 pos = skip_chars(src, pos, TOML_WS)
459
460
461 def parse_basic_str_escape( # noqa: C901
462 src: str, pos: Pos, *, multiline: bool = False
463 ) -> Tuple[Pos, str]:
464 escape_id = src[pos : pos + 2]
465 pos += 2
466 if multiline and escape_id in {"\\ ", "\\\t", "\\\n"}:
467 # Skip whitespace until next non-whitespace character or end of
468 # the doc. Error if non-whitespace is found before newline.
469 if escape_id != "\\\n":
470 pos = skip_chars(src, pos, TOML_WS)
471 try:
472 char = src[pos]
473 except IndexError:
474 return pos, ""
475 if char != "\n":
476 raise suffixed_err(src, pos, 'Unescaped "\\" in a string')
477 pos += 1
478 pos = skip_chars(src, pos, TOML_WS_AND_NEWLINE)
479 return pos, ""
480 if escape_id == "\\u":
481 return parse_hex_char(src, pos, 4)
482 if escape_id == "\\U":
483 return parse_hex_char(src, pos, 8)
484 try:
485 return pos, BASIC_STR_ESCAPE_REPLACEMENTS[escape_id]
486 except KeyError:
487 if len(escape_id) != 2:
488 raise suffixed_err(src, pos, "Unterminated string") from None
489 raise suffixed_err(src, pos, 'Unescaped "\\" in a string') from None
490
491
492 def parse_basic_str_escape_multiline(src: str, pos: Pos) -> Tuple[Pos, str]:
493 return parse_basic_str_escape(src, pos, multiline=True)
494
495
496 def parse_hex_char(src: str, pos: Pos, hex_len: int) -> Tuple[Pos, str]:
497 hex_str = src[pos : pos + hex_len]
498 if len(hex_str) != hex_len or not HEXDIGIT_CHARS.issuperset(hex_str):
499 raise suffixed_err(src, pos, "Invalid hex value")
500 pos += hex_len
501 hex_int = int(hex_str, 16)
502 if not is_unicode_scalar_value(hex_int):
503 raise suffixed_err(src, pos, "Escaped character is not a Unicode scalar value")
504 return pos, chr(hex_int)
505
506
507 def parse_literal_str(src: str, pos: Pos) -> Tuple[Pos, str]:
508 pos += 1 # Skip starting apostrophe
509 start_pos = pos
510 pos = skip_until(
511 src, pos, "'", error_on=ILLEGAL_LITERAL_STR_CHARS, error_on_eof=True
512 )
513 return pos + 1, src[start_pos:pos] # Skip ending apostrophe
514
515
516 def parse_multiline_str(src: str, pos: Pos, *, literal: bool) -> Tuple[Pos, str]:
517 pos += 3
518 if src.startswith("\n", pos):
519 pos += 1
520
521 if literal:
522 delim = "'"
523 end_pos = skip_until(
524 src,
525 pos,
526 "'''",
527 error_on=ILLEGAL_MULTILINE_LITERAL_STR_CHARS,
528 error_on_eof=True,
529 )
530 result = src[pos:end_pos]
531 pos = end_pos + 3
532 else:
533 delim = '"'
534 pos, result = parse_basic_str(src, pos, multiline=True)
535
536 # Add at maximum two extra apostrophes/quotes if the end sequence
537 # is 4 or 5 chars long instead of just 3.
538 if not src.startswith(delim, pos):
539 return pos, result
540 pos += 1
541 if not src.startswith(delim, pos):
542 return pos, result + delim
543 pos += 1
544 return pos, result + (delim * 2)
545
546
547 def parse_basic_str(src: str, pos: Pos, *, multiline: bool) -> Tuple[Pos, str]:
548 if multiline:
549 error_on = ILLEGAL_MULTILINE_BASIC_STR_CHARS
550 parse_escapes = parse_basic_str_escape_multiline
551 else:
552 error_on = ILLEGAL_BASIC_STR_CHARS
553 parse_escapes = parse_basic_str_escape
554 result = ""
555 start_pos = pos
556 while True:
557 try:
558 char = src[pos]
559 except IndexError:
560 raise suffixed_err(src, pos, "Unterminated string") from None
561 if char == '"':
562 if not multiline:
563 return pos + 1, result + src[start_pos:pos]
564 if src.startswith('"""', pos):
565 return pos + 3, result + src[start_pos:pos]
566 pos += 1
567 continue
568 if char == "\\":
569 result += src[start_pos:pos]
570 pos, parsed_escape = parse_escapes(src, pos)
571 result += parsed_escape
572 start_pos = pos
573 continue
574 if char in error_on:
575 raise suffixed_err(src, pos, f"Illegal character {char!r}")
576 pos += 1
577
578
579 def parse_value( # noqa: C901
580 src: str, pos: Pos, parse_float: ParseFloat
581 ) -> Tuple[Pos, Any]:
582 try:
583 char: Optional[str] = src[pos]
584 except IndexError:
585 char = None
586
587 # Basic strings
588 if char == '"':
589 if src.startswith('"""', pos):
590 return parse_multiline_str(src, pos, literal=False)
591 return parse_one_line_basic_str(src, pos)
592
593 # Literal strings
594 if char == "'":
595 if src.startswith("'''", pos):
596 return parse_multiline_str(src, pos, literal=True)
597 return parse_literal_str(src, pos)
598
599 # Booleans
600 if char == "t":
601 if src.startswith("true", pos):
602 return pos + 4, True
603 if char == "f":
604 if src.startswith("false", pos):
605 return pos + 5, False
606
607 # Dates and times
608 datetime_match = RE_DATETIME.match(src, pos)
609 if datetime_match:
610 try:
611 datetime_obj = match_to_datetime(datetime_match)
612 except ValueError as e:
613 raise suffixed_err(src, pos, "Invalid date or datetime") from e
614 return datetime_match.end(), datetime_obj
615 localtime_match = RE_LOCALTIME.match(src, pos)
616 if localtime_match:
617 return localtime_match.end(), match_to_localtime(localtime_match)
618
619 # Integers and "normal" floats.
620 # The regex will greedily match any type starting with a decimal
621 # char, so needs to be located after handling of dates and times.
622 number_match = RE_NUMBER.match(src, pos)
623 if number_match:
624 return number_match.end(), match_to_number(number_match, parse_float)
625
626 # Arrays
627 if char == "[":
628 return parse_array(src, pos, parse_float)
629
630 # Inline tables
631 if char == "{":
632 return parse_inline_table(src, pos, parse_float)
633
634 # Special floats
635 first_three = src[pos : pos + 3]
636 if first_three in {"inf", "nan"}:
637 return pos + 3, parse_float(first_three)
638 first_four = src[pos : pos + 4]
639 if first_four in {"-inf", "+inf", "-nan", "+nan"}:
640 return pos + 4, parse_float(first_four)
641
642 raise suffixed_err(src, pos, "Invalid value")
643
644
645 def suffixed_err(src: str, pos: Pos, msg: str) -> TOMLDecodeError:
646 """Return a `TOMLDecodeError` where error message is suffixed with
647 coordinates in source."""
648
649 def coord_repr(src: str, pos: Pos) -> str:
650 if pos >= len(src):
651 return "end of document"
652 line = src.count("\n", 0, pos) + 1
653 if line == 1:
654 column = pos + 1
655 else:
656 column = pos - src.rindex("\n", 0, pos)
657 return f"line {line}, column {column}"
658
659 return TOMLDecodeError(f"{msg} (at {coord_repr(src, pos)})")
660
661
662 def is_unicode_scalar_value(codepoint: int) -> bool:
663 return (0 <= codepoint <= 55295) or (57344 <= codepoint <= 1114111)
@@ -0,0 +1,101 b''
1 from datetime import date, datetime, time, timedelta, timezone, tzinfo
2 from functools import lru_cache
3 import re
4 from typing import Any, Optional, Union
5
6 from ._types import ParseFloat
7
8 # E.g.
9 # - 00:32:00.999999
10 # - 00:32:00
11 _TIME_RE_STR = r"([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(?:\.([0-9]{1,6})[0-9]*)?"
12
13 RE_NUMBER = re.compile(
14 r"""
15 0
16 (?:
17 x[0-9A-Fa-f](?:_?[0-9A-Fa-f])* # hex
18 |
19 b[01](?:_?[01])* # bin
20 |
21 o[0-7](?:_?[0-7])* # oct
22 )
23 |
24 [+-]?(?:0|[1-9](?:_?[0-9])*) # dec, integer part
25 (?P<floatpart>
26 (?:\.[0-9](?:_?[0-9])*)? # optional fractional part
27 (?:[eE][+-]?[0-9](?:_?[0-9])*)? # optional exponent part
28 )
29 """,
30 flags=re.VERBOSE,
31 )
32 RE_LOCALTIME = re.compile(_TIME_RE_STR)
33 RE_DATETIME = re.compile(
34 fr"""
35 ([0-9]{{4}})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) # date, e.g. 1988-10-27
36 (?:
37 [Tt ]
38 {_TIME_RE_STR}
39 (?:([Zz])|([+-])([01][0-9]|2[0-3]):([0-5][0-9]))? # optional time offset
40 )?
41 """,
42 flags=re.VERBOSE,
43 )
44
45
46 def match_to_datetime(match: "re.Match") -> Union[datetime, date]:
47 """Convert a `RE_DATETIME` match to `datetime.datetime` or `datetime.date`.
48
49 Raises ValueError if the match does not correspond to a valid date
50 or datetime.
51 """
52 (
53 year_str,
54 month_str,
55 day_str,
56 hour_str,
57 minute_str,
58 sec_str,
59 micros_str,
60 zulu_time,
61 offset_sign_str,
62 offset_hour_str,
63 offset_minute_str,
64 ) = match.groups()
65 year, month, day = int(year_str), int(month_str), int(day_str)
66 if hour_str is None:
67 return date(year, month, day)
68 hour, minute, sec = int(hour_str), int(minute_str), int(sec_str)
69 micros = int(micros_str.ljust(6, "0")) if micros_str else 0
70 if offset_sign_str:
71 tz: Optional[tzinfo] = cached_tz(
72 offset_hour_str, offset_minute_str, offset_sign_str
73 )
74 elif zulu_time:
75 tz = timezone.utc
76 else: # local date-time
77 tz = None
78 return datetime(year, month, day, hour, minute, sec, micros, tzinfo=tz)
79
80
81 @lru_cache(maxsize=None)
82 def cached_tz(hour_str: str, minute_str: str, sign_str: str) -> timezone:
83 sign = 1 if sign_str == "+" else -1
84 return timezone(
85 timedelta(
86 hours=sign * int(hour_str),
87 minutes=sign * int(minute_str),
88 )
89 )
90
91
92 def match_to_localtime(match: "re.Match") -> time:
93 hour_str, minute_str, sec_str, micros_str = match.groups()
94 micros = int(micros_str.ljust(6, "0")) if micros_str else 0
95 return time(int(hour_str), int(minute_str), int(sec_str), micros)
96
97
98 def match_to_number(match: "re.Match", parse_float: "ParseFloat") -> Any:
99 if match.group("floatpart"):
100 return parse_float(match.group())
101 return int(match.group(), 0)
@@ -0,0 +1,6 b''
1 from typing import Any, Callable, Tuple
2
3 # Type annotations
4 ParseFloat = Callable[[str], Any]
5 Key = Tuple[str, ...]
6 Pos = int
@@ -0,0 +1,1 b''
1 # Marker file for PEP 561
@@ -1,773 +1,774 b''
1 #!/usr/bin/env python3
1 #!/usr/bin/env python3
2
2
3
3
4 import ast
4 import ast
5 import collections
5 import collections
6 import io
6 import io
7 import os
7 import os
8 import sys
8 import sys
9
9
10 # Import a minimal set of stdlib modules needed for list_stdlib_modules()
10 # Import a minimal set of stdlib modules needed for list_stdlib_modules()
11 # to work when run from a virtualenv. The modules were chosen empirically
11 # to work when run from a virtualenv. The modules were chosen empirically
12 # so that the return value matches the return value without virtualenv.
12 # so that the return value matches the return value without virtualenv.
13 if True: # disable lexical sorting checks
13 if True: # disable lexical sorting checks
14 try:
14 try:
15 import BaseHTTPServer as basehttpserver
15 import BaseHTTPServer as basehttpserver
16 except ImportError:
16 except ImportError:
17 basehttpserver = None
17 basehttpserver = None
18 import zlib
18 import zlib
19
19
20 import testparseutil
20 import testparseutil
21
21
22 # Allow list of modules that symbols can be directly imported from.
22 # Allow list of modules that symbols can be directly imported from.
23 allowsymbolimports = (
23 allowsymbolimports = (
24 '__future__',
24 '__future__',
25 'breezy',
25 'breezy',
26 'concurrent',
26 'concurrent',
27 'hgclient',
27 'hgclient',
28 'mercurial',
28 'mercurial',
29 'mercurial.hgweb.common',
29 'mercurial.hgweb.common',
30 'mercurial.hgweb.request',
30 'mercurial.hgweb.request',
31 'mercurial.i18n',
31 'mercurial.i18n',
32 'mercurial.interfaces',
32 'mercurial.interfaces',
33 'mercurial.node',
33 'mercurial.node',
34 'mercurial.pycompat',
34 'mercurial.pycompat',
35 # for revlog to re-export constant to extensions
35 # for revlog to re-export constant to extensions
36 'mercurial.revlogutils.constants',
36 'mercurial.revlogutils.constants',
37 'mercurial.revlogutils.flagutil',
37 'mercurial.revlogutils.flagutil',
38 # for cffi modules to re-export pure functions
38 # for cffi modules to re-export pure functions
39 'mercurial.pure.base85',
39 'mercurial.pure.base85',
40 'mercurial.pure.bdiff',
40 'mercurial.pure.bdiff',
41 'mercurial.pure.mpatch',
41 'mercurial.pure.mpatch',
42 'mercurial.pure.osutil',
42 'mercurial.pure.osutil',
43 'mercurial.pure.parsers',
43 'mercurial.pure.parsers',
44 # third-party imports should be directly imported
44 # third-party imports should be directly imported
45 'mercurial.thirdparty',
45 'mercurial.thirdparty',
46 'mercurial.thirdparty.attr',
46 'mercurial.thirdparty.attr',
47 'mercurial.thirdparty.jaraco.collections',
47 'mercurial.thirdparty.jaraco.collections',
48 'mercurial.thirdparty.tomli',
48 'mercurial.thirdparty.zope',
49 'mercurial.thirdparty.zope',
49 'mercurial.thirdparty.zope.interface',
50 'mercurial.thirdparty.zope.interface',
50 'typing',
51 'typing',
51 'xml.etree.ElementTree',
52 'xml.etree.ElementTree',
52 )
53 )
53
54
54 # Allow list of symbols that can be directly imported.
55 # Allow list of symbols that can be directly imported.
55 directsymbols = ('demandimport',)
56 directsymbols = ('demandimport',)
56
57
57 # Modules that must be aliased because they are commonly confused with
58 # Modules that must be aliased because they are commonly confused with
58 # common variables and can create aliasing and readability issues.
59 # common variables and can create aliasing and readability issues.
59 requirealias = {
60 requirealias = {
60 'ui': 'uimod',
61 'ui': 'uimod',
61 }
62 }
62
63
63
64
64 def walklocal(root):
65 def walklocal(root):
65 """Recursively yield all descendant nodes but not in a different scope"""
66 """Recursively yield all descendant nodes but not in a different scope"""
66 todo = collections.deque(ast.iter_child_nodes(root))
67 todo = collections.deque(ast.iter_child_nodes(root))
67 yield root, False
68 yield root, False
68 while todo:
69 while todo:
69 node = todo.popleft()
70 node = todo.popleft()
70 newscope = isinstance(node, ast.FunctionDef)
71 newscope = isinstance(node, ast.FunctionDef)
71 if not newscope:
72 if not newscope:
72 todo.extend(ast.iter_child_nodes(node))
73 todo.extend(ast.iter_child_nodes(node))
73 yield node, newscope
74 yield node, newscope
74
75
75
76
76 def dotted_name_of_path(path):
77 def dotted_name_of_path(path):
77 """Given a relative path to a source file, return its dotted module name.
78 """Given a relative path to a source file, return its dotted module name.
78
79
79 >>> dotted_name_of_path('mercurial/error.py')
80 >>> dotted_name_of_path('mercurial/error.py')
80 'mercurial.error'
81 'mercurial.error'
81 >>> dotted_name_of_path('zlibmodule.so')
82 >>> dotted_name_of_path('zlibmodule.so')
82 'zlib'
83 'zlib'
83 """
84 """
84 parts = path.replace(os.sep, '/').split('/')
85 parts = path.replace(os.sep, '/').split('/')
85 parts[-1] = parts[-1].split('.', 1)[0] # remove .py and .so and .ARCH.so
86 parts[-1] = parts[-1].split('.', 1)[0] # remove .py and .so and .ARCH.so
86 if parts[-1].endswith('module'):
87 if parts[-1].endswith('module'):
87 parts[-1] = parts[-1][:-6]
88 parts[-1] = parts[-1][:-6]
88 return '.'.join(parts)
89 return '.'.join(parts)
89
90
90
91
91 def fromlocalfunc(modulename, localmods):
92 def fromlocalfunc(modulename, localmods):
92 """Get a function to examine which locally defined module the
93 """Get a function to examine which locally defined module the
93 target source imports via a specified name.
94 target source imports via a specified name.
94
95
95 `modulename` is an `dotted_name_of_path()`-ed source file path,
96 `modulename` is an `dotted_name_of_path()`-ed source file path,
96 which may have `.__init__` at the end of it, of the target source.
97 which may have `.__init__` at the end of it, of the target source.
97
98
98 `localmods` is a set of absolute `dotted_name_of_path()`-ed source file
99 `localmods` is a set of absolute `dotted_name_of_path()`-ed source file
99 paths of locally defined (= Mercurial specific) modules.
100 paths of locally defined (= Mercurial specific) modules.
100
101
101 This function assumes that module names not existing in
102 This function assumes that module names not existing in
102 `localmods` are from the Python standard library.
103 `localmods` are from the Python standard library.
103
104
104 This function returns the function, which takes `name` argument,
105 This function returns the function, which takes `name` argument,
105 and returns `(absname, dottedpath, hassubmod)` tuple if `name`
106 and returns `(absname, dottedpath, hassubmod)` tuple if `name`
106 matches against locally defined module. Otherwise, it returns
107 matches against locally defined module. Otherwise, it returns
107 False.
108 False.
108
109
109 It is assumed that `name` doesn't have `.__init__`.
110 It is assumed that `name` doesn't have `.__init__`.
110
111
111 `absname` is an absolute module name of specified `name`
112 `absname` is an absolute module name of specified `name`
112 (e.g. "hgext.convert"). This can be used to compose prefix for sub
113 (e.g. "hgext.convert"). This can be used to compose prefix for sub
113 modules or so.
114 modules or so.
114
115
115 `dottedpath` is a `dotted_name_of_path()`-ed source file path
116 `dottedpath` is a `dotted_name_of_path()`-ed source file path
116 (e.g. "hgext.convert.__init__") of `name`. This is used to look
117 (e.g. "hgext.convert.__init__") of `name`. This is used to look
117 module up in `localmods` again.
118 module up in `localmods` again.
118
119
119 `hassubmod` is whether it may have sub modules under it (for
120 `hassubmod` is whether it may have sub modules under it (for
120 convenient, even though this is also equivalent to "absname !=
121 convenient, even though this is also equivalent to "absname !=
121 dottednpath")
122 dottednpath")
122
123
123 >>> localmods = {'foo.__init__', 'foo.foo1',
124 >>> localmods = {'foo.__init__', 'foo.foo1',
124 ... 'foo.bar.__init__', 'foo.bar.bar1',
125 ... 'foo.bar.__init__', 'foo.bar.bar1',
125 ... 'baz.__init__', 'baz.baz1'}
126 ... 'baz.__init__', 'baz.baz1'}
126 >>> fromlocal = fromlocalfunc('foo.xxx', localmods)
127 >>> fromlocal = fromlocalfunc('foo.xxx', localmods)
127 >>> # relative
128 >>> # relative
128 >>> fromlocal('foo1')
129 >>> fromlocal('foo1')
129 ('foo.foo1', 'foo.foo1', False)
130 ('foo.foo1', 'foo.foo1', False)
130 >>> fromlocal('bar')
131 >>> fromlocal('bar')
131 ('foo.bar', 'foo.bar.__init__', True)
132 ('foo.bar', 'foo.bar.__init__', True)
132 >>> fromlocal('bar.bar1')
133 >>> fromlocal('bar.bar1')
133 ('foo.bar.bar1', 'foo.bar.bar1', False)
134 ('foo.bar.bar1', 'foo.bar.bar1', False)
134 >>> # absolute
135 >>> # absolute
135 >>> fromlocal('baz')
136 >>> fromlocal('baz')
136 ('baz', 'baz.__init__', True)
137 ('baz', 'baz.__init__', True)
137 >>> fromlocal('baz.baz1')
138 >>> fromlocal('baz.baz1')
138 ('baz.baz1', 'baz.baz1', False)
139 ('baz.baz1', 'baz.baz1', False)
139 >>> # unknown = maybe standard library
140 >>> # unknown = maybe standard library
140 >>> fromlocal('os')
141 >>> fromlocal('os')
141 False
142 False
142 >>> fromlocal(None, 1)
143 >>> fromlocal(None, 1)
143 ('foo', 'foo.__init__', True)
144 ('foo', 'foo.__init__', True)
144 >>> fromlocal('foo1', 1)
145 >>> fromlocal('foo1', 1)
145 ('foo.foo1', 'foo.foo1', False)
146 ('foo.foo1', 'foo.foo1', False)
146 >>> fromlocal2 = fromlocalfunc('foo.xxx.yyy', localmods)
147 >>> fromlocal2 = fromlocalfunc('foo.xxx.yyy', localmods)
147 >>> fromlocal2(None, 2)
148 >>> fromlocal2(None, 2)
148 ('foo', 'foo.__init__', True)
149 ('foo', 'foo.__init__', True)
149 >>> fromlocal2('bar2', 1)
150 >>> fromlocal2('bar2', 1)
150 False
151 False
151 >>> fromlocal2('bar', 2)
152 >>> fromlocal2('bar', 2)
152 ('foo.bar', 'foo.bar.__init__', True)
153 ('foo.bar', 'foo.bar.__init__', True)
153 """
154 """
154 if not isinstance(modulename, str):
155 if not isinstance(modulename, str):
155 modulename = modulename.decode('ascii')
156 modulename = modulename.decode('ascii')
156 prefix = '.'.join(modulename.split('.')[:-1])
157 prefix = '.'.join(modulename.split('.')[:-1])
157 if prefix:
158 if prefix:
158 prefix += '.'
159 prefix += '.'
159
160
160 def fromlocal(name, level=0):
161 def fromlocal(name, level=0):
161 # name is false value when relative imports are used.
162 # name is false value when relative imports are used.
162 if not name:
163 if not name:
163 # If relative imports are used, level must not be absolute.
164 # If relative imports are used, level must not be absolute.
164 assert level > 0
165 assert level > 0
165 candidates = ['.'.join(modulename.split('.')[:-level])]
166 candidates = ['.'.join(modulename.split('.')[:-level])]
166 else:
167 else:
167 if not level:
168 if not level:
168 # Check relative name first.
169 # Check relative name first.
169 candidates = [prefix + name, name]
170 candidates = [prefix + name, name]
170 else:
171 else:
171 candidates = [
172 candidates = [
172 '.'.join(modulename.split('.')[:-level]) + '.' + name
173 '.'.join(modulename.split('.')[:-level]) + '.' + name
173 ]
174 ]
174
175
175 for n in candidates:
176 for n in candidates:
176 if n in localmods:
177 if n in localmods:
177 return (n, n, False)
178 return (n, n, False)
178 dottedpath = n + '.__init__'
179 dottedpath = n + '.__init__'
179 if dottedpath in localmods:
180 if dottedpath in localmods:
180 return (n, dottedpath, True)
181 return (n, dottedpath, True)
181 return False
182 return False
182
183
183 return fromlocal
184 return fromlocal
184
185
185
186
186 def populateextmods(localmods):
187 def populateextmods(localmods):
187 """Populate C extension modules based on pure modules"""
188 """Populate C extension modules based on pure modules"""
188 newlocalmods = set(localmods)
189 newlocalmods = set(localmods)
189 for n in localmods:
190 for n in localmods:
190 if n.startswith('mercurial.pure.'):
191 if n.startswith('mercurial.pure.'):
191 m = n[len('mercurial.pure.') :]
192 m = n[len('mercurial.pure.') :]
192 newlocalmods.add('mercurial.cext.' + m)
193 newlocalmods.add('mercurial.cext.' + m)
193 newlocalmods.add('mercurial.cffi._' + m)
194 newlocalmods.add('mercurial.cffi._' + m)
194 return newlocalmods
195 return newlocalmods
195
196
196
197
197 def list_stdlib_modules():
198 def list_stdlib_modules():
198 """List the modules present in the stdlib.
199 """List the modules present in the stdlib.
199
200
200 >>> py3 = sys.version_info[0] >= 3
201 >>> py3 = sys.version_info[0] >= 3
201 >>> mods = set(list_stdlib_modules())
202 >>> mods = set(list_stdlib_modules())
202 >>> 'BaseHTTPServer' in mods or py3
203 >>> 'BaseHTTPServer' in mods or py3
203 True
204 True
204
205
205 os.path isn't really a module, so it's missing:
206 os.path isn't really a module, so it's missing:
206
207
207 >>> 'os.path' in mods
208 >>> 'os.path' in mods
208 False
209 False
209
210
210 sys requires special treatment, because it's baked into the
211 sys requires special treatment, because it's baked into the
211 interpreter, but it should still appear:
212 interpreter, but it should still appear:
212
213
213 >>> 'sys' in mods
214 >>> 'sys' in mods
214 True
215 True
215
216
216 >>> 'collections' in mods
217 >>> 'collections' in mods
217 True
218 True
218
219
219 >>> 'cStringIO' in mods or py3
220 >>> 'cStringIO' in mods or py3
220 True
221 True
221
222
222 >>> 'cffi' in mods
223 >>> 'cffi' in mods
223 True
224 True
224 """
225 """
225 for m in sys.builtin_module_names:
226 for m in sys.builtin_module_names:
226 yield m
227 yield m
227 # These modules only exist on windows, but we should always
228 # These modules only exist on windows, but we should always
228 # consider them stdlib.
229 # consider them stdlib.
229 for m in ['msvcrt', '_winreg']:
230 for m in ['msvcrt', '_winreg']:
230 yield m
231 yield m
231 yield '__builtin__'
232 yield '__builtin__'
232 yield 'builtins' # python3 only
233 yield 'builtins' # python3 only
233 yield 'importlib.abc' # python3 only
234 yield 'importlib.abc' # python3 only
234 yield 'importlib.machinery' # python3 only
235 yield 'importlib.machinery' # python3 only
235 yield 'importlib.util' # python3 only
236 yield 'importlib.util' # python3 only
236 yield 'packaging.version'
237 yield 'packaging.version'
237 for m in 'fcntl', 'grp', 'pwd', 'termios': # Unix only
238 for m in 'fcntl', 'grp', 'pwd', 'termios': # Unix only
238 yield m
239 yield m
239 for m in 'cPickle', 'datetime': # in Python (not C) on PyPy
240 for m in 'cPickle', 'datetime': # in Python (not C) on PyPy
240 yield m
241 yield m
241 for m in ['cffi']:
242 for m in ['cffi']:
242 yield m
243 yield m
243 stdlib_prefixes = {sys.prefix, sys.exec_prefix}
244 stdlib_prefixes = {sys.prefix, sys.exec_prefix}
244 # We need to supplement the list of prefixes for the search to work
245 # We need to supplement the list of prefixes for the search to work
245 # when run from within a virtualenv.
246 # when run from within a virtualenv.
246 for mod in (basehttpserver, zlib):
247 for mod in (basehttpserver, zlib):
247 if mod is None:
248 if mod is None:
248 continue
249 continue
249 try:
250 try:
250 # Not all module objects have a __file__ attribute.
251 # Not all module objects have a __file__ attribute.
251 filename = mod.__file__
252 filename = mod.__file__
252 except AttributeError:
253 except AttributeError:
253 continue
254 continue
254 dirname = os.path.dirname(filename)
255 dirname = os.path.dirname(filename)
255 for prefix in stdlib_prefixes:
256 for prefix in stdlib_prefixes:
256 if dirname.startswith(prefix):
257 if dirname.startswith(prefix):
257 # Then this directory is redundant.
258 # Then this directory is redundant.
258 break
259 break
259 else:
260 else:
260 stdlib_prefixes.add(dirname)
261 stdlib_prefixes.add(dirname)
261 sourceroot = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
262 sourceroot = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
262 for libpath in sys.path:
263 for libpath in sys.path:
263 # We want to walk everything in sys.path that starts with something in
264 # We want to walk everything in sys.path that starts with something in
264 # stdlib_prefixes, but not directories from the hg sources.
265 # stdlib_prefixes, but not directories from the hg sources.
265 if os.path.abspath(libpath).startswith(sourceroot) or not any(
266 if os.path.abspath(libpath).startswith(sourceroot) or not any(
266 libpath.startswith(p) for p in stdlib_prefixes
267 libpath.startswith(p) for p in stdlib_prefixes
267 ):
268 ):
268 continue
269 continue
269 for top, dirs, files in os.walk(libpath):
270 for top, dirs, files in os.walk(libpath):
270 if 'dist-packages' in top.split(os.path.sep):
271 if 'dist-packages' in top.split(os.path.sep):
271 continue
272 continue
272 for i, d in reversed(list(enumerate(dirs))):
273 for i, d in reversed(list(enumerate(dirs))):
273 if (
274 if (
274 not os.path.exists(os.path.join(top, d, '__init__.py'))
275 not os.path.exists(os.path.join(top, d, '__init__.py'))
275 or top == libpath
276 or top == libpath
276 and d in ('hgdemandimport', 'hgext', 'mercurial')
277 and d in ('hgdemandimport', 'hgext', 'mercurial')
277 ):
278 ):
278 del dirs[i]
279 del dirs[i]
279 for name in files:
280 for name in files:
280 if not name.endswith(('.py', '.so', '.pyc', '.pyo', '.pyd')):
281 if not name.endswith(('.py', '.so', '.pyc', '.pyo', '.pyd')):
281 continue
282 continue
282 if name.startswith('__init__.py'):
283 if name.startswith('__init__.py'):
283 full_path = top
284 full_path = top
284 else:
285 else:
285 full_path = os.path.join(top, name)
286 full_path = os.path.join(top, name)
286 rel_path = full_path[len(libpath) + 1 :]
287 rel_path = full_path[len(libpath) + 1 :]
287 mod = dotted_name_of_path(rel_path)
288 mod = dotted_name_of_path(rel_path)
288 yield mod
289 yield mod
289
290
290
291
291 stdlib_modules = set(list_stdlib_modules())
292 stdlib_modules = set(list_stdlib_modules())
292
293
293
294
294 def imported_modules(source, modulename, f, localmods, ignore_nested=False):
295 def imported_modules(source, modulename, f, localmods, ignore_nested=False):
295 """Given the source of a file as a string, yield the names
296 """Given the source of a file as a string, yield the names
296 imported by that file.
297 imported by that file.
297
298
298 Args:
299 Args:
299 source: The python source to examine as a string.
300 source: The python source to examine as a string.
300 modulename: of specified python source (may have `__init__`)
301 modulename: of specified python source (may have `__init__`)
301 localmods: set of locally defined module names (may have `__init__`)
302 localmods: set of locally defined module names (may have `__init__`)
302 ignore_nested: If true, import statements that do not start in
303 ignore_nested: If true, import statements that do not start in
303 column zero will be ignored.
304 column zero will be ignored.
304
305
305 Returns:
306 Returns:
306 A list of absolute module names imported by the given source.
307 A list of absolute module names imported by the given source.
307
308
308 >>> f = 'foo/xxx.py'
309 >>> f = 'foo/xxx.py'
309 >>> modulename = 'foo.xxx'
310 >>> modulename = 'foo.xxx'
310 >>> localmods = {'foo.__init__': True,
311 >>> localmods = {'foo.__init__': True,
311 ... 'foo.foo1': True, 'foo.foo2': True,
312 ... 'foo.foo1': True, 'foo.foo2': True,
312 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
313 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
313 ... 'baz.__init__': True, 'baz.baz1': True }
314 ... 'baz.__init__': True, 'baz.baz1': True }
314 >>> # standard library (= not locally defined ones)
315 >>> # standard library (= not locally defined ones)
315 >>> sorted(imported_modules(
316 >>> sorted(imported_modules(
316 ... 'from stdlib1 import foo, bar; import stdlib2',
317 ... 'from stdlib1 import foo, bar; import stdlib2',
317 ... modulename, f, localmods))
318 ... modulename, f, localmods))
318 []
319 []
319 >>> # relative importing
320 >>> # relative importing
320 >>> sorted(imported_modules(
321 >>> sorted(imported_modules(
321 ... 'import foo1; from bar import bar1',
322 ... 'import foo1; from bar import bar1',
322 ... modulename, f, localmods))
323 ... modulename, f, localmods))
323 ['foo.bar.bar1', 'foo.foo1']
324 ['foo.bar.bar1', 'foo.foo1']
324 >>> sorted(imported_modules(
325 >>> sorted(imported_modules(
325 ... 'from bar.bar1 import name1, name2, name3',
326 ... 'from bar.bar1 import name1, name2, name3',
326 ... modulename, f, localmods))
327 ... modulename, f, localmods))
327 ['foo.bar.bar1']
328 ['foo.bar.bar1']
328 >>> # absolute importing
329 >>> # absolute importing
329 >>> sorted(imported_modules(
330 >>> sorted(imported_modules(
330 ... 'from baz import baz1, name1',
331 ... 'from baz import baz1, name1',
331 ... modulename, f, localmods))
332 ... modulename, f, localmods))
332 ['baz.__init__', 'baz.baz1']
333 ['baz.__init__', 'baz.baz1']
333 >>> # mixed importing, even though it shouldn't be recommended
334 >>> # mixed importing, even though it shouldn't be recommended
334 >>> sorted(imported_modules(
335 >>> sorted(imported_modules(
335 ... 'import stdlib, foo1, baz',
336 ... 'import stdlib, foo1, baz',
336 ... modulename, f, localmods))
337 ... modulename, f, localmods))
337 ['baz.__init__', 'foo.foo1']
338 ['baz.__init__', 'foo.foo1']
338 >>> # ignore_nested
339 >>> # ignore_nested
339 >>> sorted(imported_modules(
340 >>> sorted(imported_modules(
340 ... '''import foo
341 ... '''import foo
341 ... def wat():
342 ... def wat():
342 ... import bar
343 ... import bar
343 ... ''', modulename, f, localmods))
344 ... ''', modulename, f, localmods))
344 ['foo.__init__', 'foo.bar.__init__']
345 ['foo.__init__', 'foo.bar.__init__']
345 >>> sorted(imported_modules(
346 >>> sorted(imported_modules(
346 ... '''import foo
347 ... '''import foo
347 ... def wat():
348 ... def wat():
348 ... import bar
349 ... import bar
349 ... ''', modulename, f, localmods, ignore_nested=True))
350 ... ''', modulename, f, localmods, ignore_nested=True))
350 ['foo.__init__']
351 ['foo.__init__']
351 """
352 """
352 fromlocal = fromlocalfunc(modulename, localmods)
353 fromlocal = fromlocalfunc(modulename, localmods)
353 for node in ast.walk(ast.parse(source, f)):
354 for node in ast.walk(ast.parse(source, f)):
354 if ignore_nested and getattr(node, 'col_offset', 0) > 0:
355 if ignore_nested and getattr(node, 'col_offset', 0) > 0:
355 continue
356 continue
356 if isinstance(node, ast.Import):
357 if isinstance(node, ast.Import):
357 for n in node.names:
358 for n in node.names:
358 found = fromlocal(n.name)
359 found = fromlocal(n.name)
359 if not found:
360 if not found:
360 # this should import standard library
361 # this should import standard library
361 continue
362 continue
362 yield found[1]
363 yield found[1]
363 elif isinstance(node, ast.ImportFrom):
364 elif isinstance(node, ast.ImportFrom):
364 found = fromlocal(node.module, node.level)
365 found = fromlocal(node.module, node.level)
365 if not found:
366 if not found:
366 # this should import standard library
367 # this should import standard library
367 continue
368 continue
368
369
369 absname, dottedpath, hassubmod = found
370 absname, dottedpath, hassubmod = found
370 if not hassubmod:
371 if not hassubmod:
371 # "dottedpath" is not a package; must be imported
372 # "dottedpath" is not a package; must be imported
372 yield dottedpath
373 yield dottedpath
373 # examination of "node.names" should be redundant
374 # examination of "node.names" should be redundant
374 # e.g.: from mercurial.node import nullid, nullrev
375 # e.g.: from mercurial.node import nullid, nullrev
375 continue
376 continue
376
377
377 modnotfound = False
378 modnotfound = False
378 prefix = absname + '.'
379 prefix = absname + '.'
379 for n in node.names:
380 for n in node.names:
380 found = fromlocal(prefix + n.name)
381 found = fromlocal(prefix + n.name)
381 if not found:
382 if not found:
382 # this should be a function or a property of "node.module"
383 # this should be a function or a property of "node.module"
383 modnotfound = True
384 modnotfound = True
384 continue
385 continue
385 yield found[1]
386 yield found[1]
386 if modnotfound and dottedpath != modulename:
387 if modnotfound and dottedpath != modulename:
387 # "dottedpath" is a package, but imported because of non-module
388 # "dottedpath" is a package, but imported because of non-module
388 # lookup
389 # lookup
389 # specifically allow "from . import foo" from __init__.py
390 # specifically allow "from . import foo" from __init__.py
390 yield dottedpath
391 yield dottedpath
391
392
392
393
393 def verify_import_convention(module, source, localmods):
394 def verify_import_convention(module, source, localmods):
394 """Verify imports match our established coding convention."""
395 """Verify imports match our established coding convention."""
395 root = ast.parse(source)
396 root = ast.parse(source)
396
397
397 return verify_modern_convention(module, root, localmods)
398 return verify_modern_convention(module, root, localmods)
398
399
399
400
400 def verify_modern_convention(module, root, localmods, root_col_offset=0):
401 def verify_modern_convention(module, root, localmods, root_col_offset=0):
401 """Verify a file conforms to the modern import convention rules.
402 """Verify a file conforms to the modern import convention rules.
402
403
403 The rules of the modern convention are:
404 The rules of the modern convention are:
404
405
405 * Ordering is stdlib followed by local imports. Each group is lexically
406 * Ordering is stdlib followed by local imports. Each group is lexically
406 sorted.
407 sorted.
407 * Importing multiple modules via "import X, Y" is not allowed: use
408 * Importing multiple modules via "import X, Y" is not allowed: use
408 separate import statements.
409 separate import statements.
409 * Importing multiple modules via "from X import ..." is allowed if using
410 * Importing multiple modules via "from X import ..." is allowed if using
410 parenthesis and one entry per line.
411 parenthesis and one entry per line.
411 * Only 1 relative import statement per import level ("from .", "from ..")
412 * Only 1 relative import statement per import level ("from .", "from ..")
412 is allowed.
413 is allowed.
413 * Relative imports from higher levels must occur before lower levels. e.g.
414 * Relative imports from higher levels must occur before lower levels. e.g.
414 "from .." must be before "from .".
415 "from .." must be before "from .".
415 * Imports from peer packages should use relative import (e.g. do not
416 * Imports from peer packages should use relative import (e.g. do not
416 "import mercurial.foo" from a "mercurial.*" module).
417 "import mercurial.foo" from a "mercurial.*" module).
417 * Symbols can only be imported from specific modules (see
418 * Symbols can only be imported from specific modules (see
418 `allowsymbolimports`). For other modules, first import the module then
419 `allowsymbolimports`). For other modules, first import the module then
419 assign the symbol to a module-level variable. In addition, these imports
420 assign the symbol to a module-level variable. In addition, these imports
420 must be performed before other local imports. This rule only
421 must be performed before other local imports. This rule only
421 applies to import statements outside of any blocks.
422 applies to import statements outside of any blocks.
422 * Relative imports from the standard library are not allowed, unless that
423 * Relative imports from the standard library are not allowed, unless that
423 library is also a local module.
424 library is also a local module.
424 * Certain modules must be aliased to alternate names to avoid aliasing
425 * Certain modules must be aliased to alternate names to avoid aliasing
425 and readability problems. See `requirealias`.
426 and readability problems. See `requirealias`.
426 """
427 """
427 if not isinstance(module, str):
428 if not isinstance(module, str):
428 module = module.decode('ascii')
429 module = module.decode('ascii')
429 topmodule = module.split('.')[0]
430 topmodule = module.split('.')[0]
430 fromlocal = fromlocalfunc(module, localmods)
431 fromlocal = fromlocalfunc(module, localmods)
431
432
432 # Whether a local/non-stdlib import has been performed.
433 # Whether a local/non-stdlib import has been performed.
433 seenlocal = None
434 seenlocal = None
434 # Whether a local/non-stdlib, non-symbol import has been seen.
435 # Whether a local/non-stdlib, non-symbol import has been seen.
435 seennonsymbollocal = False
436 seennonsymbollocal = False
436 # The last name to be imported (for sorting).
437 # The last name to be imported (for sorting).
437 lastname = None
438 lastname = None
438 laststdlib = None
439 laststdlib = None
439 # Relative import levels encountered so far.
440 # Relative import levels encountered so far.
440 seenlevels = set()
441 seenlevels = set()
441
442
442 for node, newscope in walklocal(root):
443 for node, newscope in walklocal(root):
443
444
444 def msg(fmt, *args):
445 def msg(fmt, *args):
445 return (fmt % args, node.lineno)
446 return (fmt % args, node.lineno)
446
447
447 if newscope:
448 if newscope:
448 # Check for local imports in function
449 # Check for local imports in function
449 for r in verify_modern_convention(
450 for r in verify_modern_convention(
450 module, node, localmods, node.col_offset + 4
451 module, node, localmods, node.col_offset + 4
451 ):
452 ):
452 yield r
453 yield r
453 elif isinstance(node, ast.Import):
454 elif isinstance(node, ast.Import):
454 # Disallow "import foo, bar" and require separate imports
455 # Disallow "import foo, bar" and require separate imports
455 # for each module.
456 # for each module.
456 if len(node.names) > 1:
457 if len(node.names) > 1:
457 yield msg(
458 yield msg(
458 'multiple imported names: %s',
459 'multiple imported names: %s',
459 ', '.join(n.name for n in node.names),
460 ', '.join(n.name for n in node.names),
460 )
461 )
461
462
462 name = node.names[0].name
463 name = node.names[0].name
463 asname = node.names[0].asname
464 asname = node.names[0].asname
464
465
465 stdlib = name in stdlib_modules
466 stdlib = name in stdlib_modules
466
467
467 # Ignore sorting rules on imports inside blocks.
468 # Ignore sorting rules on imports inside blocks.
468 if node.col_offset == root_col_offset:
469 if node.col_offset == root_col_offset:
469 if lastname and name < lastname and laststdlib == stdlib:
470 if lastname and name < lastname and laststdlib == stdlib:
470 yield msg(
471 yield msg(
471 'imports not lexically sorted: %s < %s', name, lastname
472 'imports not lexically sorted: %s < %s', name, lastname
472 )
473 )
473
474
474 lastname = name
475 lastname = name
475 laststdlib = stdlib
476 laststdlib = stdlib
476
477
477 # stdlib imports should be before local imports.
478 # stdlib imports should be before local imports.
478 if stdlib and seenlocal and node.col_offset == root_col_offset:
479 if stdlib and seenlocal and node.col_offset == root_col_offset:
479 yield msg(
480 yield msg(
480 'stdlib import "%s" follows local import: %s',
481 'stdlib import "%s" follows local import: %s',
481 name,
482 name,
482 seenlocal,
483 seenlocal,
483 )
484 )
484
485
485 if not stdlib:
486 if not stdlib:
486 seenlocal = name
487 seenlocal = name
487
488
488 # Import of sibling modules should use relative imports.
489 # Import of sibling modules should use relative imports.
489 topname = name.split('.')[0]
490 topname = name.split('.')[0]
490 if topname == topmodule:
491 if topname == topmodule:
491 yield msg('import should be relative: %s', name)
492 yield msg('import should be relative: %s', name)
492
493
493 if name in requirealias and asname != requirealias[name]:
494 if name in requirealias and asname != requirealias[name]:
494 yield msg(
495 yield msg(
495 '%s module must be "as" aliased to %s',
496 '%s module must be "as" aliased to %s',
496 name,
497 name,
497 requirealias[name],
498 requirealias[name],
498 )
499 )
499
500
500 elif isinstance(node, ast.ImportFrom):
501 elif isinstance(node, ast.ImportFrom):
501 # Resolve the full imported module name.
502 # Resolve the full imported module name.
502 if node.level > 0:
503 if node.level > 0:
503 fullname = '.'.join(module.split('.')[: -node.level])
504 fullname = '.'.join(module.split('.')[: -node.level])
504 if node.module:
505 if node.module:
505 fullname += '.%s' % node.module
506 fullname += '.%s' % node.module
506 else:
507 else:
507 assert node.module
508 assert node.module
508 fullname = node.module
509 fullname = node.module
509
510
510 topname = fullname.split('.')[0]
511 topname = fullname.split('.')[0]
511 if topname == topmodule:
512 if topname == topmodule:
512 yield msg('import should be relative: %s', fullname)
513 yield msg('import should be relative: %s', fullname)
513
514
514 # __future__ is special since it needs to come first and use
515 # __future__ is special since it needs to come first and use
515 # symbol import.
516 # symbol import.
516 if fullname != '__future__':
517 if fullname != '__future__':
517 if not fullname or (
518 if not fullname or (
518 fullname in stdlib_modules
519 fullname in stdlib_modules
519 # allow standard 'from typing import ...' style
520 # allow standard 'from typing import ...' style
520 and fullname.startswith('.')
521 and fullname.startswith('.')
521 and fullname not in localmods
522 and fullname not in localmods
522 and fullname + '.__init__' not in localmods
523 and fullname + '.__init__' not in localmods
523 ):
524 ):
524 yield msg('relative import of stdlib module')
525 yield msg('relative import of stdlib module')
525 else:
526 else:
526 seenlocal = fullname
527 seenlocal = fullname
527
528
528 # Direct symbol import is only allowed from certain modules and
529 # Direct symbol import is only allowed from certain modules and
529 # must occur before non-symbol imports.
530 # must occur before non-symbol imports.
530 found = fromlocal(node.module, node.level)
531 found = fromlocal(node.module, node.level)
531 if found and found[2]: # node.module is a package
532 if found and found[2]: # node.module is a package
532 prefix = found[0] + '.'
533 prefix = found[0] + '.'
533 symbols = (
534 symbols = (
534 n.name for n in node.names if not fromlocal(prefix + n.name)
535 n.name for n in node.names if not fromlocal(prefix + n.name)
535 )
536 )
536 else:
537 else:
537 symbols = (n.name for n in node.names)
538 symbols = (n.name for n in node.names)
538 symbols = [sym for sym in symbols if sym not in directsymbols]
539 symbols = [sym for sym in symbols if sym not in directsymbols]
539 if node.module and node.col_offset == root_col_offset:
540 if node.module and node.col_offset == root_col_offset:
540 if symbols and fullname not in allowsymbolimports:
541 if symbols and fullname not in allowsymbolimports:
541 yield msg(
542 yield msg(
542 'direct symbol import %s from %s',
543 'direct symbol import %s from %s',
543 ', '.join(symbols),
544 ', '.join(symbols),
544 fullname,
545 fullname,
545 )
546 )
546
547
547 if symbols and seennonsymbollocal:
548 if symbols and seennonsymbollocal:
548 yield msg(
549 yield msg(
549 'symbol import follows non-symbol import: %s', fullname
550 'symbol import follows non-symbol import: %s', fullname
550 )
551 )
551 if not symbols and fullname not in stdlib_modules:
552 if not symbols and fullname not in stdlib_modules:
552 seennonsymbollocal = True
553 seennonsymbollocal = True
553
554
554 if not node.module:
555 if not node.module:
555 assert node.level
556 assert node.level
556
557
557 # Only allow 1 group per level.
558 # Only allow 1 group per level.
558 if (
559 if (
559 node.level in seenlevels
560 node.level in seenlevels
560 and node.col_offset == root_col_offset
561 and node.col_offset == root_col_offset
561 ):
562 ):
562 yield msg(
563 yield msg(
563 'multiple "from %s import" statements', '.' * node.level
564 'multiple "from %s import" statements', '.' * node.level
564 )
565 )
565
566
566 # Higher-level groups come before lower-level groups.
567 # Higher-level groups come before lower-level groups.
567 if any(node.level > l for l in seenlevels):
568 if any(node.level > l for l in seenlevels):
568 yield msg(
569 yield msg(
569 'higher-level import should come first: %s', fullname
570 'higher-level import should come first: %s', fullname
570 )
571 )
571
572
572 seenlevels.add(node.level)
573 seenlevels.add(node.level)
573
574
574 # Entries in "from .X import ( ... )" lists must be lexically
575 # Entries in "from .X import ( ... )" lists must be lexically
575 # sorted.
576 # sorted.
576 lastentryname = None
577 lastentryname = None
577
578
578 for n in node.names:
579 for n in node.names:
579 if lastentryname and n.name < lastentryname:
580 if lastentryname and n.name < lastentryname:
580 yield msg(
581 yield msg(
581 'imports from %s not lexically sorted: %s < %s',
582 'imports from %s not lexically sorted: %s < %s',
582 fullname,
583 fullname,
583 n.name,
584 n.name,
584 lastentryname,
585 lastentryname,
585 )
586 )
586
587
587 lastentryname = n.name
588 lastentryname = n.name
588
589
589 if n.name in requirealias and n.asname != requirealias[n.name]:
590 if n.name in requirealias and n.asname != requirealias[n.name]:
590 yield msg(
591 yield msg(
591 '%s from %s must be "as" aliased to %s',
592 '%s from %s must be "as" aliased to %s',
592 n.name,
593 n.name,
593 fullname,
594 fullname,
594 requirealias[n.name],
595 requirealias[n.name],
595 )
596 )
596
597
597
598
598 class CircularImport(Exception):
599 class CircularImport(Exception):
599 pass
600 pass
600
601
601
602
602 def checkmod(mod, imports):
603 def checkmod(mod, imports):
603 shortest = {}
604 shortest = {}
604 visit = [[mod]]
605 visit = [[mod]]
605 while visit:
606 while visit:
606 path = visit.pop(0)
607 path = visit.pop(0)
607 for i in sorted(imports.get(path[-1], [])):
608 for i in sorted(imports.get(path[-1], [])):
608 if len(path) < shortest.get(i, 1000):
609 if len(path) < shortest.get(i, 1000):
609 shortest[i] = len(path)
610 shortest[i] = len(path)
610 if i in path:
611 if i in path:
611 if i == path[0]:
612 if i == path[0]:
612 raise CircularImport(path)
613 raise CircularImport(path)
613 continue
614 continue
614 visit.append(path + [i])
615 visit.append(path + [i])
615
616
616
617
617 def rotatecycle(cycle):
618 def rotatecycle(cycle):
618 """arrange a cycle so that the lexicographically first module listed first
619 """arrange a cycle so that the lexicographically first module listed first
619
620
620 >>> rotatecycle(['foo', 'bar'])
621 >>> rotatecycle(['foo', 'bar'])
621 ['bar', 'foo', 'bar']
622 ['bar', 'foo', 'bar']
622 """
623 """
623 lowest = min(cycle)
624 lowest = min(cycle)
624 idx = cycle.index(lowest)
625 idx = cycle.index(lowest)
625 return cycle[idx:] + cycle[:idx] + [lowest]
626 return cycle[idx:] + cycle[:idx] + [lowest]
626
627
627
628
628 def find_cycles(imports):
629 def find_cycles(imports):
629 """Find cycles in an already-loaded import graph.
630 """Find cycles in an already-loaded import graph.
630
631
631 All module names recorded in `imports` should be absolute one.
632 All module names recorded in `imports` should be absolute one.
632
633
633 >>> imports = {'top.foo': ['top.bar', 'os.path', 'top.qux'],
634 >>> imports = {'top.foo': ['top.bar', 'os.path', 'top.qux'],
634 ... 'top.bar': ['top.baz', 'sys'],
635 ... 'top.bar': ['top.baz', 'sys'],
635 ... 'top.baz': ['top.foo'],
636 ... 'top.baz': ['top.foo'],
636 ... 'top.qux': ['top.foo']}
637 ... 'top.qux': ['top.foo']}
637 >>> print('\\n'.join(sorted(find_cycles(imports))))
638 >>> print('\\n'.join(sorted(find_cycles(imports))))
638 top.bar -> top.baz -> top.foo -> top.bar
639 top.bar -> top.baz -> top.foo -> top.bar
639 top.foo -> top.qux -> top.foo
640 top.foo -> top.qux -> top.foo
640 """
641 """
641 cycles = set()
642 cycles = set()
642 for mod in sorted(imports.keys()):
643 for mod in sorted(imports.keys()):
643 try:
644 try:
644 checkmod(mod, imports)
645 checkmod(mod, imports)
645 except CircularImport as e:
646 except CircularImport as e:
646 cycle = e.args[0]
647 cycle = e.args[0]
647 cycles.add(" -> ".join(rotatecycle(cycle)))
648 cycles.add(" -> ".join(rotatecycle(cycle)))
648 return cycles
649 return cycles
649
650
650
651
651 def _cycle_sortkey(c):
652 def _cycle_sortkey(c):
652 return len(c), c
653 return len(c), c
653
654
654
655
655 def embedded(f, modname, src):
656 def embedded(f, modname, src):
656 """Extract embedded python code
657 """Extract embedded python code
657
658
658 >>> def _forcestr(thing):
659 >>> def _forcestr(thing):
659 ... if not isinstance(thing, str):
660 ... if not isinstance(thing, str):
660 ... return thing.decode('ascii')
661 ... return thing.decode('ascii')
661 ... return thing
662 ... return thing
662 >>> def test(fn, lines):
663 >>> def test(fn, lines):
663 ... for s, m, f, l in embedded(fn, b"example", lines):
664 ... for s, m, f, l in embedded(fn, b"example", lines):
664 ... print("%s %s %d" % (_forcestr(m), _forcestr(f), l))
665 ... print("%s %s %d" % (_forcestr(m), _forcestr(f), l))
665 ... print(repr(_forcestr(s)))
666 ... print(repr(_forcestr(s)))
666 >>> lines = [
667 >>> lines = [
667 ... 'comment',
668 ... 'comment',
668 ... ' >>> from __future__ import print_function',
669 ... ' >>> from __future__ import print_function',
669 ... " >>> ' multiline",
670 ... " >>> ' multiline",
670 ... " ... string'",
671 ... " ... string'",
671 ... ' ',
672 ... ' ',
672 ... 'comment',
673 ... 'comment',
673 ... ' $ cat > foo.py <<EOF',
674 ... ' $ cat > foo.py <<EOF',
674 ... ' > from __future__ import print_function',
675 ... ' > from __future__ import print_function',
675 ... ' > EOF',
676 ... ' > EOF',
676 ... ]
677 ... ]
677 >>> test(b"example.t", lines)
678 >>> test(b"example.t", lines)
678 example[2] doctest.py 1
679 example[2] doctest.py 1
679 "from __future__ import print_function\\n' multiline\\nstring'\\n\\n"
680 "from __future__ import print_function\\n' multiline\\nstring'\\n\\n"
680 example[8] foo.py 7
681 example[8] foo.py 7
681 'from __future__ import print_function\\n'
682 'from __future__ import print_function\\n'
682 """
683 """
683 errors = []
684 errors = []
684 for name, starts, ends, code in testparseutil.pyembedded(f, src, errors):
685 for name, starts, ends, code in testparseutil.pyembedded(f, src, errors):
685 if not name:
686 if not name:
686 # use 'doctest.py', in order to make already existing
687 # use 'doctest.py', in order to make already existing
687 # doctest above pass instantly
688 # doctest above pass instantly
688 name = 'doctest.py'
689 name = 'doctest.py'
689 # "starts" is "line number" (1-origin), but embedded() is
690 # "starts" is "line number" (1-origin), but embedded() is
690 # expected to return "line offset" (0-origin). Therefore, this
691 # expected to return "line offset" (0-origin). Therefore, this
691 # yields "starts - 1".
692 # yields "starts - 1".
692 if not isinstance(modname, str):
693 if not isinstance(modname, str):
693 modname = modname.decode('utf8')
694 modname = modname.decode('utf8')
694 yield code, "%s[%d]" % (modname, starts), name, starts - 1
695 yield code, "%s[%d]" % (modname, starts), name, starts - 1
695
696
696
697
697 def sources(f, modname):
698 def sources(f, modname):
698 """Yields possibly multiple sources from a filepath
699 """Yields possibly multiple sources from a filepath
699
700
700 input: filepath, modulename
701 input: filepath, modulename
701 yields: script(string), modulename, filepath, linenumber
702 yields: script(string), modulename, filepath, linenumber
702
703
703 For embedded scripts, the modulename and filepath will be different
704 For embedded scripts, the modulename and filepath will be different
704 from the function arguments. linenumber is an offset relative to
705 from the function arguments. linenumber is an offset relative to
705 the input file.
706 the input file.
706 """
707 """
707 py = False
708 py = False
708 if not f.endswith('.t'):
709 if not f.endswith('.t'):
709 with open(f, 'rb') as src:
710 with open(f, 'rb') as src:
710 yield src.read(), modname, f, 0
711 yield src.read(), modname, f, 0
711 py = True
712 py = True
712 if py or f.endswith('.t'):
713 if py or f.endswith('.t'):
713 # Strictly speaking we should sniff for the magic header that denotes
714 # Strictly speaking we should sniff for the magic header that denotes
714 # Python source file encoding. But in reality we don't use anything
715 # Python source file encoding. But in reality we don't use anything
715 # other than ASCII (mainly) and UTF-8 (in a few exceptions), so
716 # other than ASCII (mainly) and UTF-8 (in a few exceptions), so
716 # simplicity is fine.
717 # simplicity is fine.
717 with io.open(f, 'r', encoding='utf-8') as src:
718 with io.open(f, 'r', encoding='utf-8') as src:
718 for script, modname, t, line in embedded(f, modname, src):
719 for script, modname, t, line in embedded(f, modname, src):
719 yield script, modname.encode('utf8'), t, line
720 yield script, modname.encode('utf8'), t, line
720
721
721
722
722 def main(argv):
723 def main(argv):
723 if len(argv) < 2 or (argv[1] == '-' and len(argv) > 2):
724 if len(argv) < 2 or (argv[1] == '-' and len(argv) > 2):
724 print('Usage: %s {-|file [file] [file] ...}')
725 print('Usage: %s {-|file [file] [file] ...}')
725 return 1
726 return 1
726 if argv[1] == '-':
727 if argv[1] == '-':
727 argv = argv[:1]
728 argv = argv[:1]
728 argv.extend(l.rstrip() for l in sys.stdin.readlines())
729 argv.extend(l.rstrip() for l in sys.stdin.readlines())
729 localmodpaths = {}
730 localmodpaths = {}
730 used_imports = {}
731 used_imports = {}
731 any_errors = False
732 any_errors = False
732 for source_path in argv[1:]:
733 for source_path in argv[1:]:
733 modname = dotted_name_of_path(source_path)
734 modname = dotted_name_of_path(source_path)
734 localmodpaths[modname] = source_path
735 localmodpaths[modname] = source_path
735 localmods = populateextmods(localmodpaths)
736 localmods = populateextmods(localmodpaths)
736 for localmodname, source_path in sorted(localmodpaths.items()):
737 for localmodname, source_path in sorted(localmodpaths.items()):
737 if not isinstance(localmodname, bytes):
738 if not isinstance(localmodname, bytes):
738 # This is only safe because all hg's files are ascii
739 # This is only safe because all hg's files are ascii
739 localmodname = localmodname.encode('ascii')
740 localmodname = localmodname.encode('ascii')
740 for src, modname, name, line in sources(source_path, localmodname):
741 for src, modname, name, line in sources(source_path, localmodname):
741 try:
742 try:
742 used_imports[modname] = sorted(
743 used_imports[modname] = sorted(
743 imported_modules(
744 imported_modules(
744 src, modname, name, localmods, ignore_nested=True
745 src, modname, name, localmods, ignore_nested=True
745 )
746 )
746 )
747 )
747 for error, lineno in verify_import_convention(
748 for error, lineno in verify_import_convention(
748 modname, src, localmods
749 modname, src, localmods
749 ):
750 ):
750 any_errors = True
751 any_errors = True
751 print('%s:%d: %s' % (source_path, lineno + line, error))
752 print('%s:%d: %s' % (source_path, lineno + line, error))
752 except SyntaxError as e:
753 except SyntaxError as e:
753 print(
754 print(
754 '%s:%d: SyntaxError: %s' % (source_path, e.lineno + line, e)
755 '%s:%d: SyntaxError: %s' % (source_path, e.lineno + line, e)
755 )
756 )
756 cycles = find_cycles(used_imports)
757 cycles = find_cycles(used_imports)
757 if cycles:
758 if cycles:
758 firstmods = set()
759 firstmods = set()
759 for c in sorted(cycles, key=_cycle_sortkey):
760 for c in sorted(cycles, key=_cycle_sortkey):
760 first = c.split()[0]
761 first = c.split()[0]
761 # As a rough cut, ignore any cycle that starts with the
762 # As a rough cut, ignore any cycle that starts with the
762 # same module as some other cycle. Otherwise we see lots
763 # same module as some other cycle. Otherwise we see lots
763 # of cycles that are effectively duplicates.
764 # of cycles that are effectively duplicates.
764 if first in firstmods:
765 if first in firstmods:
765 continue
766 continue
766 print('Import cycle:', c)
767 print('Import cycle:', c)
767 firstmods.add(first)
768 firstmods.add(first)
768 any_errors = True
769 any_errors = True
769 return any_errors != 0
770 return any_errors != 0
770
771
771
772
772 if __name__ == '__main__':
773 if __name__ == '__main__':
773 sys.exit(int(main(sys.argv)))
774 sys.exit(int(main(sys.argv)))
@@ -1,1807 +1,1808 b''
1 #
1 #
2 # This is the mercurial setup script.
2 # This is the mercurial setup script.
3 #
3 #
4 # 'python setup.py install', or
4 # 'python setup.py install', or
5 # 'python setup.py --help' for more options
5 # 'python setup.py --help' for more options
6 import os
6 import os
7
7
8 # Mercurial can't work on 3.6.0 or 3.6.1 due to a bug in % formatting
8 # Mercurial can't work on 3.6.0 or 3.6.1 due to a bug in % formatting
9 # in bytestrings.
9 # in bytestrings.
10 supportedpy = ','.join(
10 supportedpy = ','.join(
11 [
11 [
12 '>=3.6.2',
12 '>=3.6.2',
13 ]
13 ]
14 )
14 )
15
15
16 import sys, platform
16 import sys, platform
17 import sysconfig
17 import sysconfig
18
18
19
19
20 def sysstr(s):
20 def sysstr(s):
21 return s.decode('latin-1')
21 return s.decode('latin-1')
22
22
23
23
24 def eprint(*args, **kwargs):
24 def eprint(*args, **kwargs):
25 kwargs['file'] = sys.stderr
25 kwargs['file'] = sys.stderr
26 print(*args, **kwargs)
26 print(*args, **kwargs)
27
27
28
28
29 import ssl
29 import ssl
30
30
31 # ssl.HAS_TLSv1* are preferred to check support but they were added in Python
31 # ssl.HAS_TLSv1* are preferred to check support but they were added in Python
32 # 3.7. Prior to CPython commit 6e8cda91d92da72800d891b2fc2073ecbc134d98
32 # 3.7. Prior to CPython commit 6e8cda91d92da72800d891b2fc2073ecbc134d98
33 # (backported to the 3.7 branch), ssl.PROTOCOL_TLSv1_1 / ssl.PROTOCOL_TLSv1_2
33 # (backported to the 3.7 branch), ssl.PROTOCOL_TLSv1_1 / ssl.PROTOCOL_TLSv1_2
34 # were defined only if compiled against a OpenSSL version with TLS 1.1 / 1.2
34 # were defined only if compiled against a OpenSSL version with TLS 1.1 / 1.2
35 # support. At the mentioned commit, they were unconditionally defined.
35 # support. At the mentioned commit, they were unconditionally defined.
36 _notset = object()
36 _notset = object()
37 has_tlsv1_1 = getattr(ssl, 'HAS_TLSv1_1', _notset)
37 has_tlsv1_1 = getattr(ssl, 'HAS_TLSv1_1', _notset)
38 if has_tlsv1_1 is _notset:
38 if has_tlsv1_1 is _notset:
39 has_tlsv1_1 = getattr(ssl, 'PROTOCOL_TLSv1_1', _notset) is not _notset
39 has_tlsv1_1 = getattr(ssl, 'PROTOCOL_TLSv1_1', _notset) is not _notset
40 has_tlsv1_2 = getattr(ssl, 'HAS_TLSv1_2', _notset)
40 has_tlsv1_2 = getattr(ssl, 'HAS_TLSv1_2', _notset)
41 if has_tlsv1_2 is _notset:
41 if has_tlsv1_2 is _notset:
42 has_tlsv1_2 = getattr(ssl, 'PROTOCOL_TLSv1_2', _notset) is not _notset
42 has_tlsv1_2 = getattr(ssl, 'PROTOCOL_TLSv1_2', _notset) is not _notset
43 if not (has_tlsv1_1 or has_tlsv1_2):
43 if not (has_tlsv1_1 or has_tlsv1_2):
44 error = """
44 error = """
45 The `ssl` module does not advertise support for TLS 1.1 or TLS 1.2.
45 The `ssl` module does not advertise support for TLS 1.1 or TLS 1.2.
46 Please make sure that your Python installation was compiled against an OpenSSL
46 Please make sure that your Python installation was compiled against an OpenSSL
47 version enabling these features (likely this requires the OpenSSL version to
47 version enabling these features (likely this requires the OpenSSL version to
48 be at least 1.0.1).
48 be at least 1.0.1).
49 """
49 """
50 print(error, file=sys.stderr)
50 print(error, file=sys.stderr)
51 sys.exit(1)
51 sys.exit(1)
52
52
53 DYLIB_SUFFIX = sysconfig.get_config_vars()['EXT_SUFFIX']
53 DYLIB_SUFFIX = sysconfig.get_config_vars()['EXT_SUFFIX']
54
54
55 # Solaris Python packaging brain damage
55 # Solaris Python packaging brain damage
56 try:
56 try:
57 import hashlib
57 import hashlib
58
58
59 sha = hashlib.sha1()
59 sha = hashlib.sha1()
60 except ImportError:
60 except ImportError:
61 try:
61 try:
62 import sha
62 import sha
63
63
64 sha.sha # silence unused import warning
64 sha.sha # silence unused import warning
65 except ImportError:
65 except ImportError:
66 raise SystemExit(
66 raise SystemExit(
67 "Couldn't import standard hashlib (incomplete Python install)."
67 "Couldn't import standard hashlib (incomplete Python install)."
68 )
68 )
69
69
70 try:
70 try:
71 import zlib
71 import zlib
72
72
73 zlib.compressobj # silence unused import warning
73 zlib.compressobj # silence unused import warning
74 except ImportError:
74 except ImportError:
75 raise SystemExit(
75 raise SystemExit(
76 "Couldn't import standard zlib (incomplete Python install)."
76 "Couldn't import standard zlib (incomplete Python install)."
77 )
77 )
78
78
79 # The base IronPython distribution (as of 2.7.1) doesn't support bz2
79 # The base IronPython distribution (as of 2.7.1) doesn't support bz2
80 isironpython = False
80 isironpython = False
81 try:
81 try:
82 isironpython = (
82 isironpython = (
83 platform.python_implementation().lower().find("ironpython") != -1
83 platform.python_implementation().lower().find("ironpython") != -1
84 )
84 )
85 except AttributeError:
85 except AttributeError:
86 pass
86 pass
87
87
88 if isironpython:
88 if isironpython:
89 sys.stderr.write("warning: IronPython detected (no bz2 support)\n")
89 sys.stderr.write("warning: IronPython detected (no bz2 support)\n")
90 else:
90 else:
91 try:
91 try:
92 import bz2
92 import bz2
93
93
94 bz2.BZ2Compressor # silence unused import warning
94 bz2.BZ2Compressor # silence unused import warning
95 except ImportError:
95 except ImportError:
96 raise SystemExit(
96 raise SystemExit(
97 "Couldn't import standard bz2 (incomplete Python install)."
97 "Couldn't import standard bz2 (incomplete Python install)."
98 )
98 )
99
99
100 ispypy = "PyPy" in sys.version
100 ispypy = "PyPy" in sys.version
101
101
102 import ctypes
102 import ctypes
103 import stat, subprocess, time
103 import stat, subprocess, time
104 import re
104 import re
105 import shutil
105 import shutil
106 import tempfile
106 import tempfile
107
107
108 # We have issues with setuptools on some platforms and builders. Until
108 # We have issues with setuptools on some platforms and builders. Until
109 # those are resolved, setuptools is opt-in except for platforms where
109 # those are resolved, setuptools is opt-in except for platforms where
110 # we don't have issues.
110 # we don't have issues.
111 issetuptools = os.name == 'nt' or 'FORCE_SETUPTOOLS' in os.environ
111 issetuptools = os.name == 'nt' or 'FORCE_SETUPTOOLS' in os.environ
112 if issetuptools:
112 if issetuptools:
113 from setuptools import setup
113 from setuptools import setup
114 else:
114 else:
115 try:
115 try:
116 from distutils.core import setup
116 from distutils.core import setup
117 except ModuleNotFoundError:
117 except ModuleNotFoundError:
118 from setuptools import setup
118 from setuptools import setup
119 from distutils.ccompiler import new_compiler
119 from distutils.ccompiler import new_compiler
120 from distutils.core import Command, Extension
120 from distutils.core import Command, Extension
121 from distutils.dist import Distribution
121 from distutils.dist import Distribution
122 from distutils.command.build import build
122 from distutils.command.build import build
123 from distutils.command.build_ext import build_ext
123 from distutils.command.build_ext import build_ext
124 from distutils.command.build_py import build_py
124 from distutils.command.build_py import build_py
125 from distutils.command.build_scripts import build_scripts
125 from distutils.command.build_scripts import build_scripts
126 from distutils.command.install import install
126 from distutils.command.install import install
127 from distutils.command.install_lib import install_lib
127 from distutils.command.install_lib import install_lib
128 from distutils.command.install_scripts import install_scripts
128 from distutils.command.install_scripts import install_scripts
129 from distutils import log
129 from distutils import log
130 from distutils.spawn import spawn, find_executable
130 from distutils.spawn import spawn, find_executable
131 from distutils import file_util
131 from distutils import file_util
132 from distutils.errors import (
132 from distutils.errors import (
133 CCompilerError,
133 CCompilerError,
134 DistutilsError,
134 DistutilsError,
135 DistutilsExecError,
135 DistutilsExecError,
136 )
136 )
137 from distutils.sysconfig import get_python_inc
137 from distutils.sysconfig import get_python_inc
138
138
139
139
140 def write_if_changed(path, content):
140 def write_if_changed(path, content):
141 """Write content to a file iff the content hasn't changed."""
141 """Write content to a file iff the content hasn't changed."""
142 if os.path.exists(path):
142 if os.path.exists(path):
143 with open(path, 'rb') as fh:
143 with open(path, 'rb') as fh:
144 current = fh.read()
144 current = fh.read()
145 else:
145 else:
146 current = b''
146 current = b''
147
147
148 if current != content:
148 if current != content:
149 with open(path, 'wb') as fh:
149 with open(path, 'wb') as fh:
150 fh.write(content)
150 fh.write(content)
151
151
152
152
153 scripts = ['hg']
153 scripts = ['hg']
154 if os.name == 'nt':
154 if os.name == 'nt':
155 # We remove hg.bat if we are able to build hg.exe.
155 # We remove hg.bat if we are able to build hg.exe.
156 scripts.append('contrib/win32/hg.bat')
156 scripts.append('contrib/win32/hg.bat')
157
157
158
158
159 def cancompile(cc, code):
159 def cancompile(cc, code):
160 tmpdir = tempfile.mkdtemp(prefix='hg-install-')
160 tmpdir = tempfile.mkdtemp(prefix='hg-install-')
161 devnull = oldstderr = None
161 devnull = oldstderr = None
162 try:
162 try:
163 fname = os.path.join(tmpdir, 'testcomp.c')
163 fname = os.path.join(tmpdir, 'testcomp.c')
164 f = open(fname, 'w')
164 f = open(fname, 'w')
165 f.write(code)
165 f.write(code)
166 f.close()
166 f.close()
167 # Redirect stderr to /dev/null to hide any error messages
167 # Redirect stderr to /dev/null to hide any error messages
168 # from the compiler.
168 # from the compiler.
169 # This will have to be changed if we ever have to check
169 # This will have to be changed if we ever have to check
170 # for a function on Windows.
170 # for a function on Windows.
171 devnull = open('/dev/null', 'w')
171 devnull = open('/dev/null', 'w')
172 oldstderr = os.dup(sys.stderr.fileno())
172 oldstderr = os.dup(sys.stderr.fileno())
173 os.dup2(devnull.fileno(), sys.stderr.fileno())
173 os.dup2(devnull.fileno(), sys.stderr.fileno())
174 objects = cc.compile([fname], output_dir=tmpdir)
174 objects = cc.compile([fname], output_dir=tmpdir)
175 cc.link_executable(objects, os.path.join(tmpdir, "a.out"))
175 cc.link_executable(objects, os.path.join(tmpdir, "a.out"))
176 return True
176 return True
177 except Exception:
177 except Exception:
178 return False
178 return False
179 finally:
179 finally:
180 if oldstderr is not None:
180 if oldstderr is not None:
181 os.dup2(oldstderr, sys.stderr.fileno())
181 os.dup2(oldstderr, sys.stderr.fileno())
182 if devnull is not None:
182 if devnull is not None:
183 devnull.close()
183 devnull.close()
184 shutil.rmtree(tmpdir)
184 shutil.rmtree(tmpdir)
185
185
186
186
187 # simplified version of distutils.ccompiler.CCompiler.has_function
187 # simplified version of distutils.ccompiler.CCompiler.has_function
188 # that actually removes its temporary files.
188 # that actually removes its temporary files.
189 def hasfunction(cc, funcname):
189 def hasfunction(cc, funcname):
190 code = 'int main(void) { %s(); }\n' % funcname
190 code = 'int main(void) { %s(); }\n' % funcname
191 return cancompile(cc, code)
191 return cancompile(cc, code)
192
192
193
193
194 def hasheader(cc, headername):
194 def hasheader(cc, headername):
195 code = '#include <%s>\nint main(void) { return 0; }\n' % headername
195 code = '#include <%s>\nint main(void) { return 0; }\n' % headername
196 return cancompile(cc, code)
196 return cancompile(cc, code)
197
197
198
198
199 # py2exe needs to be installed to work
199 # py2exe needs to be installed to work
200 try:
200 try:
201 import py2exe
201 import py2exe
202
202
203 py2exe.patch_distutils()
203 py2exe.patch_distutils()
204 py2exeloaded = True
204 py2exeloaded = True
205 # import py2exe's patched Distribution class
205 # import py2exe's patched Distribution class
206 from distutils.core import Distribution
206 from distutils.core import Distribution
207 except ImportError:
207 except ImportError:
208 py2exeloaded = False
208 py2exeloaded = False
209
209
210
210
211 def runcmd(cmd, env, cwd=None):
211 def runcmd(cmd, env, cwd=None):
212 p = subprocess.Popen(
212 p = subprocess.Popen(
213 cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, cwd=cwd
213 cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, cwd=cwd
214 )
214 )
215 out, err = p.communicate()
215 out, err = p.communicate()
216 return p.returncode, out, err
216 return p.returncode, out, err
217
217
218
218
219 class hgcommand:
219 class hgcommand:
220 def __init__(self, cmd, env):
220 def __init__(self, cmd, env):
221 self.cmd = cmd
221 self.cmd = cmd
222 self.env = env
222 self.env = env
223
223
224 def run(self, args):
224 def run(self, args):
225 cmd = self.cmd + args
225 cmd = self.cmd + args
226 returncode, out, err = runcmd(cmd, self.env)
226 returncode, out, err = runcmd(cmd, self.env)
227 err = filterhgerr(err)
227 err = filterhgerr(err)
228 if err:
228 if err:
229 print("stderr from '%s':" % (' '.join(cmd)), file=sys.stderr)
229 print("stderr from '%s':" % (' '.join(cmd)), file=sys.stderr)
230 print(err, file=sys.stderr)
230 print(err, file=sys.stderr)
231 if returncode != 0:
231 if returncode != 0:
232 return b''
232 return b''
233 return out
233 return out
234
234
235
235
236 def filterhgerr(err):
236 def filterhgerr(err):
237 # If root is executing setup.py, but the repository is owned by
237 # If root is executing setup.py, but the repository is owned by
238 # another user (as in "sudo python setup.py install") we will get
238 # another user (as in "sudo python setup.py install") we will get
239 # trust warnings since the .hg/hgrc file is untrusted. That is
239 # trust warnings since the .hg/hgrc file is untrusted. That is
240 # fine, we don't want to load it anyway. Python may warn about
240 # fine, we don't want to load it anyway. Python may warn about
241 # a missing __init__.py in mercurial/locale, we also ignore that.
241 # a missing __init__.py in mercurial/locale, we also ignore that.
242 err = [
242 err = [
243 e
243 e
244 for e in err.splitlines()
244 for e in err.splitlines()
245 if (
245 if (
246 not e.startswith(b'not trusting file')
246 not e.startswith(b'not trusting file')
247 and not e.startswith(b'warning: Not importing')
247 and not e.startswith(b'warning: Not importing')
248 and not e.startswith(b'obsolete feature not enabled')
248 and not e.startswith(b'obsolete feature not enabled')
249 and not e.startswith(b'*** failed to import extension')
249 and not e.startswith(b'*** failed to import extension')
250 and not e.startswith(b'devel-warn:')
250 and not e.startswith(b'devel-warn:')
251 and not (
251 and not (
252 e.startswith(b'(third party extension')
252 e.startswith(b'(third party extension')
253 and e.endswith(b'or newer of Mercurial; disabling)')
253 and e.endswith(b'or newer of Mercurial; disabling)')
254 )
254 )
255 )
255 )
256 ]
256 ]
257 return b'\n'.join(b' ' + e for e in err)
257 return b'\n'.join(b' ' + e for e in err)
258
258
259
259
260 def findhg():
260 def findhg():
261 """Try to figure out how we should invoke hg for examining the local
261 """Try to figure out how we should invoke hg for examining the local
262 repository contents.
262 repository contents.
263
263
264 Returns an hgcommand object."""
264 Returns an hgcommand object."""
265 # By default, prefer the "hg" command in the user's path. This was
265 # By default, prefer the "hg" command in the user's path. This was
266 # presumably the hg command that the user used to create this repository.
266 # presumably the hg command that the user used to create this repository.
267 #
267 #
268 # This repository may require extensions or other settings that would not
268 # This repository may require extensions or other settings that would not
269 # be enabled by running the hg script directly from this local repository.
269 # be enabled by running the hg script directly from this local repository.
270 hgenv = os.environ.copy()
270 hgenv = os.environ.copy()
271 # Use HGPLAIN to disable hgrc settings that would change output formatting,
271 # Use HGPLAIN to disable hgrc settings that would change output formatting,
272 # and disable localization for the same reasons.
272 # and disable localization for the same reasons.
273 hgenv['HGPLAIN'] = '1'
273 hgenv['HGPLAIN'] = '1'
274 hgenv['LANGUAGE'] = 'C'
274 hgenv['LANGUAGE'] = 'C'
275 hgcmd = ['hg']
275 hgcmd = ['hg']
276 # Run a simple "hg log" command just to see if using hg from the user's
276 # Run a simple "hg log" command just to see if using hg from the user's
277 # path works and can successfully interact with this repository. Windows
277 # path works and can successfully interact with this repository. Windows
278 # gives precedence to hg.exe in the current directory, so fall back to the
278 # gives precedence to hg.exe in the current directory, so fall back to the
279 # python invocation of local hg, where pythonXY.dll can always be found.
279 # python invocation of local hg, where pythonXY.dll can always be found.
280 check_cmd = ['log', '-r.', '-Ttest']
280 check_cmd = ['log', '-r.', '-Ttest']
281 if os.name != 'nt' or not os.path.exists("hg.exe"):
281 if os.name != 'nt' or not os.path.exists("hg.exe"):
282 try:
282 try:
283 retcode, out, err = runcmd(hgcmd + check_cmd, hgenv)
283 retcode, out, err = runcmd(hgcmd + check_cmd, hgenv)
284 except EnvironmentError:
284 except EnvironmentError:
285 retcode = -1
285 retcode = -1
286 if retcode == 0 and not filterhgerr(err):
286 if retcode == 0 and not filterhgerr(err):
287 return hgcommand(hgcmd, hgenv)
287 return hgcommand(hgcmd, hgenv)
288
288
289 # Fall back to trying the local hg installation.
289 # Fall back to trying the local hg installation.
290 hgenv = localhgenv()
290 hgenv = localhgenv()
291 hgcmd = [sys.executable, 'hg']
291 hgcmd = [sys.executable, 'hg']
292 try:
292 try:
293 retcode, out, err = runcmd(hgcmd + check_cmd, hgenv)
293 retcode, out, err = runcmd(hgcmd + check_cmd, hgenv)
294 except EnvironmentError:
294 except EnvironmentError:
295 retcode = -1
295 retcode = -1
296 if retcode == 0 and not filterhgerr(err):
296 if retcode == 0 and not filterhgerr(err):
297 return hgcommand(hgcmd, hgenv)
297 return hgcommand(hgcmd, hgenv)
298
298
299 eprint("/!\\")
299 eprint("/!\\")
300 eprint(r"/!\ Unable to find a working hg binary")
300 eprint(r"/!\ Unable to find a working hg binary")
301 eprint(r"/!\ Version cannot be extract from the repository")
301 eprint(r"/!\ Version cannot be extract from the repository")
302 eprint(r"/!\ Re-run the setup once a first version is built")
302 eprint(r"/!\ Re-run the setup once a first version is built")
303 return None
303 return None
304
304
305
305
306 def localhgenv():
306 def localhgenv():
307 """Get an environment dictionary to use for invoking or importing
307 """Get an environment dictionary to use for invoking or importing
308 mercurial from the local repository."""
308 mercurial from the local repository."""
309 # Execute hg out of this directory with a custom environment which takes
309 # Execute hg out of this directory with a custom environment which takes
310 # care to not use any hgrc files and do no localization.
310 # care to not use any hgrc files and do no localization.
311 env = {
311 env = {
312 'HGMODULEPOLICY': 'py',
312 'HGMODULEPOLICY': 'py',
313 'HGRCPATH': '',
313 'HGRCPATH': '',
314 'LANGUAGE': 'C',
314 'LANGUAGE': 'C',
315 'PATH': '',
315 'PATH': '',
316 } # make pypi modules that use os.environ['PATH'] happy
316 } # make pypi modules that use os.environ['PATH'] happy
317 if 'LD_LIBRARY_PATH' in os.environ:
317 if 'LD_LIBRARY_PATH' in os.environ:
318 env['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH']
318 env['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH']
319 if 'SystemRoot' in os.environ:
319 if 'SystemRoot' in os.environ:
320 # SystemRoot is required by Windows to load various DLLs. See:
320 # SystemRoot is required by Windows to load various DLLs. See:
321 # https://bugs.python.org/issue13524#msg148850
321 # https://bugs.python.org/issue13524#msg148850
322 env['SystemRoot'] = os.environ['SystemRoot']
322 env['SystemRoot'] = os.environ['SystemRoot']
323 return env
323 return env
324
324
325
325
326 version = ''
326 version = ''
327
327
328
328
329 def _try_get_version():
329 def _try_get_version():
330 hg = findhg()
330 hg = findhg()
331 if hg is None:
331 if hg is None:
332 return ''
332 return ''
333 hgid = None
333 hgid = None
334 numerictags = []
334 numerictags = []
335 cmd = ['log', '-r', '.', '--template', '{tags}\n']
335 cmd = ['log', '-r', '.', '--template', '{tags}\n']
336 pieces = sysstr(hg.run(cmd)).split()
336 pieces = sysstr(hg.run(cmd)).split()
337 numerictags = [t for t in pieces if t[0:1].isdigit()]
337 numerictags = [t for t in pieces if t[0:1].isdigit()]
338 hgid = sysstr(hg.run(['id', '-i'])).strip()
338 hgid = sysstr(hg.run(['id', '-i'])).strip()
339 if hgid.count('+') == 2:
339 if hgid.count('+') == 2:
340 hgid = hgid.replace("+", ".", 1)
340 hgid = hgid.replace("+", ".", 1)
341 if not hgid:
341 if not hgid:
342 eprint("/!\\")
342 eprint("/!\\")
343 eprint(r"/!\ Unable to determine hg version from local repository")
343 eprint(r"/!\ Unable to determine hg version from local repository")
344 eprint(r"/!\ Failed to retrieve current revision tags")
344 eprint(r"/!\ Failed to retrieve current revision tags")
345 return ''
345 return ''
346 if numerictags: # tag(s) found
346 if numerictags: # tag(s) found
347 version = numerictags[-1]
347 version = numerictags[-1]
348 if hgid.endswith('+'): # propagate the dirty status to the tag
348 if hgid.endswith('+'): # propagate the dirty status to the tag
349 version += '+'
349 version += '+'
350 else: # no tag found on the checked out revision
350 else: # no tag found on the checked out revision
351 ltagcmd = ['log', '--rev', 'wdir()', '--template', '{latesttag}']
351 ltagcmd = ['log', '--rev', 'wdir()', '--template', '{latesttag}']
352 ltag = sysstr(hg.run(ltagcmd))
352 ltag = sysstr(hg.run(ltagcmd))
353 if not ltag:
353 if not ltag:
354 eprint("/!\\")
354 eprint("/!\\")
355 eprint(r"/!\ Unable to determine hg version from local repository")
355 eprint(r"/!\ Unable to determine hg version from local repository")
356 eprint(
356 eprint(
357 r"/!\ Failed to retrieve current revision distance to lated tag"
357 r"/!\ Failed to retrieve current revision distance to lated tag"
358 )
358 )
359 return ''
359 return ''
360 changessincecmd = [
360 changessincecmd = [
361 'log',
361 'log',
362 '-T',
362 '-T',
363 'x\n',
363 'x\n',
364 '-r',
364 '-r',
365 "only(parents(),'%s')" % ltag,
365 "only(parents(),'%s')" % ltag,
366 ]
366 ]
367 changessince = len(hg.run(changessincecmd).splitlines())
367 changessince = len(hg.run(changessincecmd).splitlines())
368 version = '%s+hg%s.%s' % (ltag, changessince, hgid)
368 version = '%s+hg%s.%s' % (ltag, changessince, hgid)
369 if version.endswith('+'):
369 if version.endswith('+'):
370 version = version[:-1] + 'local' + time.strftime('%Y%m%d')
370 version = version[:-1] + 'local' + time.strftime('%Y%m%d')
371 return version
371 return version
372
372
373
373
374 if os.path.isdir('.hg'):
374 if os.path.isdir('.hg'):
375 version = _try_get_version()
375 version = _try_get_version()
376 elif os.path.exists('.hg_archival.txt'):
376 elif os.path.exists('.hg_archival.txt'):
377 kw = dict(
377 kw = dict(
378 [[t.strip() for t in l.split(':', 1)] for l in open('.hg_archival.txt')]
378 [[t.strip() for t in l.split(':', 1)] for l in open('.hg_archival.txt')]
379 )
379 )
380 if 'tag' in kw:
380 if 'tag' in kw:
381 version = kw['tag']
381 version = kw['tag']
382 elif 'latesttag' in kw:
382 elif 'latesttag' in kw:
383 if 'changessincelatesttag' in kw:
383 if 'changessincelatesttag' in kw:
384 version = (
384 version = (
385 '%(latesttag)s+hg%(changessincelatesttag)s.%(node).12s' % kw
385 '%(latesttag)s+hg%(changessincelatesttag)s.%(node).12s' % kw
386 )
386 )
387 else:
387 else:
388 version = '%(latesttag)s+hg%(latesttagdistance)s.%(node).12s' % kw
388 version = '%(latesttag)s+hg%(latesttagdistance)s.%(node).12s' % kw
389 else:
389 else:
390 version = '0+hg' + kw.get('node', '')[:12]
390 version = '0+hg' + kw.get('node', '')[:12]
391 elif os.path.exists('mercurial/__version__.py'):
391 elif os.path.exists('mercurial/__version__.py'):
392 with open('mercurial/__version__.py') as f:
392 with open('mercurial/__version__.py') as f:
393 data = f.read()
393 data = f.read()
394 version = re.search('version = b"(.*)"', data).group(1)
394 version = re.search('version = b"(.*)"', data).group(1)
395 if not version:
395 if not version:
396 if os.environ.get("MERCURIAL_SETUP_MAKE_LOCAL") == "1":
396 if os.environ.get("MERCURIAL_SETUP_MAKE_LOCAL") == "1":
397 version = "0.0+0"
397 version = "0.0+0"
398 eprint("/!\\")
398 eprint("/!\\")
399 eprint(r"/!\ Using '0.0+0' as the default version")
399 eprint(r"/!\ Using '0.0+0' as the default version")
400 eprint(r"/!\ Re-run make local once that first version is built")
400 eprint(r"/!\ Re-run make local once that first version is built")
401 eprint("/!\\")
401 eprint("/!\\")
402 else:
402 else:
403 eprint("/!\\")
403 eprint("/!\\")
404 eprint(r"/!\ Could not determine the Mercurial version")
404 eprint(r"/!\ Could not determine the Mercurial version")
405 eprint(r"/!\ You need to build a local version first")
405 eprint(r"/!\ You need to build a local version first")
406 eprint(r"/!\ Run `make local` and try again")
406 eprint(r"/!\ Run `make local` and try again")
407 eprint("/!\\")
407 eprint("/!\\")
408 msg = "Run `make local` first to get a working local version"
408 msg = "Run `make local` first to get a working local version"
409 raise SystemExit(msg)
409 raise SystemExit(msg)
410
410
411 versionb = version
411 versionb = version
412 if not isinstance(versionb, bytes):
412 if not isinstance(versionb, bytes):
413 versionb = versionb.encode('ascii')
413 versionb = versionb.encode('ascii')
414
414
415 write_if_changed(
415 write_if_changed(
416 'mercurial/__version__.py',
416 'mercurial/__version__.py',
417 b''.join(
417 b''.join(
418 [
418 [
419 b'# this file is autogenerated by setup.py\n'
419 b'# this file is autogenerated by setup.py\n'
420 b'version = b"%s"\n' % versionb,
420 b'version = b"%s"\n' % versionb,
421 ]
421 ]
422 ),
422 ),
423 )
423 )
424
424
425
425
426 class hgbuild(build):
426 class hgbuild(build):
427 # Insert hgbuildmo first so that files in mercurial/locale/ are found
427 # Insert hgbuildmo first so that files in mercurial/locale/ are found
428 # when build_py is run next.
428 # when build_py is run next.
429 sub_commands = [('build_mo', None)] + build.sub_commands
429 sub_commands = [('build_mo', None)] + build.sub_commands
430
430
431
431
432 class hgbuildmo(build):
432 class hgbuildmo(build):
433
433
434 description = "build translations (.mo files)"
434 description = "build translations (.mo files)"
435
435
436 def run(self):
436 def run(self):
437 if not find_executable('msgfmt'):
437 if not find_executable('msgfmt'):
438 self.warn(
438 self.warn(
439 "could not find msgfmt executable, no translations "
439 "could not find msgfmt executable, no translations "
440 "will be built"
440 "will be built"
441 )
441 )
442 return
442 return
443
443
444 podir = 'i18n'
444 podir = 'i18n'
445 if not os.path.isdir(podir):
445 if not os.path.isdir(podir):
446 self.warn("could not find %s/ directory" % podir)
446 self.warn("could not find %s/ directory" % podir)
447 return
447 return
448
448
449 join = os.path.join
449 join = os.path.join
450 for po in os.listdir(podir):
450 for po in os.listdir(podir):
451 if not po.endswith('.po'):
451 if not po.endswith('.po'):
452 continue
452 continue
453 pofile = join(podir, po)
453 pofile = join(podir, po)
454 modir = join('locale', po[:-3], 'LC_MESSAGES')
454 modir = join('locale', po[:-3], 'LC_MESSAGES')
455 mofile = join(modir, 'hg.mo')
455 mofile = join(modir, 'hg.mo')
456 mobuildfile = join('mercurial', mofile)
456 mobuildfile = join('mercurial', mofile)
457 cmd = ['msgfmt', '-v', '-o', mobuildfile, pofile]
457 cmd = ['msgfmt', '-v', '-o', mobuildfile, pofile]
458 if sys.platform != 'sunos5':
458 if sys.platform != 'sunos5':
459 # msgfmt on Solaris does not know about -c
459 # msgfmt on Solaris does not know about -c
460 cmd.append('-c')
460 cmd.append('-c')
461 self.mkpath(join('mercurial', modir))
461 self.mkpath(join('mercurial', modir))
462 self.make_file([pofile], mobuildfile, spawn, (cmd,))
462 self.make_file([pofile], mobuildfile, spawn, (cmd,))
463
463
464
464
465 class hgdist(Distribution):
465 class hgdist(Distribution):
466 pure = False
466 pure = False
467 rust = False
467 rust = False
468 no_rust = False
468 no_rust = False
469 cffi = ispypy
469 cffi = ispypy
470
470
471 global_options = Distribution.global_options + [
471 global_options = Distribution.global_options + [
472 ('pure', None, "use pure (slow) Python code instead of C extensions"),
472 ('pure', None, "use pure (slow) Python code instead of C extensions"),
473 ('rust', None, "use Rust extensions additionally to C extensions"),
473 ('rust', None, "use Rust extensions additionally to C extensions"),
474 (
474 (
475 'no-rust',
475 'no-rust',
476 None,
476 None,
477 "do not use Rust extensions additionally to C extensions",
477 "do not use Rust extensions additionally to C extensions",
478 ),
478 ),
479 ]
479 ]
480
480
481 negative_opt = Distribution.negative_opt.copy()
481 negative_opt = Distribution.negative_opt.copy()
482 boolean_options = ['pure', 'rust', 'no-rust']
482 boolean_options = ['pure', 'rust', 'no-rust']
483 negative_opt['no-rust'] = 'rust'
483 negative_opt['no-rust'] = 'rust'
484
484
485 def _set_command_options(self, command_obj, option_dict=None):
485 def _set_command_options(self, command_obj, option_dict=None):
486 # Not all distutils versions in the wild have boolean_options.
486 # Not all distutils versions in the wild have boolean_options.
487 # This should be cleaned up when we're Python 3 only.
487 # This should be cleaned up when we're Python 3 only.
488 command_obj.boolean_options = (
488 command_obj.boolean_options = (
489 getattr(command_obj, 'boolean_options', []) + self.boolean_options
489 getattr(command_obj, 'boolean_options', []) + self.boolean_options
490 )
490 )
491 return Distribution._set_command_options(
491 return Distribution._set_command_options(
492 self, command_obj, option_dict=option_dict
492 self, command_obj, option_dict=option_dict
493 )
493 )
494
494
495 def parse_command_line(self):
495 def parse_command_line(self):
496 ret = Distribution.parse_command_line(self)
496 ret = Distribution.parse_command_line(self)
497 if not (self.rust or self.no_rust):
497 if not (self.rust or self.no_rust):
498 hgrustext = os.environ.get('HGWITHRUSTEXT')
498 hgrustext = os.environ.get('HGWITHRUSTEXT')
499 # TODO record it for proper rebuild upon changes
499 # TODO record it for proper rebuild upon changes
500 # (see mercurial/__modulepolicy__.py)
500 # (see mercurial/__modulepolicy__.py)
501 if hgrustext != 'cpython' and hgrustext is not None:
501 if hgrustext != 'cpython' and hgrustext is not None:
502 if hgrustext:
502 if hgrustext:
503 msg = 'unknown HGWITHRUSTEXT value: %s' % hgrustext
503 msg = 'unknown HGWITHRUSTEXT value: %s' % hgrustext
504 print(msg, file=sys.stderr)
504 print(msg, file=sys.stderr)
505 hgrustext = None
505 hgrustext = None
506 self.rust = hgrustext is not None
506 self.rust = hgrustext is not None
507 self.no_rust = not self.rust
507 self.no_rust = not self.rust
508 return ret
508 return ret
509
509
510 def has_ext_modules(self):
510 def has_ext_modules(self):
511 # self.ext_modules is emptied in hgbuildpy.finalize_options which is
511 # self.ext_modules is emptied in hgbuildpy.finalize_options which is
512 # too late for some cases
512 # too late for some cases
513 return not self.pure and Distribution.has_ext_modules(self)
513 return not self.pure and Distribution.has_ext_modules(self)
514
514
515
515
516 # This is ugly as a one-liner. So use a variable.
516 # This is ugly as a one-liner. So use a variable.
517 buildextnegops = dict(getattr(build_ext, 'negative_options', {}))
517 buildextnegops = dict(getattr(build_ext, 'negative_options', {}))
518 buildextnegops['no-zstd'] = 'zstd'
518 buildextnegops['no-zstd'] = 'zstd'
519 buildextnegops['no-rust'] = 'rust'
519 buildextnegops['no-rust'] = 'rust'
520
520
521
521
522 class hgbuildext(build_ext):
522 class hgbuildext(build_ext):
523 user_options = build_ext.user_options + [
523 user_options = build_ext.user_options + [
524 ('zstd', None, 'compile zstd bindings [default]'),
524 ('zstd', None, 'compile zstd bindings [default]'),
525 ('no-zstd', None, 'do not compile zstd bindings'),
525 ('no-zstd', None, 'do not compile zstd bindings'),
526 (
526 (
527 'rust',
527 'rust',
528 None,
528 None,
529 'compile Rust extensions if they are in use '
529 'compile Rust extensions if they are in use '
530 '(requires Cargo) [default]',
530 '(requires Cargo) [default]',
531 ),
531 ),
532 ('no-rust', None, 'do not compile Rust extensions'),
532 ('no-rust', None, 'do not compile Rust extensions'),
533 ]
533 ]
534
534
535 boolean_options = build_ext.boolean_options + ['zstd', 'rust']
535 boolean_options = build_ext.boolean_options + ['zstd', 'rust']
536 negative_opt = buildextnegops
536 negative_opt = buildextnegops
537
537
538 def initialize_options(self):
538 def initialize_options(self):
539 self.zstd = True
539 self.zstd = True
540 self.rust = True
540 self.rust = True
541
541
542 return build_ext.initialize_options(self)
542 return build_ext.initialize_options(self)
543
543
544 def finalize_options(self):
544 def finalize_options(self):
545 # Unless overridden by the end user, build extensions in parallel.
545 # Unless overridden by the end user, build extensions in parallel.
546 # Only influences behavior on Python 3.5+.
546 # Only influences behavior on Python 3.5+.
547 if getattr(self, 'parallel', None) is None:
547 if getattr(self, 'parallel', None) is None:
548 self.parallel = True
548 self.parallel = True
549
549
550 return build_ext.finalize_options(self)
550 return build_ext.finalize_options(self)
551
551
552 def build_extensions(self):
552 def build_extensions(self):
553 ruststandalones = [
553 ruststandalones = [
554 e for e in self.extensions if isinstance(e, RustStandaloneExtension)
554 e for e in self.extensions if isinstance(e, RustStandaloneExtension)
555 ]
555 ]
556 self.extensions = [
556 self.extensions = [
557 e for e in self.extensions if e not in ruststandalones
557 e for e in self.extensions if e not in ruststandalones
558 ]
558 ]
559 # Filter out zstd if disabled via argument.
559 # Filter out zstd if disabled via argument.
560 if not self.zstd:
560 if not self.zstd:
561 self.extensions = [
561 self.extensions = [
562 e for e in self.extensions if e.name != 'mercurial.zstd'
562 e for e in self.extensions if e.name != 'mercurial.zstd'
563 ]
563 ]
564
564
565 # Build Rust standalone extensions if it'll be used
565 # Build Rust standalone extensions if it'll be used
566 # and its build is not explicitly disabled (for external build
566 # and its build is not explicitly disabled (for external build
567 # as Linux distributions would do)
567 # as Linux distributions would do)
568 if self.distribution.rust and self.rust:
568 if self.distribution.rust and self.rust:
569 if not sys.platform.startswith('linux'):
569 if not sys.platform.startswith('linux'):
570 self.warn(
570 self.warn(
571 "rust extensions have only been tested on Linux "
571 "rust extensions have only been tested on Linux "
572 "and may not behave correctly on other platforms"
572 "and may not behave correctly on other platforms"
573 )
573 )
574
574
575 for rustext in ruststandalones:
575 for rustext in ruststandalones:
576 rustext.build('' if self.inplace else self.build_lib)
576 rustext.build('' if self.inplace else self.build_lib)
577
577
578 return build_ext.build_extensions(self)
578 return build_ext.build_extensions(self)
579
579
580 def build_extension(self, ext):
580 def build_extension(self, ext):
581 if (
581 if (
582 self.distribution.rust
582 self.distribution.rust
583 and self.rust
583 and self.rust
584 and isinstance(ext, RustExtension)
584 and isinstance(ext, RustExtension)
585 ):
585 ):
586 ext.rustbuild()
586 ext.rustbuild()
587 try:
587 try:
588 build_ext.build_extension(self, ext)
588 build_ext.build_extension(self, ext)
589 except CCompilerError:
589 except CCompilerError:
590 if not getattr(ext, 'optional', False):
590 if not getattr(ext, 'optional', False):
591 raise
591 raise
592 log.warn(
592 log.warn(
593 "Failed to build optional extension '%s' (skipping)", ext.name
593 "Failed to build optional extension '%s' (skipping)", ext.name
594 )
594 )
595
595
596
596
597 class hgbuildscripts(build_scripts):
597 class hgbuildscripts(build_scripts):
598 def run(self):
598 def run(self):
599 if os.name != 'nt' or self.distribution.pure:
599 if os.name != 'nt' or self.distribution.pure:
600 return build_scripts.run(self)
600 return build_scripts.run(self)
601
601
602 exebuilt = False
602 exebuilt = False
603 try:
603 try:
604 self.run_command('build_hgexe')
604 self.run_command('build_hgexe')
605 exebuilt = True
605 exebuilt = True
606 except (DistutilsError, CCompilerError):
606 except (DistutilsError, CCompilerError):
607 log.warn('failed to build optional hg.exe')
607 log.warn('failed to build optional hg.exe')
608
608
609 if exebuilt:
609 if exebuilt:
610 # Copying hg.exe to the scripts build directory ensures it is
610 # Copying hg.exe to the scripts build directory ensures it is
611 # installed by the install_scripts command.
611 # installed by the install_scripts command.
612 hgexecommand = self.get_finalized_command('build_hgexe')
612 hgexecommand = self.get_finalized_command('build_hgexe')
613 dest = os.path.join(self.build_dir, 'hg.exe')
613 dest = os.path.join(self.build_dir, 'hg.exe')
614 self.mkpath(self.build_dir)
614 self.mkpath(self.build_dir)
615 self.copy_file(hgexecommand.hgexepath, dest)
615 self.copy_file(hgexecommand.hgexepath, dest)
616
616
617 # Remove hg.bat because it is redundant with hg.exe.
617 # Remove hg.bat because it is redundant with hg.exe.
618 self.scripts.remove('contrib/win32/hg.bat')
618 self.scripts.remove('contrib/win32/hg.bat')
619
619
620 return build_scripts.run(self)
620 return build_scripts.run(self)
621
621
622
622
623 class hgbuildpy(build_py):
623 class hgbuildpy(build_py):
624 def finalize_options(self):
624 def finalize_options(self):
625 build_py.finalize_options(self)
625 build_py.finalize_options(self)
626
626
627 if self.distribution.pure:
627 if self.distribution.pure:
628 self.distribution.ext_modules = []
628 self.distribution.ext_modules = []
629 elif self.distribution.cffi:
629 elif self.distribution.cffi:
630 from mercurial.cffi import (
630 from mercurial.cffi import (
631 bdiffbuild,
631 bdiffbuild,
632 mpatchbuild,
632 mpatchbuild,
633 )
633 )
634
634
635 exts = [
635 exts = [
636 mpatchbuild.ffi.distutils_extension(),
636 mpatchbuild.ffi.distutils_extension(),
637 bdiffbuild.ffi.distutils_extension(),
637 bdiffbuild.ffi.distutils_extension(),
638 ]
638 ]
639 # cffi modules go here
639 # cffi modules go here
640 if sys.platform == 'darwin':
640 if sys.platform == 'darwin':
641 from mercurial.cffi import osutilbuild
641 from mercurial.cffi import osutilbuild
642
642
643 exts.append(osutilbuild.ffi.distutils_extension())
643 exts.append(osutilbuild.ffi.distutils_extension())
644 self.distribution.ext_modules = exts
644 self.distribution.ext_modules = exts
645 else:
645 else:
646 h = os.path.join(get_python_inc(), 'Python.h')
646 h = os.path.join(get_python_inc(), 'Python.h')
647 if not os.path.exists(h):
647 if not os.path.exists(h):
648 raise SystemExit(
648 raise SystemExit(
649 'Python headers are required to build '
649 'Python headers are required to build '
650 'Mercurial but weren\'t found in %s' % h
650 'Mercurial but weren\'t found in %s' % h
651 )
651 )
652
652
653 def run(self):
653 def run(self):
654 basepath = os.path.join(self.build_lib, 'mercurial')
654 basepath = os.path.join(self.build_lib, 'mercurial')
655 self.mkpath(basepath)
655 self.mkpath(basepath)
656
656
657 rust = self.distribution.rust
657 rust = self.distribution.rust
658 if self.distribution.pure:
658 if self.distribution.pure:
659 modulepolicy = 'py'
659 modulepolicy = 'py'
660 elif self.build_lib == '.':
660 elif self.build_lib == '.':
661 # in-place build should run without rebuilding and Rust extensions
661 # in-place build should run without rebuilding and Rust extensions
662 modulepolicy = 'rust+c-allow' if rust else 'allow'
662 modulepolicy = 'rust+c-allow' if rust else 'allow'
663 else:
663 else:
664 modulepolicy = 'rust+c' if rust else 'c'
664 modulepolicy = 'rust+c' if rust else 'c'
665
665
666 content = b''.join(
666 content = b''.join(
667 [
667 [
668 b'# this file is autogenerated by setup.py\n',
668 b'# this file is autogenerated by setup.py\n',
669 b'modulepolicy = b"%s"\n' % modulepolicy.encode('ascii'),
669 b'modulepolicy = b"%s"\n' % modulepolicy.encode('ascii'),
670 ]
670 ]
671 )
671 )
672 write_if_changed(os.path.join(basepath, '__modulepolicy__.py'), content)
672 write_if_changed(os.path.join(basepath, '__modulepolicy__.py'), content)
673
673
674 build_py.run(self)
674 build_py.run(self)
675
675
676
676
677 class buildhgextindex(Command):
677 class buildhgextindex(Command):
678 description = 'generate prebuilt index of hgext (for frozen package)'
678 description = 'generate prebuilt index of hgext (for frozen package)'
679 user_options = []
679 user_options = []
680 _indexfilename = 'hgext/__index__.py'
680 _indexfilename = 'hgext/__index__.py'
681
681
682 def initialize_options(self):
682 def initialize_options(self):
683 pass
683 pass
684
684
685 def finalize_options(self):
685 def finalize_options(self):
686 pass
686 pass
687
687
688 def run(self):
688 def run(self):
689 if os.path.exists(self._indexfilename):
689 if os.path.exists(self._indexfilename):
690 with open(self._indexfilename, 'w') as f:
690 with open(self._indexfilename, 'w') as f:
691 f.write('# empty\n')
691 f.write('# empty\n')
692
692
693 # here no extension enabled, disabled() lists up everything
693 # here no extension enabled, disabled() lists up everything
694 code = (
694 code = (
695 'import pprint; from mercurial import extensions; '
695 'import pprint; from mercurial import extensions; '
696 'ext = extensions.disabled();'
696 'ext = extensions.disabled();'
697 'ext.pop("__index__", None);'
697 'ext.pop("__index__", None);'
698 'pprint.pprint(ext)'
698 'pprint.pprint(ext)'
699 )
699 )
700 returncode, out, err = runcmd(
700 returncode, out, err = runcmd(
701 [sys.executable, '-c', code], localhgenv()
701 [sys.executable, '-c', code], localhgenv()
702 )
702 )
703 if err or returncode != 0:
703 if err or returncode != 0:
704 raise DistutilsExecError(err)
704 raise DistutilsExecError(err)
705
705
706 with open(self._indexfilename, 'wb') as f:
706 with open(self._indexfilename, 'wb') as f:
707 f.write(b'# this file is autogenerated by setup.py\n')
707 f.write(b'# this file is autogenerated by setup.py\n')
708 f.write(b'docs = ')
708 f.write(b'docs = ')
709 f.write(out)
709 f.write(out)
710
710
711
711
712 class buildhgexe(build_ext):
712 class buildhgexe(build_ext):
713 description = 'compile hg.exe from mercurial/exewrapper.c'
713 description = 'compile hg.exe from mercurial/exewrapper.c'
714
714
715 LONG_PATHS_MANIFEST = """\
715 LONG_PATHS_MANIFEST = """\
716 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
716 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
717 <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
717 <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
718 <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
718 <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
719 <security>
719 <security>
720 <requestedPrivileges>
720 <requestedPrivileges>
721 <requestedExecutionLevel
721 <requestedExecutionLevel
722 level="asInvoker"
722 level="asInvoker"
723 uiAccess="false"
723 uiAccess="false"
724 />
724 />
725 </requestedPrivileges>
725 </requestedPrivileges>
726 </security>
726 </security>
727 </trustInfo>
727 </trustInfo>
728 <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
728 <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
729 <application>
729 <application>
730 <!-- Windows Vista -->
730 <!-- Windows Vista -->
731 <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
731 <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
732 <!-- Windows 7 -->
732 <!-- Windows 7 -->
733 <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
733 <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
734 <!-- Windows 8 -->
734 <!-- Windows 8 -->
735 <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
735 <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
736 <!-- Windows 8.1 -->
736 <!-- Windows 8.1 -->
737 <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
737 <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
738 <!-- Windows 10 and Windows 11 -->
738 <!-- Windows 10 and Windows 11 -->
739 <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
739 <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
740 </application>
740 </application>
741 </compatibility>
741 </compatibility>
742 <application xmlns="urn:schemas-microsoft-com:asm.v3">
742 <application xmlns="urn:schemas-microsoft-com:asm.v3">
743 <windowsSettings
743 <windowsSettings
744 xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
744 xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
745 <ws2:longPathAware>true</ws2:longPathAware>
745 <ws2:longPathAware>true</ws2:longPathAware>
746 </windowsSettings>
746 </windowsSettings>
747 </application>
747 </application>
748 <dependency>
748 <dependency>
749 <dependentAssembly>
749 <dependentAssembly>
750 <assemblyIdentity type="win32"
750 <assemblyIdentity type="win32"
751 name="Microsoft.Windows.Common-Controls"
751 name="Microsoft.Windows.Common-Controls"
752 version="6.0.0.0"
752 version="6.0.0.0"
753 processorArchitecture="*"
753 processorArchitecture="*"
754 publicKeyToken="6595b64144ccf1df"
754 publicKeyToken="6595b64144ccf1df"
755 language="*" />
755 language="*" />
756 </dependentAssembly>
756 </dependentAssembly>
757 </dependency>
757 </dependency>
758 </assembly>
758 </assembly>
759 """
759 """
760
760
761 def initialize_options(self):
761 def initialize_options(self):
762 build_ext.initialize_options(self)
762 build_ext.initialize_options(self)
763
763
764 def build_extensions(self):
764 def build_extensions(self):
765 if os.name != 'nt':
765 if os.name != 'nt':
766 return
766 return
767 if isinstance(self.compiler, HackedMingw32CCompiler):
767 if isinstance(self.compiler, HackedMingw32CCompiler):
768 self.compiler.compiler_so = self.compiler.compiler # no -mdll
768 self.compiler.compiler_so = self.compiler.compiler # no -mdll
769 self.compiler.dll_libraries = [] # no -lmsrvc90
769 self.compiler.dll_libraries = [] # no -lmsrvc90
770
770
771 pythonlib = None
771 pythonlib = None
772
772
773 dirname = os.path.dirname(self.get_ext_fullpath('dummy'))
773 dirname = os.path.dirname(self.get_ext_fullpath('dummy'))
774 self.hgtarget = os.path.join(dirname, 'hg')
774 self.hgtarget = os.path.join(dirname, 'hg')
775
775
776 if getattr(sys, 'dllhandle', None):
776 if getattr(sys, 'dllhandle', None):
777 # Different Python installs can have different Python library
777 # Different Python installs can have different Python library
778 # names. e.g. the official CPython distribution uses pythonXY.dll
778 # names. e.g. the official CPython distribution uses pythonXY.dll
779 # and MinGW uses libpythonX.Y.dll.
779 # and MinGW uses libpythonX.Y.dll.
780 _kernel32 = ctypes.windll.kernel32
780 _kernel32 = ctypes.windll.kernel32
781 _kernel32.GetModuleFileNameA.argtypes = [
781 _kernel32.GetModuleFileNameA.argtypes = [
782 ctypes.c_void_p,
782 ctypes.c_void_p,
783 ctypes.c_void_p,
783 ctypes.c_void_p,
784 ctypes.c_ulong,
784 ctypes.c_ulong,
785 ]
785 ]
786 _kernel32.GetModuleFileNameA.restype = ctypes.c_ulong
786 _kernel32.GetModuleFileNameA.restype = ctypes.c_ulong
787 size = 1000
787 size = 1000
788 buf = ctypes.create_string_buffer(size + 1)
788 buf = ctypes.create_string_buffer(size + 1)
789 filelen = _kernel32.GetModuleFileNameA(
789 filelen = _kernel32.GetModuleFileNameA(
790 sys.dllhandle, ctypes.byref(buf), size
790 sys.dllhandle, ctypes.byref(buf), size
791 )
791 )
792
792
793 if filelen > 0 and filelen != size:
793 if filelen > 0 and filelen != size:
794 dllbasename = os.path.basename(buf.value)
794 dllbasename = os.path.basename(buf.value)
795 if not dllbasename.lower().endswith(b'.dll'):
795 if not dllbasename.lower().endswith(b'.dll'):
796 raise SystemExit(
796 raise SystemExit(
797 'Python DLL does not end with .dll: %s' % dllbasename
797 'Python DLL does not end with .dll: %s' % dllbasename
798 )
798 )
799 pythonlib = dllbasename[:-4]
799 pythonlib = dllbasename[:-4]
800
800
801 # Copy the pythonXY.dll next to the binary so that it runs
801 # Copy the pythonXY.dll next to the binary so that it runs
802 # without tampering with PATH.
802 # without tampering with PATH.
803 dest = os.path.join(
803 dest = os.path.join(
804 os.path.dirname(self.hgtarget),
804 os.path.dirname(self.hgtarget),
805 os.fsdecode(dllbasename),
805 os.fsdecode(dllbasename),
806 )
806 )
807
807
808 if not os.path.exists(dest):
808 if not os.path.exists(dest):
809 shutil.copy(buf.value, dest)
809 shutil.copy(buf.value, dest)
810
810
811 # Also overwrite python3.dll so that hgext.git is usable.
811 # Also overwrite python3.dll so that hgext.git is usable.
812 # TODO: also handle the MSYS flavor
812 # TODO: also handle the MSYS flavor
813 python_x = os.path.join(
813 python_x = os.path.join(
814 os.path.dirname(os.fsdecode(buf.value)),
814 os.path.dirname(os.fsdecode(buf.value)),
815 "python3.dll",
815 "python3.dll",
816 )
816 )
817
817
818 if os.path.exists(python_x):
818 if os.path.exists(python_x):
819 dest = os.path.join(
819 dest = os.path.join(
820 os.path.dirname(self.hgtarget),
820 os.path.dirname(self.hgtarget),
821 os.path.basename(python_x),
821 os.path.basename(python_x),
822 )
822 )
823
823
824 shutil.copy(python_x, dest)
824 shutil.copy(python_x, dest)
825
825
826 if not pythonlib:
826 if not pythonlib:
827 log.warn(
827 log.warn(
828 'could not determine Python DLL filename; assuming pythonXY'
828 'could not determine Python DLL filename; assuming pythonXY'
829 )
829 )
830
830
831 hv = sys.hexversion
831 hv = sys.hexversion
832 pythonlib = b'python%d%d' % (hv >> 24, (hv >> 16) & 0xFF)
832 pythonlib = b'python%d%d' % (hv >> 24, (hv >> 16) & 0xFF)
833
833
834 log.info('using %s as Python library name' % pythonlib)
834 log.info('using %s as Python library name' % pythonlib)
835 with open('mercurial/hgpythonlib.h', 'wb') as f:
835 with open('mercurial/hgpythonlib.h', 'wb') as f:
836 f.write(b'/* this file is autogenerated by setup.py */\n')
836 f.write(b'/* this file is autogenerated by setup.py */\n')
837 f.write(b'#define HGPYTHONLIB "%s"\n' % pythonlib)
837 f.write(b'#define HGPYTHONLIB "%s"\n' % pythonlib)
838
838
839 objects = self.compiler.compile(
839 objects = self.compiler.compile(
840 ['mercurial/exewrapper.c'],
840 ['mercurial/exewrapper.c'],
841 output_dir=self.build_temp,
841 output_dir=self.build_temp,
842 macros=[('_UNICODE', None), ('UNICODE', None)],
842 macros=[('_UNICODE', None), ('UNICODE', None)],
843 )
843 )
844 self.compiler.link_executable(
844 self.compiler.link_executable(
845 objects, self.hgtarget, libraries=[], output_dir=self.build_temp
845 objects, self.hgtarget, libraries=[], output_dir=self.build_temp
846 )
846 )
847
847
848 self.addlongpathsmanifest()
848 self.addlongpathsmanifest()
849
849
850 def addlongpathsmanifest(self):
850 def addlongpathsmanifest(self):
851 """Add manifest pieces so that hg.exe understands long paths
851 """Add manifest pieces so that hg.exe understands long paths
852
852
853 Why resource #1 should be used for .exe manifests? I don't know and
853 Why resource #1 should be used for .exe manifests? I don't know and
854 wasn't able to find an explanation for mortals. But it seems to work.
854 wasn't able to find an explanation for mortals. But it seems to work.
855 """
855 """
856 exefname = self.compiler.executable_filename(self.hgtarget)
856 exefname = self.compiler.executable_filename(self.hgtarget)
857 fdauto, manfname = tempfile.mkstemp(suffix='.hg.exe.manifest')
857 fdauto, manfname = tempfile.mkstemp(suffix='.hg.exe.manifest')
858 os.close(fdauto)
858 os.close(fdauto)
859 with open(manfname, 'w', encoding="UTF-8") as f:
859 with open(manfname, 'w', encoding="UTF-8") as f:
860 f.write(self.LONG_PATHS_MANIFEST)
860 f.write(self.LONG_PATHS_MANIFEST)
861 log.info("long paths manifest is written to '%s'" % manfname)
861 log.info("long paths manifest is written to '%s'" % manfname)
862 outputresource = '-outputresource:%s;#1' % exefname
862 outputresource = '-outputresource:%s;#1' % exefname
863 log.info("running mt.exe to update hg.exe's manifest in-place")
863 log.info("running mt.exe to update hg.exe's manifest in-place")
864
864
865 self.spawn(
865 self.spawn(
866 [
866 [
867 self.compiler.mt,
867 self.compiler.mt,
868 '-nologo',
868 '-nologo',
869 '-manifest',
869 '-manifest',
870 manfname,
870 manfname,
871 outputresource,
871 outputresource,
872 ]
872 ]
873 )
873 )
874 log.info("done updating hg.exe's manifest")
874 log.info("done updating hg.exe's manifest")
875 os.remove(manfname)
875 os.remove(manfname)
876
876
877 @property
877 @property
878 def hgexepath(self):
878 def hgexepath(self):
879 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
879 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
880 return os.path.join(self.build_temp, dir, 'hg.exe')
880 return os.path.join(self.build_temp, dir, 'hg.exe')
881
881
882
882
883 class hgbuilddoc(Command):
883 class hgbuilddoc(Command):
884 description = 'build documentation'
884 description = 'build documentation'
885 user_options = [
885 user_options = [
886 ('man', None, 'generate man pages'),
886 ('man', None, 'generate man pages'),
887 ('html', None, 'generate html pages'),
887 ('html', None, 'generate html pages'),
888 ]
888 ]
889
889
890 def initialize_options(self):
890 def initialize_options(self):
891 self.man = None
891 self.man = None
892 self.html = None
892 self.html = None
893
893
894 def finalize_options(self):
894 def finalize_options(self):
895 # If --man or --html are set, only generate what we're told to.
895 # If --man or --html are set, only generate what we're told to.
896 # Otherwise generate everything.
896 # Otherwise generate everything.
897 have_subset = self.man is not None or self.html is not None
897 have_subset = self.man is not None or self.html is not None
898
898
899 if have_subset:
899 if have_subset:
900 self.man = True if self.man else False
900 self.man = True if self.man else False
901 self.html = True if self.html else False
901 self.html = True if self.html else False
902 else:
902 else:
903 self.man = True
903 self.man = True
904 self.html = True
904 self.html = True
905
905
906 def run(self):
906 def run(self):
907 def normalizecrlf(p):
907 def normalizecrlf(p):
908 with open(p, 'rb') as fh:
908 with open(p, 'rb') as fh:
909 orig = fh.read()
909 orig = fh.read()
910
910
911 if b'\r\n' not in orig:
911 if b'\r\n' not in orig:
912 return
912 return
913
913
914 log.info('normalizing %s to LF line endings' % p)
914 log.info('normalizing %s to LF line endings' % p)
915 with open(p, 'wb') as fh:
915 with open(p, 'wb') as fh:
916 fh.write(orig.replace(b'\r\n', b'\n'))
916 fh.write(orig.replace(b'\r\n', b'\n'))
917
917
918 def gentxt(root):
918 def gentxt(root):
919 txt = 'doc/%s.txt' % root
919 txt = 'doc/%s.txt' % root
920 log.info('generating %s' % txt)
920 log.info('generating %s' % txt)
921 res, out, err = runcmd(
921 res, out, err = runcmd(
922 [sys.executable, 'gendoc.py', root], os.environ, cwd='doc'
922 [sys.executable, 'gendoc.py', root], os.environ, cwd='doc'
923 )
923 )
924 if res:
924 if res:
925 raise SystemExit(
925 raise SystemExit(
926 'error running gendoc.py: %s'
926 'error running gendoc.py: %s'
927 % '\n'.join([sysstr(out), sysstr(err)])
927 % '\n'.join([sysstr(out), sysstr(err)])
928 )
928 )
929
929
930 with open(txt, 'wb') as fh:
930 with open(txt, 'wb') as fh:
931 fh.write(out)
931 fh.write(out)
932
932
933 def gengendoc(root):
933 def gengendoc(root):
934 gendoc = 'doc/%s.gendoc.txt' % root
934 gendoc = 'doc/%s.gendoc.txt' % root
935
935
936 log.info('generating %s' % gendoc)
936 log.info('generating %s' % gendoc)
937 res, out, err = runcmd(
937 res, out, err = runcmd(
938 [sys.executable, 'gendoc.py', '%s.gendoc' % root],
938 [sys.executable, 'gendoc.py', '%s.gendoc' % root],
939 os.environ,
939 os.environ,
940 cwd='doc',
940 cwd='doc',
941 )
941 )
942 if res:
942 if res:
943 raise SystemExit(
943 raise SystemExit(
944 'error running gendoc: %s'
944 'error running gendoc: %s'
945 % '\n'.join([sysstr(out), sysstr(err)])
945 % '\n'.join([sysstr(out), sysstr(err)])
946 )
946 )
947
947
948 with open(gendoc, 'wb') as fh:
948 with open(gendoc, 'wb') as fh:
949 fh.write(out)
949 fh.write(out)
950
950
951 def genman(root):
951 def genman(root):
952 log.info('generating doc/%s' % root)
952 log.info('generating doc/%s' % root)
953 res, out, err = runcmd(
953 res, out, err = runcmd(
954 [
954 [
955 sys.executable,
955 sys.executable,
956 'runrst',
956 'runrst',
957 'hgmanpage',
957 'hgmanpage',
958 '--halt',
958 '--halt',
959 'warning',
959 'warning',
960 '--strip-elements-with-class',
960 '--strip-elements-with-class',
961 'htmlonly',
961 'htmlonly',
962 '%s.txt' % root,
962 '%s.txt' % root,
963 root,
963 root,
964 ],
964 ],
965 os.environ,
965 os.environ,
966 cwd='doc',
966 cwd='doc',
967 )
967 )
968 if res:
968 if res:
969 raise SystemExit(
969 raise SystemExit(
970 'error running runrst: %s'
970 'error running runrst: %s'
971 % '\n'.join([sysstr(out), sysstr(err)])
971 % '\n'.join([sysstr(out), sysstr(err)])
972 )
972 )
973
973
974 normalizecrlf('doc/%s' % root)
974 normalizecrlf('doc/%s' % root)
975
975
976 def genhtml(root):
976 def genhtml(root):
977 log.info('generating doc/%s.html' % root)
977 log.info('generating doc/%s.html' % root)
978 res, out, err = runcmd(
978 res, out, err = runcmd(
979 [
979 [
980 sys.executable,
980 sys.executable,
981 'runrst',
981 'runrst',
982 'html',
982 'html',
983 '--halt',
983 '--halt',
984 'warning',
984 'warning',
985 '--link-stylesheet',
985 '--link-stylesheet',
986 '--stylesheet-path',
986 '--stylesheet-path',
987 'style.css',
987 'style.css',
988 '%s.txt' % root,
988 '%s.txt' % root,
989 '%s.html' % root,
989 '%s.html' % root,
990 ],
990 ],
991 os.environ,
991 os.environ,
992 cwd='doc',
992 cwd='doc',
993 )
993 )
994 if res:
994 if res:
995 raise SystemExit(
995 raise SystemExit(
996 'error running runrst: %s'
996 'error running runrst: %s'
997 % '\n'.join([sysstr(out), sysstr(err)])
997 % '\n'.join([sysstr(out), sysstr(err)])
998 )
998 )
999
999
1000 normalizecrlf('doc/%s.html' % root)
1000 normalizecrlf('doc/%s.html' % root)
1001
1001
1002 # This logic is duplicated in doc/Makefile.
1002 # This logic is duplicated in doc/Makefile.
1003 sources = {
1003 sources = {
1004 f
1004 f
1005 for f in os.listdir('mercurial/helptext')
1005 for f in os.listdir('mercurial/helptext')
1006 if re.search(r'[0-9]\.txt$', f)
1006 if re.search(r'[0-9]\.txt$', f)
1007 }
1007 }
1008
1008
1009 # common.txt is a one-off.
1009 # common.txt is a one-off.
1010 gentxt('common')
1010 gentxt('common')
1011
1011
1012 for source in sorted(sources):
1012 for source in sorted(sources):
1013 assert source[-4:] == '.txt'
1013 assert source[-4:] == '.txt'
1014 root = source[:-4]
1014 root = source[:-4]
1015
1015
1016 gentxt(root)
1016 gentxt(root)
1017 gengendoc(root)
1017 gengendoc(root)
1018
1018
1019 if self.man:
1019 if self.man:
1020 genman(root)
1020 genman(root)
1021 if self.html:
1021 if self.html:
1022 genhtml(root)
1022 genhtml(root)
1023
1023
1024
1024
1025 class hginstall(install):
1025 class hginstall(install):
1026
1026
1027 user_options = install.user_options + [
1027 user_options = install.user_options + [
1028 (
1028 (
1029 'old-and-unmanageable',
1029 'old-and-unmanageable',
1030 None,
1030 None,
1031 'noop, present for eggless setuptools compat',
1031 'noop, present for eggless setuptools compat',
1032 ),
1032 ),
1033 (
1033 (
1034 'single-version-externally-managed',
1034 'single-version-externally-managed',
1035 None,
1035 None,
1036 'noop, present for eggless setuptools compat',
1036 'noop, present for eggless setuptools compat',
1037 ),
1037 ),
1038 ]
1038 ]
1039
1039
1040 sub_commands = install.sub_commands + [
1040 sub_commands = install.sub_commands + [
1041 ('install_completion', lambda self: True)
1041 ('install_completion', lambda self: True)
1042 ]
1042 ]
1043
1043
1044 # Also helps setuptools not be sad while we refuse to create eggs.
1044 # Also helps setuptools not be sad while we refuse to create eggs.
1045 single_version_externally_managed = True
1045 single_version_externally_managed = True
1046
1046
1047 def get_sub_commands(self):
1047 def get_sub_commands(self):
1048 # Screen out egg related commands to prevent egg generation. But allow
1048 # Screen out egg related commands to prevent egg generation. But allow
1049 # mercurial.egg-info generation, since that is part of modern
1049 # mercurial.egg-info generation, since that is part of modern
1050 # packaging.
1050 # packaging.
1051 excl = {'bdist_egg'}
1051 excl = {'bdist_egg'}
1052 return filter(lambda x: x not in excl, install.get_sub_commands(self))
1052 return filter(lambda x: x not in excl, install.get_sub_commands(self))
1053
1053
1054
1054
1055 class hginstalllib(install_lib):
1055 class hginstalllib(install_lib):
1056 """
1056 """
1057 This is a specialization of install_lib that replaces the copy_file used
1057 This is a specialization of install_lib that replaces the copy_file used
1058 there so that it supports setting the mode of files after copying them,
1058 there so that it supports setting the mode of files after copying them,
1059 instead of just preserving the mode that the files originally had. If your
1059 instead of just preserving the mode that the files originally had. If your
1060 system has a umask of something like 027, preserving the permissions when
1060 system has a umask of something like 027, preserving the permissions when
1061 copying will lead to a broken install.
1061 copying will lead to a broken install.
1062
1062
1063 Note that just passing keep_permissions=False to copy_file would be
1063 Note that just passing keep_permissions=False to copy_file would be
1064 insufficient, as it might still be applying a umask.
1064 insufficient, as it might still be applying a umask.
1065 """
1065 """
1066
1066
1067 def run(self):
1067 def run(self):
1068 realcopyfile = file_util.copy_file
1068 realcopyfile = file_util.copy_file
1069
1069
1070 def copyfileandsetmode(*args, **kwargs):
1070 def copyfileandsetmode(*args, **kwargs):
1071 src, dst = args[0], args[1]
1071 src, dst = args[0], args[1]
1072 dst, copied = realcopyfile(*args, **kwargs)
1072 dst, copied = realcopyfile(*args, **kwargs)
1073 if copied:
1073 if copied:
1074 st = os.stat(src)
1074 st = os.stat(src)
1075 # Persist executable bit (apply it to group and other if user
1075 # Persist executable bit (apply it to group and other if user
1076 # has it)
1076 # has it)
1077 if st[stat.ST_MODE] & stat.S_IXUSR:
1077 if st[stat.ST_MODE] & stat.S_IXUSR:
1078 setmode = int('0755', 8)
1078 setmode = int('0755', 8)
1079 else:
1079 else:
1080 setmode = int('0644', 8)
1080 setmode = int('0644', 8)
1081 m = stat.S_IMODE(st[stat.ST_MODE])
1081 m = stat.S_IMODE(st[stat.ST_MODE])
1082 m = (m & ~int('0777', 8)) | setmode
1082 m = (m & ~int('0777', 8)) | setmode
1083 os.chmod(dst, m)
1083 os.chmod(dst, m)
1084
1084
1085 file_util.copy_file = copyfileandsetmode
1085 file_util.copy_file = copyfileandsetmode
1086 try:
1086 try:
1087 install_lib.run(self)
1087 install_lib.run(self)
1088 finally:
1088 finally:
1089 file_util.copy_file = realcopyfile
1089 file_util.copy_file = realcopyfile
1090
1090
1091
1091
1092 class hginstallscripts(install_scripts):
1092 class hginstallscripts(install_scripts):
1093 """
1093 """
1094 This is a specialization of install_scripts that replaces the @LIBDIR@ with
1094 This is a specialization of install_scripts that replaces the @LIBDIR@ with
1095 the configured directory for modules. If possible, the path is made relative
1095 the configured directory for modules. If possible, the path is made relative
1096 to the directory for scripts.
1096 to the directory for scripts.
1097 """
1097 """
1098
1098
1099 def initialize_options(self):
1099 def initialize_options(self):
1100 install_scripts.initialize_options(self)
1100 install_scripts.initialize_options(self)
1101
1101
1102 self.install_lib = None
1102 self.install_lib = None
1103
1103
1104 def finalize_options(self):
1104 def finalize_options(self):
1105 install_scripts.finalize_options(self)
1105 install_scripts.finalize_options(self)
1106 self.set_undefined_options('install', ('install_lib', 'install_lib'))
1106 self.set_undefined_options('install', ('install_lib', 'install_lib'))
1107
1107
1108 def run(self):
1108 def run(self):
1109 install_scripts.run(self)
1109 install_scripts.run(self)
1110
1110
1111 # It only makes sense to replace @LIBDIR@ with the install path if
1111 # It only makes sense to replace @LIBDIR@ with the install path if
1112 # the install path is known. For wheels, the logic below calculates
1112 # the install path is known. For wheels, the logic below calculates
1113 # the libdir to be "../..". This is because the internal layout of a
1113 # the libdir to be "../..". This is because the internal layout of a
1114 # wheel archive looks like:
1114 # wheel archive looks like:
1115 #
1115 #
1116 # mercurial-3.6.1.data/scripts/hg
1116 # mercurial-3.6.1.data/scripts/hg
1117 # mercurial/__init__.py
1117 # mercurial/__init__.py
1118 #
1118 #
1119 # When installing wheels, the subdirectories of the "<pkg>.data"
1119 # When installing wheels, the subdirectories of the "<pkg>.data"
1120 # directory are translated to system local paths and files therein
1120 # directory are translated to system local paths and files therein
1121 # are copied in place. The mercurial/* files are installed into the
1121 # are copied in place. The mercurial/* files are installed into the
1122 # site-packages directory. However, the site-packages directory
1122 # site-packages directory. However, the site-packages directory
1123 # isn't known until wheel install time. This means we have no clue
1123 # isn't known until wheel install time. This means we have no clue
1124 # at wheel generation time what the installed site-packages directory
1124 # at wheel generation time what the installed site-packages directory
1125 # will be. And, wheels don't appear to provide the ability to register
1125 # will be. And, wheels don't appear to provide the ability to register
1126 # custom code to run during wheel installation. This all means that
1126 # custom code to run during wheel installation. This all means that
1127 # we can't reliably set the libdir in wheels: the default behavior
1127 # we can't reliably set the libdir in wheels: the default behavior
1128 # of looking in sys.path must do.
1128 # of looking in sys.path must do.
1129
1129
1130 if (
1130 if (
1131 os.path.splitdrive(self.install_dir)[0]
1131 os.path.splitdrive(self.install_dir)[0]
1132 != os.path.splitdrive(self.install_lib)[0]
1132 != os.path.splitdrive(self.install_lib)[0]
1133 ):
1133 ):
1134 # can't make relative paths from one drive to another, so use an
1134 # can't make relative paths from one drive to another, so use an
1135 # absolute path instead
1135 # absolute path instead
1136 libdir = self.install_lib
1136 libdir = self.install_lib
1137 else:
1137 else:
1138 libdir = os.path.relpath(self.install_lib, self.install_dir)
1138 libdir = os.path.relpath(self.install_lib, self.install_dir)
1139
1139
1140 for outfile in self.outfiles:
1140 for outfile in self.outfiles:
1141 with open(outfile, 'rb') as fp:
1141 with open(outfile, 'rb') as fp:
1142 data = fp.read()
1142 data = fp.read()
1143
1143
1144 # skip binary files
1144 # skip binary files
1145 if b'\0' in data:
1145 if b'\0' in data:
1146 continue
1146 continue
1147
1147
1148 # During local installs, the shebang will be rewritten to the final
1148 # During local installs, the shebang will be rewritten to the final
1149 # install path. During wheel packaging, the shebang has a special
1149 # install path. During wheel packaging, the shebang has a special
1150 # value.
1150 # value.
1151 if data.startswith(b'#!python'):
1151 if data.startswith(b'#!python'):
1152 log.info(
1152 log.info(
1153 'not rewriting @LIBDIR@ in %s because install path '
1153 'not rewriting @LIBDIR@ in %s because install path '
1154 'not known' % outfile
1154 'not known' % outfile
1155 )
1155 )
1156 continue
1156 continue
1157
1157
1158 data = data.replace(b'@LIBDIR@', libdir.encode('unicode_escape'))
1158 data = data.replace(b'@LIBDIR@', libdir.encode('unicode_escape'))
1159 with open(outfile, 'wb') as fp:
1159 with open(outfile, 'wb') as fp:
1160 fp.write(data)
1160 fp.write(data)
1161
1161
1162
1162
1163 class hginstallcompletion(Command):
1163 class hginstallcompletion(Command):
1164 description = 'Install shell completion'
1164 description = 'Install shell completion'
1165
1165
1166 def initialize_options(self):
1166 def initialize_options(self):
1167 self.install_dir = None
1167 self.install_dir = None
1168 self.outputs = []
1168 self.outputs = []
1169
1169
1170 def finalize_options(self):
1170 def finalize_options(self):
1171 self.set_undefined_options(
1171 self.set_undefined_options(
1172 'install_data', ('install_dir', 'install_dir')
1172 'install_data', ('install_dir', 'install_dir')
1173 )
1173 )
1174
1174
1175 def get_outputs(self):
1175 def get_outputs(self):
1176 return self.outputs
1176 return self.outputs
1177
1177
1178 def run(self):
1178 def run(self):
1179 for src, dir_path, dest in (
1179 for src, dir_path, dest in (
1180 (
1180 (
1181 'bash_completion',
1181 'bash_completion',
1182 ('share', 'bash-completion', 'completions'),
1182 ('share', 'bash-completion', 'completions'),
1183 'hg',
1183 'hg',
1184 ),
1184 ),
1185 ('zsh_completion', ('share', 'zsh', 'site-functions'), '_hg'),
1185 ('zsh_completion', ('share', 'zsh', 'site-functions'), '_hg'),
1186 ):
1186 ):
1187 dir = os.path.join(self.install_dir, *dir_path)
1187 dir = os.path.join(self.install_dir, *dir_path)
1188 self.mkpath(dir)
1188 self.mkpath(dir)
1189
1189
1190 dest = os.path.join(dir, dest)
1190 dest = os.path.join(dir, dest)
1191 self.outputs.append(dest)
1191 self.outputs.append(dest)
1192 self.copy_file(os.path.join('contrib', src), dest)
1192 self.copy_file(os.path.join('contrib', src), dest)
1193
1193
1194
1194
1195 # virtualenv installs custom distutils/__init__.py and
1195 # virtualenv installs custom distutils/__init__.py and
1196 # distutils/distutils.cfg files which essentially proxy back to the
1196 # distutils/distutils.cfg files which essentially proxy back to the
1197 # "real" distutils in the main Python install. The presence of this
1197 # "real" distutils in the main Python install. The presence of this
1198 # directory causes py2exe to pick up the "hacked" distutils package
1198 # directory causes py2exe to pick up the "hacked" distutils package
1199 # from the virtualenv and "import distutils" will fail from the py2exe
1199 # from the virtualenv and "import distutils" will fail from the py2exe
1200 # build because the "real" distutils files can't be located.
1200 # build because the "real" distutils files can't be located.
1201 #
1201 #
1202 # We work around this by monkeypatching the py2exe code finding Python
1202 # We work around this by monkeypatching the py2exe code finding Python
1203 # modules to replace the found virtualenv distutils modules with the
1203 # modules to replace the found virtualenv distutils modules with the
1204 # original versions via filesystem scanning. This is a bit hacky. But
1204 # original versions via filesystem scanning. This is a bit hacky. But
1205 # it allows us to use virtualenvs for py2exe packaging, which is more
1205 # it allows us to use virtualenvs for py2exe packaging, which is more
1206 # deterministic and reproducible.
1206 # deterministic and reproducible.
1207 #
1207 #
1208 # It's worth noting that the common StackOverflow suggestions for this
1208 # It's worth noting that the common StackOverflow suggestions for this
1209 # problem involve copying the original distutils files into the
1209 # problem involve copying the original distutils files into the
1210 # virtualenv or into the staging directory after setup() is invoked.
1210 # virtualenv or into the staging directory after setup() is invoked.
1211 # The former is very brittle and can easily break setup(). Our hacking
1211 # The former is very brittle and can easily break setup(). Our hacking
1212 # of the found modules routine has a similar result as copying the files
1212 # of the found modules routine has a similar result as copying the files
1213 # manually. But it makes fewer assumptions about how py2exe works and
1213 # manually. But it makes fewer assumptions about how py2exe works and
1214 # is less brittle.
1214 # is less brittle.
1215
1215
1216 # This only catches virtualenvs made with virtualenv (as opposed to
1216 # This only catches virtualenvs made with virtualenv (as opposed to
1217 # venv, which is likely what Python 3 uses).
1217 # venv, which is likely what Python 3 uses).
1218 py2exehacked = py2exeloaded and getattr(sys, 'real_prefix', None) is not None
1218 py2exehacked = py2exeloaded and getattr(sys, 'real_prefix', None) is not None
1219
1219
1220 if py2exehacked:
1220 if py2exehacked:
1221 from distutils.command.py2exe import py2exe as buildpy2exe
1221 from distutils.command.py2exe import py2exe as buildpy2exe
1222 from py2exe.mf import Module as py2exemodule
1222 from py2exe.mf import Module as py2exemodule
1223
1223
1224 class hgbuildpy2exe(buildpy2exe):
1224 class hgbuildpy2exe(buildpy2exe):
1225 def find_needed_modules(self, mf, files, modules):
1225 def find_needed_modules(self, mf, files, modules):
1226 res = buildpy2exe.find_needed_modules(self, mf, files, modules)
1226 res = buildpy2exe.find_needed_modules(self, mf, files, modules)
1227
1227
1228 # Replace virtualenv's distutils modules with the real ones.
1228 # Replace virtualenv's distutils modules with the real ones.
1229 modules = {}
1229 modules = {}
1230 for k, v in res.modules.items():
1230 for k, v in res.modules.items():
1231 if k != 'distutils' and not k.startswith('distutils.'):
1231 if k != 'distutils' and not k.startswith('distutils.'):
1232 modules[k] = v
1232 modules[k] = v
1233
1233
1234 res.modules = modules
1234 res.modules = modules
1235
1235
1236 import opcode
1236 import opcode
1237
1237
1238 distutilsreal = os.path.join(
1238 distutilsreal = os.path.join(
1239 os.path.dirname(opcode.__file__), 'distutils'
1239 os.path.dirname(opcode.__file__), 'distutils'
1240 )
1240 )
1241
1241
1242 for root, dirs, files in os.walk(distutilsreal):
1242 for root, dirs, files in os.walk(distutilsreal):
1243 for f in sorted(files):
1243 for f in sorted(files):
1244 if not f.endswith('.py'):
1244 if not f.endswith('.py'):
1245 continue
1245 continue
1246
1246
1247 full = os.path.join(root, f)
1247 full = os.path.join(root, f)
1248
1248
1249 parents = ['distutils']
1249 parents = ['distutils']
1250
1250
1251 if root != distutilsreal:
1251 if root != distutilsreal:
1252 rel = os.path.relpath(root, distutilsreal)
1252 rel = os.path.relpath(root, distutilsreal)
1253 parents.extend(p for p in rel.split(os.sep))
1253 parents.extend(p for p in rel.split(os.sep))
1254
1254
1255 modname = '%s.%s' % ('.'.join(parents), f[:-3])
1255 modname = '%s.%s' % ('.'.join(parents), f[:-3])
1256
1256
1257 if modname.startswith('distutils.tests.'):
1257 if modname.startswith('distutils.tests.'):
1258 continue
1258 continue
1259
1259
1260 if modname.endswith('.__init__'):
1260 if modname.endswith('.__init__'):
1261 modname = modname[: -len('.__init__')]
1261 modname = modname[: -len('.__init__')]
1262 path = os.path.dirname(full)
1262 path = os.path.dirname(full)
1263 else:
1263 else:
1264 path = None
1264 path = None
1265
1265
1266 res.modules[modname] = py2exemodule(
1266 res.modules[modname] = py2exemodule(
1267 modname, full, path=path
1267 modname, full, path=path
1268 )
1268 )
1269
1269
1270 if 'distutils' not in res.modules:
1270 if 'distutils' not in res.modules:
1271 raise SystemExit('could not find distutils modules')
1271 raise SystemExit('could not find distutils modules')
1272
1272
1273 return res
1273 return res
1274
1274
1275
1275
1276 cmdclass = {
1276 cmdclass = {
1277 'build': hgbuild,
1277 'build': hgbuild,
1278 'build_doc': hgbuilddoc,
1278 'build_doc': hgbuilddoc,
1279 'build_mo': hgbuildmo,
1279 'build_mo': hgbuildmo,
1280 'build_ext': hgbuildext,
1280 'build_ext': hgbuildext,
1281 'build_py': hgbuildpy,
1281 'build_py': hgbuildpy,
1282 'build_scripts': hgbuildscripts,
1282 'build_scripts': hgbuildscripts,
1283 'build_hgextindex': buildhgextindex,
1283 'build_hgextindex': buildhgextindex,
1284 'install': hginstall,
1284 'install': hginstall,
1285 'install_completion': hginstallcompletion,
1285 'install_completion': hginstallcompletion,
1286 'install_lib': hginstalllib,
1286 'install_lib': hginstalllib,
1287 'install_scripts': hginstallscripts,
1287 'install_scripts': hginstallscripts,
1288 'build_hgexe': buildhgexe,
1288 'build_hgexe': buildhgexe,
1289 }
1289 }
1290
1290
1291 if py2exehacked:
1291 if py2exehacked:
1292 cmdclass['py2exe'] = hgbuildpy2exe
1292 cmdclass['py2exe'] = hgbuildpy2exe
1293
1293
1294 packages = [
1294 packages = [
1295 'mercurial',
1295 'mercurial',
1296 'mercurial.cext',
1296 'mercurial.cext',
1297 'mercurial.cffi',
1297 'mercurial.cffi',
1298 'mercurial.defaultrc',
1298 'mercurial.defaultrc',
1299 'mercurial.dirstateutils',
1299 'mercurial.dirstateutils',
1300 'mercurial.helptext',
1300 'mercurial.helptext',
1301 'mercurial.helptext.internals',
1301 'mercurial.helptext.internals',
1302 'mercurial.hgweb',
1302 'mercurial.hgweb',
1303 'mercurial.interfaces',
1303 'mercurial.interfaces',
1304 'mercurial.pure',
1304 'mercurial.pure',
1305 'mercurial.stabletailgraph',
1305 'mercurial.stabletailgraph',
1306 'mercurial.templates',
1306 'mercurial.templates',
1307 'mercurial.thirdparty',
1307 'mercurial.thirdparty',
1308 'mercurial.thirdparty.attr',
1308 'mercurial.thirdparty.attr',
1309 'mercurial.thirdparty.tomli',
1309 'mercurial.thirdparty.zope',
1310 'mercurial.thirdparty.zope',
1310 'mercurial.thirdparty.zope.interface',
1311 'mercurial.thirdparty.zope.interface',
1311 'mercurial.upgrade_utils',
1312 'mercurial.upgrade_utils',
1312 'mercurial.utils',
1313 'mercurial.utils',
1313 'mercurial.revlogutils',
1314 'mercurial.revlogutils',
1314 'mercurial.testing',
1315 'mercurial.testing',
1315 'hgext',
1316 'hgext',
1316 'hgext.convert',
1317 'hgext.convert',
1317 'hgext.fsmonitor',
1318 'hgext.fsmonitor',
1318 'hgext.fastannotate',
1319 'hgext.fastannotate',
1319 'hgext.fsmonitor.pywatchman',
1320 'hgext.fsmonitor.pywatchman',
1320 'hgext.git',
1321 'hgext.git',
1321 'hgext.highlight',
1322 'hgext.highlight',
1322 'hgext.hooklib',
1323 'hgext.hooklib',
1323 'hgext.infinitepush',
1324 'hgext.infinitepush',
1324 'hgext.largefiles',
1325 'hgext.largefiles',
1325 'hgext.lfs',
1326 'hgext.lfs',
1326 'hgext.narrow',
1327 'hgext.narrow',
1327 'hgext.remotefilelog',
1328 'hgext.remotefilelog',
1328 'hgext.zeroconf',
1329 'hgext.zeroconf',
1329 'hgext3rd',
1330 'hgext3rd',
1330 'hgdemandimport',
1331 'hgdemandimport',
1331 ]
1332 ]
1332
1333
1333 for name in os.listdir(os.path.join('mercurial', 'templates')):
1334 for name in os.listdir(os.path.join('mercurial', 'templates')):
1334 if name != '__pycache__' and os.path.isdir(
1335 if name != '__pycache__' and os.path.isdir(
1335 os.path.join('mercurial', 'templates', name)
1336 os.path.join('mercurial', 'templates', name)
1336 ):
1337 ):
1337 packages.append('mercurial.templates.%s' % name)
1338 packages.append('mercurial.templates.%s' % name)
1338
1339
1339 if 'HG_PY2EXE_EXTRA_INSTALL_PACKAGES' in os.environ:
1340 if 'HG_PY2EXE_EXTRA_INSTALL_PACKAGES' in os.environ:
1340 # py2exe can't cope with namespace packages very well, so we have to
1341 # py2exe can't cope with namespace packages very well, so we have to
1341 # install any hgext3rd.* extensions that we want in the final py2exe
1342 # install any hgext3rd.* extensions that we want in the final py2exe
1342 # image here. This is gross, but you gotta do what you gotta do.
1343 # image here. This is gross, but you gotta do what you gotta do.
1343 packages.extend(os.environ['HG_PY2EXE_EXTRA_INSTALL_PACKAGES'].split(' '))
1344 packages.extend(os.environ['HG_PY2EXE_EXTRA_INSTALL_PACKAGES'].split(' '))
1344
1345
1345 common_depends = [
1346 common_depends = [
1346 'mercurial/bitmanipulation.h',
1347 'mercurial/bitmanipulation.h',
1347 'mercurial/compat.h',
1348 'mercurial/compat.h',
1348 'mercurial/cext/util.h',
1349 'mercurial/cext/util.h',
1349 ]
1350 ]
1350 common_include_dirs = ['mercurial']
1351 common_include_dirs = ['mercurial']
1351
1352
1352 common_cflags = []
1353 common_cflags = []
1353
1354
1354 # MSVC 2008 still needs declarations at the top of the scope, but Python 3.9
1355 # MSVC 2008 still needs declarations at the top of the scope, but Python 3.9
1355 # makes declarations not at the top of a scope in the headers.
1356 # makes declarations not at the top of a scope in the headers.
1356 if os.name != 'nt' and sys.version_info[1] < 9:
1357 if os.name != 'nt' and sys.version_info[1] < 9:
1357 common_cflags = ['-Werror=declaration-after-statement']
1358 common_cflags = ['-Werror=declaration-after-statement']
1358
1359
1359 osutil_cflags = []
1360 osutil_cflags = []
1360 osutil_ldflags = []
1361 osutil_ldflags = []
1361
1362
1362 # platform specific macros
1363 # platform specific macros
1363 for plat, func in [('bsd', 'setproctitle')]:
1364 for plat, func in [('bsd', 'setproctitle')]:
1364 if re.search(plat, sys.platform) and hasfunction(new_compiler(), func):
1365 if re.search(plat, sys.platform) and hasfunction(new_compiler(), func):
1365 osutil_cflags.append('-DHAVE_%s' % func.upper())
1366 osutil_cflags.append('-DHAVE_%s' % func.upper())
1366
1367
1367 for plat, macro, code in [
1368 for plat, macro, code in [
1368 (
1369 (
1369 'bsd|darwin',
1370 'bsd|darwin',
1370 'BSD_STATFS',
1371 'BSD_STATFS',
1371 '''
1372 '''
1372 #include <sys/param.h>
1373 #include <sys/param.h>
1373 #include <sys/mount.h>
1374 #include <sys/mount.h>
1374 int main() { struct statfs s; return sizeof(s.f_fstypename); }
1375 int main() { struct statfs s; return sizeof(s.f_fstypename); }
1375 ''',
1376 ''',
1376 ),
1377 ),
1377 (
1378 (
1378 'linux',
1379 'linux',
1379 'LINUX_STATFS',
1380 'LINUX_STATFS',
1380 '''
1381 '''
1381 #include <linux/magic.h>
1382 #include <linux/magic.h>
1382 #include <sys/vfs.h>
1383 #include <sys/vfs.h>
1383 int main() { struct statfs s; return sizeof(s.f_type); }
1384 int main() { struct statfs s; return sizeof(s.f_type); }
1384 ''',
1385 ''',
1385 ),
1386 ),
1386 ]:
1387 ]:
1387 if re.search(plat, sys.platform) and cancompile(new_compiler(), code):
1388 if re.search(plat, sys.platform) and cancompile(new_compiler(), code):
1388 osutil_cflags.append('-DHAVE_%s' % macro)
1389 osutil_cflags.append('-DHAVE_%s' % macro)
1389
1390
1390 if sys.platform == 'darwin':
1391 if sys.platform == 'darwin':
1391 osutil_ldflags += ['-framework', 'ApplicationServices']
1392 osutil_ldflags += ['-framework', 'ApplicationServices']
1392
1393
1393 if sys.platform == 'sunos5':
1394 if sys.platform == 'sunos5':
1394 osutil_ldflags += ['-lsocket']
1395 osutil_ldflags += ['-lsocket']
1395
1396
1396 xdiff_srcs = [
1397 xdiff_srcs = [
1397 'mercurial/thirdparty/xdiff/xdiffi.c',
1398 'mercurial/thirdparty/xdiff/xdiffi.c',
1398 'mercurial/thirdparty/xdiff/xprepare.c',
1399 'mercurial/thirdparty/xdiff/xprepare.c',
1399 'mercurial/thirdparty/xdiff/xutils.c',
1400 'mercurial/thirdparty/xdiff/xutils.c',
1400 ]
1401 ]
1401
1402
1402 xdiff_headers = [
1403 xdiff_headers = [
1403 'mercurial/thirdparty/xdiff/xdiff.h',
1404 'mercurial/thirdparty/xdiff/xdiff.h',
1404 'mercurial/thirdparty/xdiff/xdiffi.h',
1405 'mercurial/thirdparty/xdiff/xdiffi.h',
1405 'mercurial/thirdparty/xdiff/xinclude.h',
1406 'mercurial/thirdparty/xdiff/xinclude.h',
1406 'mercurial/thirdparty/xdiff/xmacros.h',
1407 'mercurial/thirdparty/xdiff/xmacros.h',
1407 'mercurial/thirdparty/xdiff/xprepare.h',
1408 'mercurial/thirdparty/xdiff/xprepare.h',
1408 'mercurial/thirdparty/xdiff/xtypes.h',
1409 'mercurial/thirdparty/xdiff/xtypes.h',
1409 'mercurial/thirdparty/xdiff/xutils.h',
1410 'mercurial/thirdparty/xdiff/xutils.h',
1410 ]
1411 ]
1411
1412
1412
1413
1413 class RustCompilationError(CCompilerError):
1414 class RustCompilationError(CCompilerError):
1414 """Exception class for Rust compilation errors."""
1415 """Exception class for Rust compilation errors."""
1415
1416
1416
1417
1417 class RustExtension(Extension):
1418 class RustExtension(Extension):
1418 """Base classes for concrete Rust Extension classes."""
1419 """Base classes for concrete Rust Extension classes."""
1419
1420
1420 rusttargetdir = os.path.join('rust', 'target', 'release')
1421 rusttargetdir = os.path.join('rust', 'target', 'release')
1421
1422
1422 def __init__(self, mpath, sources, rustlibname, subcrate, **kw):
1423 def __init__(self, mpath, sources, rustlibname, subcrate, **kw):
1423 Extension.__init__(self, mpath, sources, **kw)
1424 Extension.__init__(self, mpath, sources, **kw)
1424 srcdir = self.rustsrcdir = os.path.join('rust', subcrate)
1425 srcdir = self.rustsrcdir = os.path.join('rust', subcrate)
1425
1426
1426 # adding Rust source and control files to depends so that the extension
1427 # adding Rust source and control files to depends so that the extension
1427 # gets rebuilt if they've changed
1428 # gets rebuilt if they've changed
1428 self.depends.append(os.path.join(srcdir, 'Cargo.toml'))
1429 self.depends.append(os.path.join(srcdir, 'Cargo.toml'))
1429 cargo_lock = os.path.join(srcdir, 'Cargo.lock')
1430 cargo_lock = os.path.join(srcdir, 'Cargo.lock')
1430 if os.path.exists(cargo_lock):
1431 if os.path.exists(cargo_lock):
1431 self.depends.append(cargo_lock)
1432 self.depends.append(cargo_lock)
1432 for dirpath, subdir, fnames in os.walk(os.path.join(srcdir, 'src')):
1433 for dirpath, subdir, fnames in os.walk(os.path.join(srcdir, 'src')):
1433 self.depends.extend(
1434 self.depends.extend(
1434 os.path.join(dirpath, fname)
1435 os.path.join(dirpath, fname)
1435 for fname in fnames
1436 for fname in fnames
1436 if os.path.splitext(fname)[1] == '.rs'
1437 if os.path.splitext(fname)[1] == '.rs'
1437 )
1438 )
1438
1439
1439 @staticmethod
1440 @staticmethod
1440 def rustdylibsuffix():
1441 def rustdylibsuffix():
1441 """Return the suffix for shared libraries produced by rustc.
1442 """Return the suffix for shared libraries produced by rustc.
1442
1443
1443 See also: https://doc.rust-lang.org/reference/linkage.html
1444 See also: https://doc.rust-lang.org/reference/linkage.html
1444 """
1445 """
1445 if sys.platform == 'darwin':
1446 if sys.platform == 'darwin':
1446 return '.dylib'
1447 return '.dylib'
1447 elif os.name == 'nt':
1448 elif os.name == 'nt':
1448 return '.dll'
1449 return '.dll'
1449 else:
1450 else:
1450 return '.so'
1451 return '.so'
1451
1452
1452 def rustbuild(self):
1453 def rustbuild(self):
1453 env = os.environ.copy()
1454 env = os.environ.copy()
1454 if 'HGTEST_RESTOREENV' in env:
1455 if 'HGTEST_RESTOREENV' in env:
1455 # Mercurial tests change HOME to a temporary directory,
1456 # Mercurial tests change HOME to a temporary directory,
1456 # but, if installed with rustup, the Rust toolchain needs
1457 # but, if installed with rustup, the Rust toolchain needs
1457 # HOME to be correct (otherwise the 'no default toolchain'
1458 # HOME to be correct (otherwise the 'no default toolchain'
1458 # error message is issued and the build fails).
1459 # error message is issued and the build fails).
1459 # This happens currently with test-hghave.t, which does
1460 # This happens currently with test-hghave.t, which does
1460 # invoke this build.
1461 # invoke this build.
1461
1462
1462 # Unix only fix (os.path.expanduser not really reliable if
1463 # Unix only fix (os.path.expanduser not really reliable if
1463 # HOME is shadowed like this)
1464 # HOME is shadowed like this)
1464 import pwd
1465 import pwd
1465
1466
1466 env['HOME'] = pwd.getpwuid(os.getuid()).pw_dir
1467 env['HOME'] = pwd.getpwuid(os.getuid()).pw_dir
1467
1468
1468 cargocmd = ['cargo', 'rustc', '--release']
1469 cargocmd = ['cargo', 'rustc', '--release']
1469
1470
1470 rust_features = env.get("HG_RUST_FEATURES")
1471 rust_features = env.get("HG_RUST_FEATURES")
1471 if rust_features:
1472 if rust_features:
1472 cargocmd.extend(('--features', rust_features))
1473 cargocmd.extend(('--features', rust_features))
1473
1474
1474 cargocmd.append('--')
1475 cargocmd.append('--')
1475 if sys.platform == 'darwin':
1476 if sys.platform == 'darwin':
1476 cargocmd.extend(
1477 cargocmd.extend(
1477 ("-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup")
1478 ("-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup")
1478 )
1479 )
1479 try:
1480 try:
1480 subprocess.check_call(cargocmd, env=env, cwd=self.rustsrcdir)
1481 subprocess.check_call(cargocmd, env=env, cwd=self.rustsrcdir)
1481 except FileNotFoundError:
1482 except FileNotFoundError:
1482 raise RustCompilationError("Cargo not found")
1483 raise RustCompilationError("Cargo not found")
1483 except PermissionError:
1484 except PermissionError:
1484 raise RustCompilationError(
1485 raise RustCompilationError(
1485 "Cargo found, but permission to execute it is denied"
1486 "Cargo found, but permission to execute it is denied"
1486 )
1487 )
1487 except subprocess.CalledProcessError:
1488 except subprocess.CalledProcessError:
1488 raise RustCompilationError(
1489 raise RustCompilationError(
1489 "Cargo failed. Working directory: %r, "
1490 "Cargo failed. Working directory: %r, "
1490 "command: %r, environment: %r"
1491 "command: %r, environment: %r"
1491 % (self.rustsrcdir, cargocmd, env)
1492 % (self.rustsrcdir, cargocmd, env)
1492 )
1493 )
1493
1494
1494
1495
1495 class RustStandaloneExtension(RustExtension):
1496 class RustStandaloneExtension(RustExtension):
1496 def __init__(self, pydottedname, rustcrate, dylibname, **kw):
1497 def __init__(self, pydottedname, rustcrate, dylibname, **kw):
1497 RustExtension.__init__(
1498 RustExtension.__init__(
1498 self, pydottedname, [], dylibname, rustcrate, **kw
1499 self, pydottedname, [], dylibname, rustcrate, **kw
1499 )
1500 )
1500 self.dylibname = dylibname
1501 self.dylibname = dylibname
1501
1502
1502 def build(self, target_dir):
1503 def build(self, target_dir):
1503 self.rustbuild()
1504 self.rustbuild()
1504 target = [target_dir]
1505 target = [target_dir]
1505 target.extend(self.name.split('.'))
1506 target.extend(self.name.split('.'))
1506 target[-1] += DYLIB_SUFFIX
1507 target[-1] += DYLIB_SUFFIX
1507 target = os.path.join(*target)
1508 target = os.path.join(*target)
1508 os.makedirs(os.path.dirname(target), exist_ok=True)
1509 os.makedirs(os.path.dirname(target), exist_ok=True)
1509 shutil.copy2(
1510 shutil.copy2(
1510 os.path.join(
1511 os.path.join(
1511 self.rusttargetdir, self.dylibname + self.rustdylibsuffix()
1512 self.rusttargetdir, self.dylibname + self.rustdylibsuffix()
1512 ),
1513 ),
1513 target,
1514 target,
1514 )
1515 )
1515
1516
1516
1517
1517 extmodules = [
1518 extmodules = [
1518 Extension(
1519 Extension(
1519 'mercurial.cext.base85',
1520 'mercurial.cext.base85',
1520 ['mercurial/cext/base85.c'],
1521 ['mercurial/cext/base85.c'],
1521 include_dirs=common_include_dirs,
1522 include_dirs=common_include_dirs,
1522 extra_compile_args=common_cflags,
1523 extra_compile_args=common_cflags,
1523 depends=common_depends,
1524 depends=common_depends,
1524 ),
1525 ),
1525 Extension(
1526 Extension(
1526 'mercurial.cext.bdiff',
1527 'mercurial.cext.bdiff',
1527 ['mercurial/bdiff.c', 'mercurial/cext/bdiff.c'] + xdiff_srcs,
1528 ['mercurial/bdiff.c', 'mercurial/cext/bdiff.c'] + xdiff_srcs,
1528 include_dirs=common_include_dirs,
1529 include_dirs=common_include_dirs,
1529 extra_compile_args=common_cflags,
1530 extra_compile_args=common_cflags,
1530 depends=common_depends + ['mercurial/bdiff.h'] + xdiff_headers,
1531 depends=common_depends + ['mercurial/bdiff.h'] + xdiff_headers,
1531 ),
1532 ),
1532 Extension(
1533 Extension(
1533 'mercurial.cext.mpatch',
1534 'mercurial.cext.mpatch',
1534 ['mercurial/mpatch.c', 'mercurial/cext/mpatch.c'],
1535 ['mercurial/mpatch.c', 'mercurial/cext/mpatch.c'],
1535 include_dirs=common_include_dirs,
1536 include_dirs=common_include_dirs,
1536 extra_compile_args=common_cflags,
1537 extra_compile_args=common_cflags,
1537 depends=common_depends,
1538 depends=common_depends,
1538 ),
1539 ),
1539 Extension(
1540 Extension(
1540 'mercurial.cext.parsers',
1541 'mercurial.cext.parsers',
1541 [
1542 [
1542 'mercurial/cext/charencode.c',
1543 'mercurial/cext/charencode.c',
1543 'mercurial/cext/dirs.c',
1544 'mercurial/cext/dirs.c',
1544 'mercurial/cext/manifest.c',
1545 'mercurial/cext/manifest.c',
1545 'mercurial/cext/parsers.c',
1546 'mercurial/cext/parsers.c',
1546 'mercurial/cext/pathencode.c',
1547 'mercurial/cext/pathencode.c',
1547 'mercurial/cext/revlog.c',
1548 'mercurial/cext/revlog.c',
1548 ],
1549 ],
1549 include_dirs=common_include_dirs,
1550 include_dirs=common_include_dirs,
1550 extra_compile_args=common_cflags,
1551 extra_compile_args=common_cflags,
1551 depends=common_depends
1552 depends=common_depends
1552 + [
1553 + [
1553 'mercurial/cext/charencode.h',
1554 'mercurial/cext/charencode.h',
1554 'mercurial/cext/revlog.h',
1555 'mercurial/cext/revlog.h',
1555 ],
1556 ],
1556 ),
1557 ),
1557 Extension(
1558 Extension(
1558 'mercurial.cext.osutil',
1559 'mercurial.cext.osutil',
1559 ['mercurial/cext/osutil.c'],
1560 ['mercurial/cext/osutil.c'],
1560 include_dirs=common_include_dirs,
1561 include_dirs=common_include_dirs,
1561 extra_compile_args=common_cflags + osutil_cflags,
1562 extra_compile_args=common_cflags + osutil_cflags,
1562 extra_link_args=osutil_ldflags,
1563 extra_link_args=osutil_ldflags,
1563 depends=common_depends,
1564 depends=common_depends,
1564 ),
1565 ),
1565 Extension(
1566 Extension(
1566 'mercurial.thirdparty.zope.interface._zope_interface_coptimizations',
1567 'mercurial.thirdparty.zope.interface._zope_interface_coptimizations',
1567 [
1568 [
1568 'mercurial/thirdparty/zope/interface/_zope_interface_coptimizations.c',
1569 'mercurial/thirdparty/zope/interface/_zope_interface_coptimizations.c',
1569 ],
1570 ],
1570 extra_compile_args=common_cflags,
1571 extra_compile_args=common_cflags,
1571 ),
1572 ),
1572 Extension(
1573 Extension(
1573 'mercurial.thirdparty.sha1dc',
1574 'mercurial.thirdparty.sha1dc',
1574 [
1575 [
1575 'mercurial/thirdparty/sha1dc/cext.c',
1576 'mercurial/thirdparty/sha1dc/cext.c',
1576 'mercurial/thirdparty/sha1dc/lib/sha1.c',
1577 'mercurial/thirdparty/sha1dc/lib/sha1.c',
1577 'mercurial/thirdparty/sha1dc/lib/ubc_check.c',
1578 'mercurial/thirdparty/sha1dc/lib/ubc_check.c',
1578 ],
1579 ],
1579 extra_compile_args=common_cflags,
1580 extra_compile_args=common_cflags,
1580 ),
1581 ),
1581 Extension(
1582 Extension(
1582 'hgext.fsmonitor.pywatchman.bser',
1583 'hgext.fsmonitor.pywatchman.bser',
1583 ['hgext/fsmonitor/pywatchman/bser.c'],
1584 ['hgext/fsmonitor/pywatchman/bser.c'],
1584 extra_compile_args=common_cflags,
1585 extra_compile_args=common_cflags,
1585 ),
1586 ),
1586 RustStandaloneExtension(
1587 RustStandaloneExtension(
1587 'mercurial.rustext',
1588 'mercurial.rustext',
1588 'hg-cpython',
1589 'hg-cpython',
1589 'librusthg',
1590 'librusthg',
1590 ),
1591 ),
1591 ]
1592 ]
1592
1593
1593
1594
1594 sys.path.insert(0, 'contrib/python-zstandard')
1595 sys.path.insert(0, 'contrib/python-zstandard')
1595 import setup_zstd
1596 import setup_zstd
1596
1597
1597 zstd = setup_zstd.get_c_extension(
1598 zstd = setup_zstd.get_c_extension(
1598 name='mercurial.zstd', root=os.path.abspath(os.path.dirname(__file__))
1599 name='mercurial.zstd', root=os.path.abspath(os.path.dirname(__file__))
1599 )
1600 )
1600 zstd.extra_compile_args += common_cflags
1601 zstd.extra_compile_args += common_cflags
1601 extmodules.append(zstd)
1602 extmodules.append(zstd)
1602
1603
1603 try:
1604 try:
1604 from distutils import cygwinccompiler
1605 from distutils import cygwinccompiler
1605
1606
1606 # the -mno-cygwin option has been deprecated for years
1607 # the -mno-cygwin option has been deprecated for years
1607 mingw32compilerclass = cygwinccompiler.Mingw32CCompiler
1608 mingw32compilerclass = cygwinccompiler.Mingw32CCompiler
1608
1609
1609 class HackedMingw32CCompiler(cygwinccompiler.Mingw32CCompiler):
1610 class HackedMingw32CCompiler(cygwinccompiler.Mingw32CCompiler):
1610 def __init__(self, *args, **kwargs):
1611 def __init__(self, *args, **kwargs):
1611 mingw32compilerclass.__init__(self, *args, **kwargs)
1612 mingw32compilerclass.__init__(self, *args, **kwargs)
1612 for i in 'compiler compiler_so linker_exe linker_so'.split():
1613 for i in 'compiler compiler_so linker_exe linker_so'.split():
1613 try:
1614 try:
1614 getattr(self, i).remove('-mno-cygwin')
1615 getattr(self, i).remove('-mno-cygwin')
1615 except ValueError:
1616 except ValueError:
1616 pass
1617 pass
1617
1618
1618 cygwinccompiler.Mingw32CCompiler = HackedMingw32CCompiler
1619 cygwinccompiler.Mingw32CCompiler = HackedMingw32CCompiler
1619 except ImportError:
1620 except ImportError:
1620 # the cygwinccompiler package is not available on some Python
1621 # the cygwinccompiler package is not available on some Python
1621 # distributions like the ones from the optware project for Synology
1622 # distributions like the ones from the optware project for Synology
1622 # DiskStation boxes
1623 # DiskStation boxes
1623 class HackedMingw32CCompiler:
1624 class HackedMingw32CCompiler:
1624 pass
1625 pass
1625
1626
1626
1627
1627 if os.name == 'nt':
1628 if os.name == 'nt':
1628 # Allow compiler/linker flags to be added to Visual Studio builds. Passing
1629 # Allow compiler/linker flags to be added to Visual Studio builds. Passing
1629 # extra_link_args to distutils.extensions.Extension() doesn't have any
1630 # extra_link_args to distutils.extensions.Extension() doesn't have any
1630 # effect.
1631 # effect.
1631 from distutils import msvccompiler
1632 from distutils import msvccompiler
1632
1633
1633 msvccompilerclass = msvccompiler.MSVCCompiler
1634 msvccompilerclass = msvccompiler.MSVCCompiler
1634
1635
1635 class HackedMSVCCompiler(msvccompiler.MSVCCompiler):
1636 class HackedMSVCCompiler(msvccompiler.MSVCCompiler):
1636 def initialize(self):
1637 def initialize(self):
1637 msvccompilerclass.initialize(self)
1638 msvccompilerclass.initialize(self)
1638 # "warning LNK4197: export 'func' specified multiple times"
1639 # "warning LNK4197: export 'func' specified multiple times"
1639 self.ldflags_shared.append('/ignore:4197')
1640 self.ldflags_shared.append('/ignore:4197')
1640 self.ldflags_shared_debug.append('/ignore:4197')
1641 self.ldflags_shared_debug.append('/ignore:4197')
1641
1642
1642 msvccompiler.MSVCCompiler = HackedMSVCCompiler
1643 msvccompiler.MSVCCompiler = HackedMSVCCompiler
1643
1644
1644 packagedata = {
1645 packagedata = {
1645 'mercurial': [
1646 'mercurial': [
1646 'locale/*/LC_MESSAGES/hg.mo',
1647 'locale/*/LC_MESSAGES/hg.mo',
1647 'dummycert.pem',
1648 'dummycert.pem',
1648 ],
1649 ],
1649 'mercurial.defaultrc': [
1650 'mercurial.defaultrc': [
1650 '*.rc',
1651 '*.rc',
1651 ],
1652 ],
1652 'mercurial.helptext': [
1653 'mercurial.helptext': [
1653 '*.txt',
1654 '*.txt',
1654 ],
1655 ],
1655 'mercurial.helptext.internals': [
1656 'mercurial.helptext.internals': [
1656 '*.txt',
1657 '*.txt',
1657 ],
1658 ],
1658 'mercurial.thirdparty.attr': [
1659 'mercurial.thirdparty.attr': [
1659 '*.pyi',
1660 '*.pyi',
1660 'py.typed',
1661 'py.typed',
1661 ],
1662 ],
1662 }
1663 }
1663
1664
1664
1665
1665 def ordinarypath(p):
1666 def ordinarypath(p):
1666 return p and p[0] != '.' and p[-1] != '~'
1667 return p and p[0] != '.' and p[-1] != '~'
1667
1668
1668
1669
1669 for root in ('templates',):
1670 for root in ('templates',):
1670 for curdir, dirs, files in os.walk(os.path.join('mercurial', root)):
1671 for curdir, dirs, files in os.walk(os.path.join('mercurial', root)):
1671 packagename = curdir.replace(os.sep, '.')
1672 packagename = curdir.replace(os.sep, '.')
1672 packagedata[packagename] = list(filter(ordinarypath, files))
1673 packagedata[packagename] = list(filter(ordinarypath, files))
1673
1674
1674 datafiles = []
1675 datafiles = []
1675
1676
1676 # distutils expects version to be str/unicode. Converting it to
1677 # distutils expects version to be str/unicode. Converting it to
1677 # unicode on Python 2 still works because it won't contain any
1678 # unicode on Python 2 still works because it won't contain any
1678 # non-ascii bytes and will be implicitly converted back to bytes
1679 # non-ascii bytes and will be implicitly converted back to bytes
1679 # when operated on.
1680 # when operated on.
1680 assert isinstance(version, str)
1681 assert isinstance(version, str)
1681 setupversion = version
1682 setupversion = version
1682
1683
1683 extra = {}
1684 extra = {}
1684
1685
1685 py2exepackages = [
1686 py2exepackages = [
1686 'hgdemandimport',
1687 'hgdemandimport',
1687 'hgext3rd',
1688 'hgext3rd',
1688 'hgext',
1689 'hgext',
1689 'email',
1690 'email',
1690 # implicitly imported per module policy
1691 # implicitly imported per module policy
1691 # (cffi wouldn't be used as a frozen exe)
1692 # (cffi wouldn't be used as a frozen exe)
1692 'mercurial.cext',
1693 'mercurial.cext',
1693 #'mercurial.cffi',
1694 #'mercurial.cffi',
1694 'mercurial.pure',
1695 'mercurial.pure',
1695 ]
1696 ]
1696
1697
1697 py2exe_includes = []
1698 py2exe_includes = []
1698
1699
1699 py2exeexcludes = []
1700 py2exeexcludes = []
1700 py2exedllexcludes = ['crypt32.dll']
1701 py2exedllexcludes = ['crypt32.dll']
1701
1702
1702 if issetuptools:
1703 if issetuptools:
1703 extra['python_requires'] = supportedpy
1704 extra['python_requires'] = supportedpy
1704
1705
1705 if py2exeloaded:
1706 if py2exeloaded:
1706 extra['console'] = [
1707 extra['console'] = [
1707 {
1708 {
1708 'script': 'hg',
1709 'script': 'hg',
1709 'copyright': 'Copyright (C) 2005-2023 Olivia Mackall and others',
1710 'copyright': 'Copyright (C) 2005-2023 Olivia Mackall and others',
1710 'product_version': version,
1711 'product_version': version,
1711 }
1712 }
1712 ]
1713 ]
1713 # Sub command of 'build' because 'py2exe' does not handle sub_commands.
1714 # Sub command of 'build' because 'py2exe' does not handle sub_commands.
1714 # Need to override hgbuild because it has a private copy of
1715 # Need to override hgbuild because it has a private copy of
1715 # build.sub_commands.
1716 # build.sub_commands.
1716 hgbuild.sub_commands.insert(0, ('build_hgextindex', None))
1717 hgbuild.sub_commands.insert(0, ('build_hgextindex', None))
1717 # put dlls in sub directory so that they won't pollute PATH
1718 # put dlls in sub directory so that they won't pollute PATH
1718 extra['zipfile'] = 'lib/library.zip'
1719 extra['zipfile'] = 'lib/library.zip'
1719
1720
1720 # We allow some configuration to be supplemented via environment
1721 # We allow some configuration to be supplemented via environment
1721 # variables. This is better than setup.cfg files because it allows
1722 # variables. This is better than setup.cfg files because it allows
1722 # supplementing configs instead of replacing them.
1723 # supplementing configs instead of replacing them.
1723 extrapackages = os.environ.get('HG_PY2EXE_EXTRA_PACKAGES')
1724 extrapackages = os.environ.get('HG_PY2EXE_EXTRA_PACKAGES')
1724 if extrapackages:
1725 if extrapackages:
1725 py2exepackages.extend(extrapackages.split(' '))
1726 py2exepackages.extend(extrapackages.split(' '))
1726
1727
1727 extra_includes = os.environ.get('HG_PY2EXE_EXTRA_INCLUDES')
1728 extra_includes = os.environ.get('HG_PY2EXE_EXTRA_INCLUDES')
1728 if extra_includes:
1729 if extra_includes:
1729 py2exe_includes.extend(extra_includes.split(' '))
1730 py2exe_includes.extend(extra_includes.split(' '))
1730
1731
1731 excludes = os.environ.get('HG_PY2EXE_EXTRA_EXCLUDES')
1732 excludes = os.environ.get('HG_PY2EXE_EXTRA_EXCLUDES')
1732 if excludes:
1733 if excludes:
1733 py2exeexcludes.extend(excludes.split(' '))
1734 py2exeexcludes.extend(excludes.split(' '))
1734
1735
1735 dllexcludes = os.environ.get('HG_PY2EXE_EXTRA_DLL_EXCLUDES')
1736 dllexcludes = os.environ.get('HG_PY2EXE_EXTRA_DLL_EXCLUDES')
1736 if dllexcludes:
1737 if dllexcludes:
1737 py2exedllexcludes.extend(dllexcludes.split(' '))
1738 py2exedllexcludes.extend(dllexcludes.split(' '))
1738
1739
1739 if os.environ.get('PYOXIDIZER'):
1740 if os.environ.get('PYOXIDIZER'):
1740 hgbuild.sub_commands.insert(0, ('build_hgextindex', None))
1741 hgbuild.sub_commands.insert(0, ('build_hgextindex', None))
1741
1742
1742 if os.name == 'nt':
1743 if os.name == 'nt':
1743 # Windows binary file versions for exe/dll files must have the
1744 # Windows binary file versions for exe/dll files must have the
1744 # form W.X.Y.Z, where W,X,Y,Z are numbers in the range 0..65535
1745 # form W.X.Y.Z, where W,X,Y,Z are numbers in the range 0..65535
1745 setupversion = setupversion.split(r'+', 1)[0]
1746 setupversion = setupversion.split(r'+', 1)[0]
1746
1747
1747 setup(
1748 setup(
1748 name='mercurial',
1749 name='mercurial',
1749 version=setupversion,
1750 version=setupversion,
1750 author='Olivia Mackall and many others',
1751 author='Olivia Mackall and many others',
1751 author_email='mercurial@mercurial-scm.org',
1752 author_email='mercurial@mercurial-scm.org',
1752 url='https://mercurial-scm.org/',
1753 url='https://mercurial-scm.org/',
1753 download_url='https://mercurial-scm.org/release/',
1754 download_url='https://mercurial-scm.org/release/',
1754 description=(
1755 description=(
1755 'Fast scalable distributed SCM (revision control, version '
1756 'Fast scalable distributed SCM (revision control, version '
1756 'control) system'
1757 'control) system'
1757 ),
1758 ),
1758 long_description=(
1759 long_description=(
1759 'Mercurial is a distributed SCM tool written in Python.'
1760 'Mercurial is a distributed SCM tool written in Python.'
1760 ' It is used by a number of large projects that require'
1761 ' It is used by a number of large projects that require'
1761 ' fast, reliable distributed revision control, such as '
1762 ' fast, reliable distributed revision control, such as '
1762 'Mozilla.'
1763 'Mozilla.'
1763 ),
1764 ),
1764 license='GNU GPLv2 or any later version',
1765 license='GNU GPLv2 or any later version',
1765 classifiers=[
1766 classifiers=[
1766 'Development Status :: 6 - Mature',
1767 'Development Status :: 6 - Mature',
1767 'Environment :: Console',
1768 'Environment :: Console',
1768 'Intended Audience :: Developers',
1769 'Intended Audience :: Developers',
1769 'Intended Audience :: System Administrators',
1770 'Intended Audience :: System Administrators',
1770 'License :: OSI Approved :: GNU General Public License (GPL)',
1771 'License :: OSI Approved :: GNU General Public License (GPL)',
1771 'Natural Language :: Danish',
1772 'Natural Language :: Danish',
1772 'Natural Language :: English',
1773 'Natural Language :: English',
1773 'Natural Language :: German',
1774 'Natural Language :: German',
1774 'Natural Language :: Italian',
1775 'Natural Language :: Italian',
1775 'Natural Language :: Japanese',
1776 'Natural Language :: Japanese',
1776 'Natural Language :: Portuguese (Brazilian)',
1777 'Natural Language :: Portuguese (Brazilian)',
1777 'Operating System :: Microsoft :: Windows',
1778 'Operating System :: Microsoft :: Windows',
1778 'Operating System :: OS Independent',
1779 'Operating System :: OS Independent',
1779 'Operating System :: POSIX',
1780 'Operating System :: POSIX',
1780 'Programming Language :: C',
1781 'Programming Language :: C',
1781 'Programming Language :: Python',
1782 'Programming Language :: Python',
1782 'Topic :: Software Development :: Version Control',
1783 'Topic :: Software Development :: Version Control',
1783 ],
1784 ],
1784 scripts=scripts,
1785 scripts=scripts,
1785 packages=packages,
1786 packages=packages,
1786 ext_modules=extmodules,
1787 ext_modules=extmodules,
1787 data_files=datafiles,
1788 data_files=datafiles,
1788 package_data=packagedata,
1789 package_data=packagedata,
1789 cmdclass=cmdclass,
1790 cmdclass=cmdclass,
1790 distclass=hgdist,
1791 distclass=hgdist,
1791 options={
1792 options={
1792 'py2exe': {
1793 'py2exe': {
1793 'bundle_files': 3,
1794 'bundle_files': 3,
1794 'dll_excludes': py2exedllexcludes,
1795 'dll_excludes': py2exedllexcludes,
1795 'includes': py2exe_includes,
1796 'includes': py2exe_includes,
1796 'excludes': py2exeexcludes,
1797 'excludes': py2exeexcludes,
1797 'packages': py2exepackages,
1798 'packages': py2exepackages,
1798 },
1799 },
1799 'bdist_mpkg': {
1800 'bdist_mpkg': {
1800 'zipdist': False,
1801 'zipdist': False,
1801 'license': 'COPYING',
1802 'license': 'COPYING',
1802 'readme': 'contrib/packaging/macosx/Readme.html',
1803 'readme': 'contrib/packaging/macosx/Readme.html',
1803 'welcome': 'contrib/packaging/macosx/Welcome.html',
1804 'welcome': 'contrib/packaging/macosx/Welcome.html',
1804 },
1805 },
1805 },
1806 },
1806 **extra
1807 **extra
1807 )
1808 )
General Comments 0
You need to be logged in to leave comments. Login now