relenv 0.21.2__py3-none-any.whl → 0.22.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- relenv/__init__.py +14 -2
- relenv/__main__.py +12 -6
- relenv/_resources/xz/config.h +148 -0
- relenv/_resources/xz/readme.md +4 -0
- relenv/build/__init__.py +28 -30
- relenv/build/common/__init__.py +50 -0
- relenv/build/common/_sysconfigdata_template.py +72 -0
- relenv/build/common/builder.py +907 -0
- relenv/build/common/builders.py +163 -0
- relenv/build/common/download.py +324 -0
- relenv/build/common/install.py +609 -0
- relenv/build/common/ui.py +432 -0
- relenv/build/darwin.py +128 -14
- relenv/build/linux.py +292 -74
- relenv/build/windows.py +123 -169
- relenv/buildenv.py +48 -17
- relenv/check.py +10 -5
- relenv/common.py +489 -165
- relenv/create.py +147 -7
- relenv/fetch.py +16 -4
- relenv/manifest.py +15 -7
- relenv/python-versions.json +329 -0
- relenv/pyversions.py +817 -30
- relenv/relocate.py +101 -55
- relenv/runtime.py +452 -282
- relenv/toolchain.py +9 -3
- {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/METADATA +1 -1
- relenv-0.22.0.dist-info/RECORD +48 -0
- tests/__init__.py +2 -0
- tests/_pytest_typing.py +45 -0
- tests/conftest.py +42 -36
- tests/test_build.py +426 -9
- tests/test_common.py +311 -48
- tests/test_create.py +149 -6
- tests/test_downloads.py +19 -15
- tests/test_fips_photon.py +6 -3
- tests/test_module_imports.py +44 -0
- tests/test_pyversions_runtime.py +177 -0
- tests/test_relocate.py +45 -39
- tests/test_relocate_module.py +257 -0
- tests/test_runtime.py +1802 -6
- tests/test_verify_build.py +477 -34
- relenv/build/common.py +0 -1707
- relenv-0.21.2.dist-info/RECORD +0 -35
- {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/WHEEL +0 -0
- {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/entry_points.txt +0 -0
- {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/licenses/LICENSE.md +0 -0
- {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/licenses/NOTICE +0 -0
- {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/top_level.txt +0 -0
relenv/common.py
CHANGED
|
@@ -1,24 +1,42 @@
|
|
|
1
|
-
# Copyright
|
|
2
|
-
# SPDX-License-Identifier: Apache-2
|
|
1
|
+
# Copyright 2022-2025 Broadcom.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
"""
|
|
4
4
|
Common classes and values used around relenv.
|
|
5
5
|
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
6
8
|
import http.client
|
|
9
|
+
import json
|
|
7
10
|
import logging
|
|
8
11
|
import os
|
|
9
12
|
import pathlib
|
|
10
13
|
import platform
|
|
11
14
|
import queue
|
|
12
15
|
import selectors
|
|
16
|
+
import shutil
|
|
13
17
|
import subprocess
|
|
14
18
|
import sys
|
|
15
19
|
import tarfile
|
|
16
20
|
import textwrap
|
|
17
21
|
import threading
|
|
18
22
|
import time
|
|
23
|
+
from typing import (
|
|
24
|
+
IO,
|
|
25
|
+
Any,
|
|
26
|
+
BinaryIO,
|
|
27
|
+
Callable,
|
|
28
|
+
Iterable,
|
|
29
|
+
Literal,
|
|
30
|
+
Mapping,
|
|
31
|
+
Optional,
|
|
32
|
+
Union,
|
|
33
|
+
cast,
|
|
34
|
+
)
|
|
19
35
|
|
|
20
36
|
# relenv package version
|
|
21
|
-
__version__ = "0.
|
|
37
|
+
__version__ = "0.22.0"
|
|
38
|
+
|
|
39
|
+
log = logging.getLogger(__name__)
|
|
22
40
|
|
|
23
41
|
MODULE_DIR = pathlib.Path(__file__).resolve().parent
|
|
24
42
|
|
|
@@ -30,6 +48,83 @@ DARWIN = "darwin"
|
|
|
30
48
|
|
|
31
49
|
MACOS_DEVELOPMENT_TARGET = "10.15"
|
|
32
50
|
|
|
51
|
+
TOOLCHAIN_CACHE_ENV = "RELENV_TOOLCHAIN_CACHE"
|
|
52
|
+
_TOOLCHAIN_MANIFEST = ".toolchain-manifest.json"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# 8 GiB archives are not unusual; stick to metadata to fingerprint them.
|
|
56
|
+
def _archive_metadata(path: pathlib.Path) -> dict[str, Union[str, int]]:
|
|
57
|
+
stat = path.stat()
|
|
58
|
+
return {
|
|
59
|
+
"archive": str(path.resolve()),
|
|
60
|
+
"size": stat.st_size,
|
|
61
|
+
"mtime": stat.st_mtime_ns,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _toolchain_cache_root() -> Optional[pathlib.Path]:
|
|
66
|
+
override = os.environ.get(TOOLCHAIN_CACHE_ENV)
|
|
67
|
+
if override:
|
|
68
|
+
if override.strip().lower() == "none":
|
|
69
|
+
return None
|
|
70
|
+
return pathlib.Path(override).expanduser()
|
|
71
|
+
cache_home = os.environ.get("XDG_CACHE_HOME")
|
|
72
|
+
if cache_home:
|
|
73
|
+
base = pathlib.Path(cache_home)
|
|
74
|
+
else:
|
|
75
|
+
base = pathlib.Path.home() / ".cache"
|
|
76
|
+
return base / "relenv" / "toolchains"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _toolchain_manifest_path(toolchain_path: pathlib.Path) -> pathlib.Path:
|
|
80
|
+
return toolchain_path / _TOOLCHAIN_MANIFEST
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _load_toolchain_manifest(path: pathlib.Path) -> Optional[Mapping[str, Any]]:
|
|
84
|
+
if not path.exists():
|
|
85
|
+
return None
|
|
86
|
+
try:
|
|
87
|
+
with path.open(encoding="utf-8") as handle:
|
|
88
|
+
data = json.load(handle)
|
|
89
|
+
except (OSError, json.JSONDecodeError):
|
|
90
|
+
return None
|
|
91
|
+
if not isinstance(data, dict):
|
|
92
|
+
return None
|
|
93
|
+
return data
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _manifest_matches(manifest: Mapping[str, Any], metadata: Mapping[str, Any]) -> bool:
|
|
97
|
+
return (
|
|
98
|
+
manifest.get("archive") == metadata.get("archive")
|
|
99
|
+
and manifest.get("size") == metadata.get("size")
|
|
100
|
+
and manifest.get("mtime") == metadata.get("mtime")
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _write_toolchain_manifest(
|
|
105
|
+
toolchain_path: pathlib.Path, metadata: Mapping[str, Any]
|
|
106
|
+
) -> None:
|
|
107
|
+
manifest_path = _toolchain_manifest_path(toolchain_path)
|
|
108
|
+
try:
|
|
109
|
+
with manifest_path.open("w", encoding="utf-8") as handle:
|
|
110
|
+
json.dump(metadata, handle, indent=2, sort_keys=True)
|
|
111
|
+
handle.write("\n")
|
|
112
|
+
except OSError as exc: # pragma: no cover - permissions edge cases
|
|
113
|
+
log.warning(
|
|
114
|
+
"Unable to persist toolchain manifest at %s: %s", manifest_path, exc
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def toolchain_root_dir() -> pathlib.Path:
|
|
119
|
+
"""Return the root directory used for cached toolchains."""
|
|
120
|
+
if sys.platform != "linux":
|
|
121
|
+
return DATA_DIR
|
|
122
|
+
root = _toolchain_cache_root()
|
|
123
|
+
if root is None:
|
|
124
|
+
return DATA_DIR / "toolchain"
|
|
125
|
+
return root
|
|
126
|
+
|
|
127
|
+
|
|
33
128
|
REQUEST_HEADERS = {"User-Agent": f"relenv {__version__}"}
|
|
34
129
|
|
|
35
130
|
CHECK_HOSTS = (
|
|
@@ -61,8 +156,9 @@ DATA_DIR = pathlib.Path(os.environ.get("RELENV_DATA", DEFAULT_DATA_DIR)).resolve
|
|
|
61
156
|
SHEBANG_TPL_LINUX = textwrap.dedent(
|
|
62
157
|
"""#!/bin/sh
|
|
63
158
|
"true" ''''
|
|
159
|
+
# shellcheck disable=SC2093
|
|
64
160
|
"exec" "$(dirname "$(readlink -f "$0")"){}" "$0" "$@"
|
|
65
|
-
'''
|
|
161
|
+
' '''
|
|
66
162
|
"""
|
|
67
163
|
)
|
|
68
164
|
|
|
@@ -82,8 +178,10 @@ do
|
|
|
82
178
|
done
|
|
83
179
|
PHYS_DIR=$(pwd -P)
|
|
84
180
|
REALPATH=$PHYS_DIR/$TARGET_FILE
|
|
181
|
+
# shellcheck disable=SC2093
|
|
85
182
|
"exec" "$(dirname "$REALPATH")"{} "$REALPATH" "$@"
|
|
86
|
-
'''
|
|
183
|
+
' '''
|
|
184
|
+
"""
|
|
87
185
|
)
|
|
88
186
|
|
|
89
187
|
if sys.platform == "linux":
|
|
@@ -92,23 +190,141 @@ else:
|
|
|
92
190
|
SHEBANG_TPL = SHEBANG_TPL_MACOS
|
|
93
191
|
|
|
94
192
|
|
|
95
|
-
log = logging.getLogger(__name__)
|
|
96
|
-
|
|
97
|
-
|
|
98
193
|
class RelenvException(Exception):
|
|
99
194
|
"""
|
|
100
195
|
Base class for exeptions generated from relenv.
|
|
101
196
|
"""
|
|
102
197
|
|
|
103
198
|
|
|
104
|
-
|
|
199
|
+
# Validation Errors
|
|
200
|
+
class ValidationError(RelenvException):
|
|
201
|
+
"""Base class for validation-related errors.
|
|
202
|
+
|
|
203
|
+
Raised when data validation fails (checksums, signatures, etc.).
|
|
204
|
+
This follows CPython's convention of having intermediate base classes
|
|
205
|
+
for related exception types.
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class ChecksumValidationError(ValidationError):
|
|
210
|
+
"""Raised when file checksum verification fails.
|
|
211
|
+
|
|
212
|
+
This typically indicates file corruption or tampering. The error message
|
|
213
|
+
should include the expected and actual checksums when available.
|
|
214
|
+
|
|
215
|
+
Example:
|
|
216
|
+
raise ChecksumValidationError(
|
|
217
|
+
f"Checksum mismatch for {filename}: "
|
|
218
|
+
f"expected {expected}, got {actual}"
|
|
219
|
+
)
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class SignatureValidationError(ValidationError):
|
|
224
|
+
"""Raised when GPG signature verification fails.
|
|
225
|
+
|
|
226
|
+
This indicates that a downloaded file's cryptographic signature
|
|
227
|
+
does not match the expected signature, suggesting tampering or
|
|
228
|
+
an incomplete download.
|
|
229
|
+
|
|
230
|
+
Example:
|
|
231
|
+
raise SignatureValidationError(
|
|
232
|
+
f"GPG signature verification failed for {filename}"
|
|
233
|
+
)
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# Download Errors
|
|
238
|
+
class DownloadError(RelenvException):
|
|
239
|
+
"""Raised when downloading a file from a URL fails.
|
|
240
|
+
|
|
241
|
+
This encompasses network errors, HTTP errors, and other issues
|
|
242
|
+
that prevent successfully retrieving a remote resource.
|
|
243
|
+
|
|
244
|
+
Example:
|
|
245
|
+
raise DownloadError(f"Failed to download {url}: {reason}")
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# Configuration Errors
|
|
250
|
+
class ConfigurationError(RelenvException):
|
|
251
|
+
"""Raised when required configuration is missing or invalid.
|
|
252
|
+
|
|
253
|
+
This typically occurs during build setup when recipes are incomplete
|
|
254
|
+
or environment variables are not properly set.
|
|
255
|
+
|
|
256
|
+
Example:
|
|
257
|
+
raise ConfigurationError(
|
|
258
|
+
"Python recipe is missing download configuration"
|
|
259
|
+
)
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# Platform Errors
|
|
264
|
+
class PlatformError(RelenvException):
|
|
265
|
+
"""Raised when operating on an unsupported platform.
|
|
266
|
+
|
|
267
|
+
Relenv supports Linux, macOS, and Windows. This exception is raised
|
|
268
|
+
when attempting operations that are platform-specific or when running
|
|
269
|
+
on an unsupported platform.
|
|
270
|
+
|
|
271
|
+
Example:
|
|
272
|
+
raise PlatformError(f"Unsupported platform: {sys.platform}")
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# Build Errors
|
|
277
|
+
class BuildCommandError(RelenvException):
|
|
278
|
+
"""Raised when a build command execution fails.
|
|
279
|
+
|
|
280
|
+
This indicates that a subprocess (compiler, linker, etc.) returned
|
|
281
|
+
a non-zero exit code during the build process.
|
|
282
|
+
|
|
283
|
+
Example:
|
|
284
|
+
raise BuildCommandError(
|
|
285
|
+
f"Build command failed: {' '.join(cmd)}"
|
|
286
|
+
)
|
|
287
|
+
"""
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class MissingDependencyError(RelenvException):
|
|
291
|
+
"""Raised when a required build dependency is not found.
|
|
292
|
+
|
|
293
|
+
This typically occurs when expected files, directories, or system
|
|
294
|
+
packages are missing from the build environment.
|
|
295
|
+
|
|
296
|
+
Example:
|
|
297
|
+
raise MissingDependencyError(
|
|
298
|
+
f"Unable to locate {dependency_name}"
|
|
299
|
+
)
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# Environment Errors
|
|
304
|
+
class RelenvEnvironmentError(RelenvException):
|
|
305
|
+
"""Raised when there are issues with the relenv environment.
|
|
306
|
+
|
|
307
|
+
This occurs when operations require being inside a relenv environment
|
|
308
|
+
but the current environment is not properly configured.
|
|
309
|
+
|
|
310
|
+
Example:
|
|
311
|
+
raise RelenvEnvironmentError(
|
|
312
|
+
"Not in a relenv environment"
|
|
313
|
+
)
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def format_shebang(python: str, tpl: str = SHEBANG_TPL) -> str:
|
|
105
318
|
"""
|
|
106
319
|
Return a formatted shebang.
|
|
107
320
|
"""
|
|
108
|
-
|
|
321
|
+
shebang = tpl.format(python).strip()
|
|
322
|
+
if shebang.endswith("'''"):
|
|
323
|
+
return shebang + "\n\n"
|
|
324
|
+
return shebang + "\n"
|
|
109
325
|
|
|
110
326
|
|
|
111
|
-
def build_arch():
|
|
327
|
+
def build_arch() -> str:
|
|
112
328
|
"""
|
|
113
329
|
Return the current machine.
|
|
114
330
|
"""
|
|
@@ -116,7 +332,9 @@ def build_arch():
|
|
|
116
332
|
return machine.lower()
|
|
117
333
|
|
|
118
334
|
|
|
119
|
-
def work_root(
|
|
335
|
+
def work_root(
|
|
336
|
+
root: Optional[Union[str, os.PathLike[str]]] = None,
|
|
337
|
+
) -> pathlib.Path:
|
|
120
338
|
"""
|
|
121
339
|
Get the root directory that all other relenv working directories should be based on.
|
|
122
340
|
|
|
@@ -133,7 +351,9 @@ def work_root(root=None):
|
|
|
133
351
|
return base
|
|
134
352
|
|
|
135
353
|
|
|
136
|
-
def work_dir(
|
|
354
|
+
def work_dir(
|
|
355
|
+
name: str, root: Optional[Union[str, os.PathLike[str]]] = None
|
|
356
|
+
) -> pathlib.Path:
|
|
137
357
|
"""
|
|
138
358
|
Get the absolute path to the relenv working directory of the given name.
|
|
139
359
|
|
|
@@ -161,17 +381,17 @@ class WorkDirs:
|
|
|
161
381
|
:type root: str
|
|
162
382
|
"""
|
|
163
383
|
|
|
164
|
-
def __init__(self, root):
|
|
165
|
-
self.root = root
|
|
166
|
-
self.data = DATA_DIR
|
|
167
|
-
self.toolchain_config = work_dir("toolchain", self.root)
|
|
168
|
-
self.toolchain =
|
|
169
|
-
self.build = work_dir("build", DATA_DIR)
|
|
170
|
-
self.src = work_dir("src", DATA_DIR)
|
|
171
|
-
self.logs = work_dir("logs", DATA_DIR)
|
|
172
|
-
self.download = work_dir("download", DATA_DIR)
|
|
173
|
-
|
|
174
|
-
def __getstate__(self):
|
|
384
|
+
def __init__(self: "WorkDirs", root: Union[str, os.PathLike[str]]) -> None:
|
|
385
|
+
self.root: pathlib.Path = pathlib.Path(root)
|
|
386
|
+
self.data: pathlib.Path = DATA_DIR
|
|
387
|
+
self.toolchain_config: pathlib.Path = work_dir("toolchain", self.root)
|
|
388
|
+
self.toolchain: pathlib.Path = toolchain_root_dir()
|
|
389
|
+
self.build: pathlib.Path = work_dir("build", DATA_DIR)
|
|
390
|
+
self.src: pathlib.Path = work_dir("src", DATA_DIR)
|
|
391
|
+
self.logs: pathlib.Path = work_dir("logs", DATA_DIR)
|
|
392
|
+
self.download: pathlib.Path = work_dir("download", DATA_DIR)
|
|
393
|
+
|
|
394
|
+
def __getstate__(self: "WorkDirs") -> dict[str, pathlib.Path]:
|
|
175
395
|
"""
|
|
176
396
|
Return an object used for pickling.
|
|
177
397
|
|
|
@@ -187,7 +407,7 @@ class WorkDirs:
|
|
|
187
407
|
"download": self.download,
|
|
188
408
|
}
|
|
189
409
|
|
|
190
|
-
def __setstate__(self, state):
|
|
410
|
+
def __setstate__(self: "WorkDirs", state: Mapping[str, pathlib.Path]) -> None:
|
|
191
411
|
"""
|
|
192
412
|
Unwrap the object returned from unpickling.
|
|
193
413
|
|
|
@@ -203,7 +423,9 @@ class WorkDirs:
|
|
|
203
423
|
self.download = state["download"]
|
|
204
424
|
|
|
205
425
|
|
|
206
|
-
def work_dirs(
|
|
426
|
+
def work_dirs(
|
|
427
|
+
root: Optional[Union[str, os.PathLike[str]]] = None,
|
|
428
|
+
) -> WorkDirs:
|
|
207
429
|
"""
|
|
208
430
|
Returns a WorkDirs instance based on the given root.
|
|
209
431
|
|
|
@@ -216,41 +438,76 @@ def work_dirs(root=None):
|
|
|
216
438
|
return WorkDirs(work_root(root))
|
|
217
439
|
|
|
218
440
|
|
|
219
|
-
def get_toolchain(
|
|
441
|
+
def get_toolchain(
|
|
442
|
+
arch: Optional[str] = None,
|
|
443
|
+
root: Optional[Union[str, os.PathLike[str]]] = None,
|
|
444
|
+
) -> Optional[pathlib.Path]:
|
|
220
445
|
"""
|
|
221
446
|
Get a the toolchain directory, specific to the arch if supplied.
|
|
222
447
|
|
|
448
|
+
On Linux, this function will extract the toolchain from ppbt if needed.
|
|
449
|
+
If the toolchain already exists, it will be returned even if ppbt is
|
|
450
|
+
not available (e.g., when running tests on non-Linux platforms that
|
|
451
|
+
patch sys.platform to "linux"). This allows using existing toolchains
|
|
452
|
+
without requiring ppbt to be installed.
|
|
453
|
+
|
|
223
454
|
:param arch: The architecture to get the toolchain for
|
|
224
455
|
:type arch: str
|
|
225
456
|
:param root: The root of the relenv working directories to search in
|
|
226
457
|
:type root: str
|
|
227
458
|
|
|
228
|
-
:return: The directory holding the toolchain
|
|
459
|
+
:return: The directory holding the toolchain, or None if on Linux and
|
|
460
|
+
the toolchain doesn't exist and ppbt is unavailable
|
|
229
461
|
:rtype: ``pathlib.Path``
|
|
230
462
|
"""
|
|
463
|
+
del root # Kept for backward compatibility; location driven by DATA_DIR
|
|
231
464
|
os.makedirs(DATA_DIR, exist_ok=True)
|
|
232
465
|
if sys.platform != "linux":
|
|
233
|
-
return
|
|
234
|
-
|
|
235
|
-
TOOLCHAIN_ROOT = DATA_DIR / "toolchain"
|
|
236
|
-
TOOLCHAIN_PATH = TOOLCHAIN_ROOT / get_triplet()
|
|
237
|
-
if TOOLCHAIN_PATH.exists():
|
|
238
|
-
return TOOLCHAIN_PATH
|
|
466
|
+
return toolchain_root_dir()
|
|
239
467
|
|
|
240
|
-
|
|
468
|
+
toolchain_root = toolchain_root_dir()
|
|
469
|
+
triplet = get_triplet(machine=arch)
|
|
470
|
+
toolchain_path = toolchain_root / triplet
|
|
471
|
+
metadata: Optional[Mapping[str, Any]] = None
|
|
472
|
+
if toolchain_path.exists():
|
|
473
|
+
metadata = _load_toolchain_manifest(_toolchain_manifest_path(toolchain_path))
|
|
241
474
|
|
|
242
475
|
try:
|
|
243
|
-
import
|
|
476
|
+
from importlib import import_module
|
|
477
|
+
|
|
478
|
+
ppbt_common = import_module("ppbt.common")
|
|
244
479
|
except ImportError:
|
|
245
|
-
|
|
480
|
+
# If toolchain already exists, use it even without ppbt
|
|
481
|
+
return toolchain_path if toolchain_path.exists() else None
|
|
482
|
+
archive_attr = getattr(ppbt_common, "ARCHIVE", None)
|
|
483
|
+
extract = getattr(ppbt_common, "extract_archive", None)
|
|
484
|
+
if archive_attr is None or not callable(extract):
|
|
485
|
+
raise RelenvException("ppbt.common missing ARCHIVE or extract_archive")
|
|
486
|
+
|
|
487
|
+
toolchain_root.mkdir(parents=True, exist_ok=True)
|
|
488
|
+
archive_path = pathlib.Path(archive_attr)
|
|
489
|
+
archive_meta = _archive_metadata(archive_path)
|
|
490
|
+
|
|
491
|
+
if (
|
|
492
|
+
toolchain_path.exists()
|
|
493
|
+
and metadata
|
|
494
|
+
and _manifest_matches(metadata, archive_meta)
|
|
495
|
+
):
|
|
496
|
+
return toolchain_path
|
|
246
497
|
|
|
247
|
-
if
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
498
|
+
if toolchain_path.exists():
|
|
499
|
+
shutil.rmtree(toolchain_path)
|
|
500
|
+
|
|
501
|
+
extract(str(toolchain_root), str(archive_path))
|
|
502
|
+
if not toolchain_path.exists():
|
|
503
|
+
raise RelenvException(
|
|
504
|
+
f"Toolchain archive {archive_path} did not produce {toolchain_path}"
|
|
505
|
+
)
|
|
506
|
+
_write_toolchain_manifest(toolchain_path, archive_meta)
|
|
507
|
+
return toolchain_path
|
|
251
508
|
|
|
252
509
|
|
|
253
|
-
def get_triplet(machine=None, plat=None):
|
|
510
|
+
def get_triplet(machine: Optional[str] = None, plat: Optional[str] = None) -> str:
|
|
254
511
|
"""
|
|
255
512
|
Get the target triplet for the specified machine and platform.
|
|
256
513
|
|
|
@@ -277,10 +534,10 @@ def get_triplet(machine=None, plat=None):
|
|
|
277
534
|
elif plat == "linux":
|
|
278
535
|
return f"{machine}-linux-gnu"
|
|
279
536
|
else:
|
|
280
|
-
raise
|
|
537
|
+
raise PlatformError(f"Unknown platform {plat}")
|
|
281
538
|
|
|
282
539
|
|
|
283
|
-
def plat_from_triplet(plat):
|
|
540
|
+
def plat_from_triplet(plat: str) -> str:
|
|
284
541
|
"""
|
|
285
542
|
Convert platform from build to the value of sys.platform.
|
|
286
543
|
"""
|
|
@@ -290,26 +547,27 @@ def plat_from_triplet(plat):
|
|
|
290
547
|
return "darwin"
|
|
291
548
|
elif plat == "win":
|
|
292
549
|
return "win32"
|
|
293
|
-
raise
|
|
550
|
+
raise PlatformError(f"Unkown platform {plat}")
|
|
294
551
|
|
|
295
552
|
|
|
296
|
-
def list_archived_builds():
|
|
553
|
+
def list_archived_builds() -> list[tuple[str, str, str]]:
|
|
297
554
|
"""
|
|
298
555
|
Return a list of version, architecture and platforms for builds.
|
|
299
556
|
"""
|
|
300
|
-
builds = []
|
|
301
|
-
|
|
302
|
-
for
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
557
|
+
builds: list[tuple[str, str, str]] = []
|
|
558
|
+
working_dirs = work_dirs(DATA_DIR)
|
|
559
|
+
for root_dir, dirnames, filenames in os.walk(working_dirs.build):
|
|
560
|
+
del dirnames # unused
|
|
561
|
+
for filename in filenames:
|
|
562
|
+
if filename.endswith(".tar.xz"):
|
|
563
|
+
base_name = filename[:-7]
|
|
564
|
+
version, triplet = base_name.split("-", 1)
|
|
307
565
|
arch, plat = triplet.split("-", 1)
|
|
308
566
|
builds.append((version, arch, plat))
|
|
309
567
|
return builds
|
|
310
568
|
|
|
311
569
|
|
|
312
|
-
def archived_build(triplet=None):
|
|
570
|
+
def archived_build(triplet: Optional[str] = None) -> pathlib.Path:
|
|
313
571
|
"""
|
|
314
572
|
Finds a the location of an archived build.
|
|
315
573
|
|
|
@@ -326,7 +584,9 @@ def archived_build(triplet=None):
|
|
|
326
584
|
return dirs.build / archive
|
|
327
585
|
|
|
328
586
|
|
|
329
|
-
def extract_archive(
|
|
587
|
+
def extract_archive(
|
|
588
|
+
to_dir: Union[str, os.PathLike[str]], archive: Union[str, os.PathLike[str]]
|
|
589
|
+
) -> None:
|
|
330
590
|
"""
|
|
331
591
|
Extract an archive to a specific location.
|
|
332
592
|
|
|
@@ -335,26 +595,30 @@ def extract_archive(to_dir, archive):
|
|
|
335
595
|
:param archive: The archive to extract
|
|
336
596
|
:type archive: str
|
|
337
597
|
"""
|
|
338
|
-
|
|
598
|
+
archive_path = pathlib.Path(archive)
|
|
599
|
+
archive_str = str(archive_path)
|
|
600
|
+
to_path = pathlib.Path(to_dir)
|
|
601
|
+
TarReadMode = Literal["r:gz", "r:xz", "r:bz2", "r"]
|
|
602
|
+
read_type: TarReadMode = "r"
|
|
603
|
+
if archive_str.endswith(".tgz"):
|
|
339
604
|
log.debug("Found tgz archive")
|
|
340
605
|
read_type = "r:gz"
|
|
341
|
-
elif
|
|
606
|
+
elif archive_str.endswith(".tar.gz"):
|
|
342
607
|
log.debug("Found tar.gz archive")
|
|
343
608
|
read_type = "r:gz"
|
|
344
|
-
elif
|
|
609
|
+
elif archive_str.endswith(".xz"):
|
|
345
610
|
log.debug("Found xz archive")
|
|
346
611
|
read_type = "r:xz"
|
|
347
|
-
elif
|
|
612
|
+
elif archive_str.endswith(".bz2"):
|
|
348
613
|
log.debug("Found bz2 archive")
|
|
349
614
|
read_type = "r:bz2"
|
|
350
615
|
else:
|
|
351
|
-
log.warning("Found unknown archive type: %s",
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
t.extractall(to_dir)
|
|
616
|
+
log.warning("Found unknown archive type: %s", archive_path)
|
|
617
|
+
with tarfile.open(str(archive_path), mode=read_type) as tar:
|
|
618
|
+
tar.extractall(str(to_path))
|
|
355
619
|
|
|
356
620
|
|
|
357
|
-
def get_download_location(url, dest):
|
|
621
|
+
def get_download_location(url: str, dest: Union[str, os.PathLike[str]]) -> str:
|
|
358
622
|
"""
|
|
359
623
|
Get the full path to where the url will be downloaded to.
|
|
360
624
|
|
|
@@ -366,10 +630,10 @@ def get_download_location(url, dest):
|
|
|
366
630
|
:return: The path to where the url will be downloaded to
|
|
367
631
|
:rtype: str
|
|
368
632
|
"""
|
|
369
|
-
return os.path.join(dest, os.path.basename(url))
|
|
633
|
+
return os.path.join(os.fspath(dest), os.path.basename(url))
|
|
370
634
|
|
|
371
635
|
|
|
372
|
-
def check_url(url, timestamp=None, timeout=30):
|
|
636
|
+
def check_url(url: str, timestamp: Optional[float] = None, timeout: float = 30) -> bool:
|
|
373
637
|
"""
|
|
374
638
|
Check that the url returns a 200.
|
|
375
639
|
"""
|
|
@@ -400,54 +664,79 @@ def check_url(url, timestamp=None, timeout=30):
|
|
|
400
664
|
return True
|
|
401
665
|
|
|
402
666
|
|
|
403
|
-
def fetch_url(
|
|
667
|
+
def fetch_url(
|
|
668
|
+
url: str,
|
|
669
|
+
fp: BinaryIO,
|
|
670
|
+
backoff: int = 3,
|
|
671
|
+
timeout: float = 30,
|
|
672
|
+
progress_callback: Optional[Callable[[int, int], None]] = None,
|
|
673
|
+
) -> None:
|
|
404
674
|
"""
|
|
405
675
|
Fetch the contents of a url.
|
|
406
676
|
|
|
407
677
|
This method will store the contents in the given file like object.
|
|
678
|
+
|
|
679
|
+
:param progress_callback: Optional callback(downloaded_bytes, total_bytes)
|
|
680
|
+
:type progress_callback: Optional[Callable[[int, int], None]]
|
|
408
681
|
"""
|
|
409
682
|
# Late import so we do not import hashlib before runtime.bootstrap is called.
|
|
410
683
|
import urllib.error
|
|
411
684
|
import urllib.request
|
|
412
685
|
|
|
413
686
|
last = time.time()
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
while n < backoff:
|
|
418
|
-
n += 1
|
|
687
|
+
attempts = max(backoff, 1)
|
|
688
|
+
response: http.client.HTTPResponse | None = None
|
|
689
|
+
for attempt in range(1, attempts + 1):
|
|
419
690
|
try:
|
|
420
|
-
|
|
691
|
+
response = urllib.request.urlopen(url, timeout=timeout)
|
|
421
692
|
break
|
|
422
693
|
except (
|
|
423
694
|
urllib.error.HTTPError,
|
|
424
695
|
urllib.error.URLError,
|
|
425
696
|
http.client.RemoteDisconnected,
|
|
426
697
|
) as exc:
|
|
427
|
-
if
|
|
698
|
+
if attempt >= attempts:
|
|
428
699
|
raise RelenvException(f"Error fetching url {url} {exc}")
|
|
429
700
|
log.debug("Unable to connect %s", url)
|
|
430
|
-
time.sleep(
|
|
431
|
-
|
|
432
|
-
raise RelenvException(f"
|
|
701
|
+
time.sleep(attempt * 10)
|
|
702
|
+
if response is None:
|
|
703
|
+
raise RelenvException(f"Unable to open url {url}")
|
|
433
704
|
log.info("url opened %s", url)
|
|
705
|
+
|
|
706
|
+
# Get content length from headers
|
|
707
|
+
content_length = 0
|
|
434
708
|
try:
|
|
435
|
-
|
|
709
|
+
content_length_str = response.headers.get("Content-Length")
|
|
710
|
+
if content_length_str:
|
|
711
|
+
content_length = int(content_length_str)
|
|
712
|
+
log.info("Content-Length: %d bytes", content_length)
|
|
713
|
+
# Report initial state to callback
|
|
714
|
+
if progress_callback:
|
|
715
|
+
progress_callback(0, content_length)
|
|
716
|
+
except (ValueError, TypeError):
|
|
717
|
+
log.debug("Could not parse Content-Length header")
|
|
718
|
+
|
|
719
|
+
try:
|
|
720
|
+
downloaded = 0
|
|
436
721
|
size = 1024 * 300
|
|
437
|
-
block =
|
|
722
|
+
block = response.read(size)
|
|
438
723
|
while block:
|
|
439
|
-
|
|
724
|
+
block_size = len(block)
|
|
725
|
+
downloaded += block_size
|
|
440
726
|
if time.time() - last > 10:
|
|
441
|
-
log.info("%s > %d", url,
|
|
727
|
+
log.info("%s > %d", url, downloaded)
|
|
442
728
|
last = time.time()
|
|
443
729
|
fp.write(block)
|
|
444
|
-
|
|
730
|
+
# Report progress
|
|
731
|
+
if progress_callback and content_length > 0:
|
|
732
|
+
progress_callback(downloaded, content_length)
|
|
733
|
+
block = response.read(10240)
|
|
445
734
|
finally:
|
|
446
|
-
|
|
735
|
+
response.close()
|
|
447
736
|
log.info("Download complete %s", url)
|
|
448
737
|
|
|
449
738
|
|
|
450
|
-
def fetch_url_content(url, backoff=3, timeout=30):
|
|
739
|
+
def fetch_url_content(url: str, backoff: int = 3, timeout: float = 30) -> str:
|
|
451
740
|
"""
|
|
452
741
|
Fetch the contents of a url.
|
|
453
742
|
|
|
@@ -455,55 +744,47 @@ def fetch_url_content(url, backoff=3, timeout=30):
|
|
|
455
744
|
"""
|
|
456
745
|
# Late import so we do not import hashlib before runtime.bootstrap is called.
|
|
457
746
|
import gzip
|
|
458
|
-
import io
|
|
459
747
|
import urllib.error
|
|
460
748
|
import urllib.request
|
|
461
749
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
if backoff < 1:
|
|
466
|
-
backoff = 1
|
|
467
|
-
n = 0
|
|
468
|
-
while n < backoff:
|
|
469
|
-
n += 1
|
|
750
|
+
attempts = max(backoff, 1)
|
|
751
|
+
response: http.client.HTTPResponse | None = None
|
|
752
|
+
for attempt in range(1, attempts + 1):
|
|
470
753
|
try:
|
|
471
|
-
|
|
754
|
+
response = urllib.request.urlopen(url, timeout=timeout)
|
|
755
|
+
break
|
|
472
756
|
except (
|
|
473
757
|
urllib.error.HTTPError,
|
|
474
758
|
urllib.error.URLError,
|
|
475
759
|
http.client.RemoteDisconnected,
|
|
476
760
|
) as exc:
|
|
477
|
-
if
|
|
761
|
+
if attempt >= attempts:
|
|
478
762
|
raise RelenvException(f"Error fetching url {url} {exc}")
|
|
479
763
|
log.debug("Unable to connect %s", url)
|
|
480
|
-
time.sleep(
|
|
764
|
+
time.sleep(attempt * 10)
|
|
765
|
+
if response is None:
|
|
766
|
+
raise RelenvException(f"Unable to open url {url}")
|
|
481
767
|
log.info("url opened %s", url)
|
|
482
768
|
try:
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
block = fin.read(size)
|
|
486
|
-
while block:
|
|
487
|
-
total += size
|
|
488
|
-
if time.time() - last > 10:
|
|
489
|
-
log.info("%s > %d", url, total)
|
|
490
|
-
last = time.time()
|
|
491
|
-
fp.write(block)
|
|
492
|
-
block = fin.read(10240)
|
|
769
|
+
data = response.read()
|
|
770
|
+
encoding = response.headers.get("content-encoding", "").lower()
|
|
493
771
|
finally:
|
|
494
|
-
|
|
495
|
-
|
|
772
|
+
response.close()
|
|
773
|
+
if encoding == "gzip":
|
|
774
|
+
log.debug("Found gzipped content")
|
|
775
|
+
data = gzip.decompress(data)
|
|
496
776
|
log.info("Download complete %s", url)
|
|
497
|
-
|
|
498
|
-
info = fin.info()
|
|
499
|
-
if "content-encoding" in info:
|
|
500
|
-
if info["content-encoding"] == "gzip":
|
|
501
|
-
log.debug("Found gzipped content")
|
|
502
|
-
fp = gzip.GzipFile(fileobj=fp)
|
|
503
|
-
return fp.read().decode()
|
|
777
|
+
return data.decode()
|
|
504
778
|
|
|
505
779
|
|
|
506
|
-
def download_url(
|
|
780
|
+
def download_url(
|
|
781
|
+
url: str,
|
|
782
|
+
dest: Union[str, os.PathLike[str]],
|
|
783
|
+
verbose: bool = True,
|
|
784
|
+
backoff: int = 3,
|
|
785
|
+
timeout: float = 60,
|
|
786
|
+
progress_callback: Optional[Callable[[int, int], None]] = None,
|
|
787
|
+
) -> str:
|
|
507
788
|
"""
|
|
508
789
|
Download the url to the provided destination.
|
|
509
790
|
|
|
@@ -515,6 +796,8 @@ def download_url(url, dest, verbose=True, backoff=3, timeout=60):
|
|
|
515
796
|
:type dest: str
|
|
516
797
|
:param verbose: Print download url and destination to stdout
|
|
517
798
|
:type verbose: bool
|
|
799
|
+
:param progress_callback: Optional callback(downloaded_bytes, total_bytes)
|
|
800
|
+
:type progress_callback: Optional[Callable[[int, int], None]]
|
|
518
801
|
|
|
519
802
|
:raises urllib.error.HTTPError: If the url was unable to be downloaded
|
|
520
803
|
|
|
@@ -524,9 +807,9 @@ def download_url(url, dest, verbose=True, backoff=3, timeout=60):
|
|
|
524
807
|
local = get_download_location(url, dest)
|
|
525
808
|
if verbose:
|
|
526
809
|
log.debug(f"Downloading {url} -> {local}")
|
|
527
|
-
fout = open(local, "wb")
|
|
528
810
|
try:
|
|
529
|
-
|
|
811
|
+
with open(local, "wb") as fout:
|
|
812
|
+
fetch_url(url, fout, backoff, timeout, progress_callback)
|
|
530
813
|
except Exception as exc:
|
|
531
814
|
if verbose:
|
|
532
815
|
log.error("Unable to download: %s\n%s", url, exc)
|
|
@@ -536,12 +819,11 @@ def download_url(url, dest, verbose=True, backoff=3, timeout=60):
|
|
|
536
819
|
pass
|
|
537
820
|
raise
|
|
538
821
|
finally:
|
|
539
|
-
fout.close()
|
|
540
822
|
log.debug(f"Finished downloading {url} -> {local}")
|
|
541
823
|
return local
|
|
542
824
|
|
|
543
825
|
|
|
544
|
-
def runcmd(*args, **kwargs):
|
|
826
|
+
def runcmd(*args: Any, **kwargs: Any) -> subprocess.Popen[str]:
|
|
545
827
|
"""
|
|
546
828
|
Run a command.
|
|
547
829
|
|
|
@@ -553,7 +835,9 @@ def runcmd(*args, **kwargs):
|
|
|
553
835
|
|
|
554
836
|
:raises RelenvException: If the command finishes with a non zero exit code
|
|
555
837
|
"""
|
|
556
|
-
|
|
838
|
+
if not args:
|
|
839
|
+
raise RelenvException("No command provided to runcmd")
|
|
840
|
+
log.debug("Running command: %s", " ".join(map(str, args[0])))
|
|
557
841
|
# if "stdout" not in kwargs:
|
|
558
842
|
kwargs["stdout"] = subprocess.PIPE
|
|
559
843
|
# if "stderr" not in kwargs:
|
|
@@ -563,44 +847,63 @@ def runcmd(*args, **kwargs):
|
|
|
563
847
|
if sys.platform != "win32":
|
|
564
848
|
|
|
565
849
|
p = subprocess.Popen(*args, **kwargs)
|
|
850
|
+
stdout_stream = p.stdout
|
|
851
|
+
stderr_stream = p.stderr
|
|
852
|
+
if stdout_stream is None or stderr_stream is None:
|
|
853
|
+
p.wait()
|
|
854
|
+
raise RelenvException("Process pipes are unavailable")
|
|
566
855
|
# Read both stdout and stderr simultaneously
|
|
567
856
|
sel = selectors.DefaultSelector()
|
|
568
|
-
sel.register(
|
|
569
|
-
sel.register(
|
|
857
|
+
sel.register(stdout_stream, selectors.EVENT_READ)
|
|
858
|
+
sel.register(stderr_stream, selectors.EVENT_READ)
|
|
570
859
|
ok = True
|
|
571
860
|
while ok:
|
|
572
861
|
for key, val1 in sel.select():
|
|
573
|
-
|
|
862
|
+
del val1 # unused
|
|
863
|
+
stream = cast(IO[str], key.fileobj)
|
|
864
|
+
line = stream.readline()
|
|
574
865
|
if not line:
|
|
575
866
|
ok = False
|
|
576
867
|
break
|
|
577
868
|
if line.endswith("\n"):
|
|
578
869
|
line = line[:-1]
|
|
579
|
-
if
|
|
870
|
+
if stream is stdout_stream:
|
|
580
871
|
log.info(line)
|
|
581
872
|
else:
|
|
582
873
|
log.error(line)
|
|
583
874
|
|
|
584
875
|
else:
|
|
585
876
|
|
|
586
|
-
def enqueue_stream(
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
877
|
+
def enqueue_stream(
|
|
878
|
+
stream: IO[str],
|
|
879
|
+
item_queue: "queue.Queue[tuple[int | str, str]]",
|
|
880
|
+
kind: int,
|
|
881
|
+
) -> None:
|
|
882
|
+
last_line = ""
|
|
883
|
+
for line in iter(stream.readline, ""):
|
|
884
|
+
if line == "":
|
|
590
885
|
break
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
log.debug("stream close %r %r",
|
|
886
|
+
item_queue.put((kind, line))
|
|
887
|
+
last_line = line
|
|
888
|
+
log.debug("stream close %r %r", kind, last_line)
|
|
594
889
|
stream.close()
|
|
595
890
|
|
|
596
|
-
def enqueue_process(
|
|
891
|
+
def enqueue_process(
|
|
892
|
+
process: subprocess.Popen[str],
|
|
893
|
+
item_queue: "queue.Queue[tuple[int | str, str]]",
|
|
894
|
+
) -> None:
|
|
597
895
|
process.wait()
|
|
598
|
-
|
|
896
|
+
item_queue.put(("x", ""))
|
|
599
897
|
|
|
600
898
|
p = subprocess.Popen(*args, **kwargs)
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
899
|
+
stdout_stream = p.stdout
|
|
900
|
+
stderr_stream = p.stderr
|
|
901
|
+
if stdout_stream is None or stderr_stream is None:
|
|
902
|
+
p.wait()
|
|
903
|
+
raise RelenvException("Process pipes are unavailable")
|
|
904
|
+
q: "queue.Queue[tuple[int | str, str]]" = queue.Queue()
|
|
905
|
+
to = threading.Thread(target=enqueue_stream, args=(stdout_stream, q, 1))
|
|
906
|
+
te = threading.Thread(target=enqueue_stream, args=(stderr_stream, q, 2))
|
|
604
907
|
tp = threading.Thread(target=enqueue_process, args=(p, q))
|
|
605
908
|
te.start()
|
|
606
909
|
to.start()
|
|
@@ -622,11 +925,15 @@ def runcmd(*args, **kwargs):
|
|
|
622
925
|
|
|
623
926
|
p.wait()
|
|
624
927
|
if p.returncode != 0:
|
|
625
|
-
raise
|
|
928
|
+
raise BuildCommandError("Build cmd '{}' failed".format(" ".join(args[0])))
|
|
626
929
|
return p
|
|
627
930
|
|
|
628
931
|
|
|
629
|
-
def relative_interpreter(
|
|
932
|
+
def relative_interpreter(
|
|
933
|
+
root_dir: Union[str, os.PathLike[str]],
|
|
934
|
+
scripts_dir: Union[str, os.PathLike[str]],
|
|
935
|
+
interpreter: Union[str, os.PathLike[str]],
|
|
936
|
+
) -> pathlib.Path:
|
|
630
937
|
"""
|
|
631
938
|
Return a relativized path to the given scripts_dir and interpreter.
|
|
632
939
|
"""
|
|
@@ -644,7 +951,7 @@ def relative_interpreter(root_dir, scripts_dir, interpreter):
|
|
|
644
951
|
return relscripts / relinterp
|
|
645
952
|
|
|
646
953
|
|
|
647
|
-
def makepath(*paths):
|
|
954
|
+
def makepath(*paths: Union[str, os.PathLike[str]]) -> tuple[str, str]:
|
|
648
955
|
"""
|
|
649
956
|
Make a normalized path name from paths.
|
|
650
957
|
"""
|
|
@@ -656,31 +963,33 @@ def makepath(*paths):
|
|
|
656
963
|
return dir, os.path.normcase(dir)
|
|
657
964
|
|
|
658
965
|
|
|
659
|
-
def addpackage(sitedir, name):
|
|
966
|
+
def addpackage(sitedir: str, name: Union[str, os.PathLike[str]]) -> list[str] | None:
|
|
660
967
|
"""
|
|
661
968
|
Add editable package to path.
|
|
662
969
|
"""
|
|
663
970
|
import io
|
|
664
971
|
import stat
|
|
665
972
|
|
|
666
|
-
fullname = os.path.join(sitedir, name)
|
|
667
|
-
paths = []
|
|
973
|
+
fullname = os.path.join(sitedir, os.fspath(name))
|
|
974
|
+
paths: list[str] = []
|
|
668
975
|
try:
|
|
669
976
|
st = os.lstat(fullname)
|
|
670
977
|
except OSError:
|
|
671
|
-
return
|
|
672
|
-
|
|
673
|
-
|
|
978
|
+
return None
|
|
979
|
+
file_attr_hidden = getattr(stat, "FILE_ATTRIBUTE_HIDDEN", 0)
|
|
980
|
+
uf_hidden = getattr(stat, "UF_HIDDEN", 0)
|
|
981
|
+
if (getattr(st, "st_flags", 0) & uf_hidden) or (
|
|
982
|
+
getattr(st, "st_file_attributes", 0) & file_attr_hidden
|
|
674
983
|
):
|
|
675
984
|
# print(f"Skipping hidden .pth file: {fullname!r}")
|
|
676
|
-
return
|
|
985
|
+
return None
|
|
677
986
|
# print(f"Processing .pth file: {fullname!r}")
|
|
678
987
|
try:
|
|
679
988
|
# locale encoding is not ideal especially on Windows. But we have used
|
|
680
989
|
# it for a long time. setuptools uses the locale encoding too.
|
|
681
990
|
f = io.TextIOWrapper(io.open_code(fullname), encoding="locale")
|
|
682
991
|
except OSError:
|
|
683
|
-
return
|
|
992
|
+
return None
|
|
684
993
|
with f:
|
|
685
994
|
for n, line in enumerate(f):
|
|
686
995
|
if line.startswith("#"):
|
|
@@ -710,11 +1019,11 @@ def addpackage(sitedir, name):
|
|
|
710
1019
|
return paths
|
|
711
1020
|
|
|
712
1021
|
|
|
713
|
-
def sanitize_sys_path(sys_path_entries):
|
|
1022
|
+
def sanitize_sys_path(sys_path_entries: Iterable[str]) -> list[str]:
|
|
714
1023
|
"""
|
|
715
1024
|
Sanitize `sys.path` to only include paths relative to the onedir environment.
|
|
716
1025
|
"""
|
|
717
|
-
__sys_path = []
|
|
1026
|
+
__sys_path: list[str] = []
|
|
718
1027
|
__valid_path_prefixes = tuple(
|
|
719
1028
|
{
|
|
720
1029
|
pathlib.Path(sys.prefix).resolve(),
|
|
@@ -735,6 +1044,8 @@ def sanitize_sys_path(sys_path_entries):
|
|
|
735
1044
|
for known_path in __sys_path[:]:
|
|
736
1045
|
for _ in pathlib.Path(known_path).glob("__editable__.*.pth"):
|
|
737
1046
|
paths = addpackage(known_path, _)
|
|
1047
|
+
if not paths:
|
|
1048
|
+
continue
|
|
738
1049
|
for p in paths:
|
|
739
1050
|
if p not in __sys_path:
|
|
740
1051
|
__sys_path.append(p)
|
|
@@ -746,23 +1057,26 @@ class Version:
|
|
|
746
1057
|
Version comparisons.
|
|
747
1058
|
"""
|
|
748
1059
|
|
|
749
|
-
def __init__(self, data):
|
|
750
|
-
|
|
751
|
-
self.
|
|
1060
|
+
def __init__(self, data: str) -> None:
|
|
1061
|
+
major, minor, micro = self.parse_string(data)
|
|
1062
|
+
self.major: int = major
|
|
1063
|
+
self.minor: Optional[int] = minor
|
|
1064
|
+
self.micro: Optional[int] = micro
|
|
1065
|
+
self._data: str = data
|
|
752
1066
|
|
|
753
|
-
def __str__(self):
|
|
1067
|
+
def __str__(self: "Version") -> str:
|
|
754
1068
|
"""
|
|
755
1069
|
Version as string.
|
|
756
1070
|
"""
|
|
757
|
-
|
|
1071
|
+
result = f"{self.major}"
|
|
758
1072
|
if self.minor is not None:
|
|
759
|
-
|
|
1073
|
+
result += f".{self.minor}"
|
|
760
1074
|
if self.micro is not None:
|
|
761
|
-
|
|
1075
|
+
result += f".{self.micro}"
|
|
762
1076
|
# XXX What if minor was None but micro was an int.
|
|
763
|
-
return
|
|
1077
|
+
return result
|
|
764
1078
|
|
|
765
|
-
def __hash__(self):
|
|
1079
|
+
def __hash__(self: "Version") -> int:
|
|
766
1080
|
"""
|
|
767
1081
|
Hash of the version.
|
|
768
1082
|
|
|
@@ -771,7 +1085,7 @@ class Version:
|
|
|
771
1085
|
return hash((self.major, self.minor, self.micro))
|
|
772
1086
|
|
|
773
1087
|
@staticmethod
|
|
774
|
-
def parse_string(data):
|
|
1088
|
+
def parse_string(data: str) -> tuple[int, Optional[int], Optional[int]]:
|
|
775
1089
|
"""
|
|
776
1090
|
Parse a version string into major, minor, and micro integers.
|
|
777
1091
|
"""
|
|
@@ -785,10 +1099,12 @@ class Version:
|
|
|
785
1099
|
else:
|
|
786
1100
|
raise RuntimeError("Too many parts to parse")
|
|
787
1101
|
|
|
788
|
-
def __eq__(self, other):
|
|
1102
|
+
def __eq__(self: "Version", other: object) -> bool:
|
|
789
1103
|
"""
|
|
790
1104
|
Equality comparisons.
|
|
791
1105
|
"""
|
|
1106
|
+
if not isinstance(other, Version):
|
|
1107
|
+
return NotImplemented
|
|
792
1108
|
mymajor = 0 if self.major is None else self.major
|
|
793
1109
|
myminor = 0 if self.minor is None else self.minor
|
|
794
1110
|
mymicro = 0 if self.micro is None else self.micro
|
|
@@ -797,10 +1113,12 @@ class Version:
|
|
|
797
1113
|
micro = 0 if other.micro is None else other.micro
|
|
798
1114
|
return mymajor == major and myminor == minor and mymicro == micro
|
|
799
1115
|
|
|
800
|
-
def __lt__(self, other):
|
|
1116
|
+
def __lt__(self: "Version", other: object) -> bool:
|
|
801
1117
|
"""
|
|
802
1118
|
Less than comparrison.
|
|
803
1119
|
"""
|
|
1120
|
+
if not isinstance(other, Version):
|
|
1121
|
+
return NotImplemented
|
|
804
1122
|
mymajor = 0 if self.major is None else self.major
|
|
805
1123
|
myminor = 0 if self.minor is None else self.minor
|
|
806
1124
|
mymicro = 0 if self.micro is None else self.micro
|
|
@@ -816,10 +1134,12 @@ class Version:
|
|
|
816
1134
|
return True
|
|
817
1135
|
return False
|
|
818
1136
|
|
|
819
|
-
def __le__(self, other):
|
|
1137
|
+
def __le__(self: "Version", other: object) -> bool:
|
|
820
1138
|
"""
|
|
821
1139
|
Less than or equal to comparrison.
|
|
822
1140
|
"""
|
|
1141
|
+
if not isinstance(other, Version):
|
|
1142
|
+
return NotImplemented
|
|
823
1143
|
mymajor = 0 if self.major is None else self.major
|
|
824
1144
|
myminor = 0 if self.minor is None else self.minor
|
|
825
1145
|
mymicro = 0 if self.micro is None else self.micro
|
|
@@ -832,14 +1152,18 @@ class Version:
|
|
|
832
1152
|
return True
|
|
833
1153
|
return False
|
|
834
1154
|
|
|
835
|
-
def __gt__(self, other):
|
|
1155
|
+
def __gt__(self: "Version", other: object) -> bool:
|
|
836
1156
|
"""
|
|
837
1157
|
Greater than comparrison.
|
|
838
1158
|
"""
|
|
1159
|
+
if not isinstance(other, Version):
|
|
1160
|
+
return NotImplemented
|
|
839
1161
|
return not self.__le__(other)
|
|
840
1162
|
|
|
841
|
-
def __ge__(self, other):
|
|
1163
|
+
def __ge__(self: "Version", other: object) -> bool:
|
|
842
1164
|
"""
|
|
843
1165
|
Greater than or equal to comparrison.
|
|
844
1166
|
"""
|
|
1167
|
+
if not isinstance(other, Version):
|
|
1168
|
+
return NotImplemented
|
|
845
1169
|
return not self.__lt__(other)
|