relenv 0.21.1__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 +296 -78
- relenv/build/windows.py +259 -44
- relenv/buildenv.py +48 -17
- relenv/check.py +10 -5
- relenv/common.py +499 -163
- 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 -253
- relenv/toolchain.py +9 -3
- {relenv-0.21.1.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 +500 -34
- relenv/build/common.py +0 -1609
- relenv-0.21.1.dist-info/RECORD +0 -35
- {relenv-0.21.1.dist-info → relenv-0.22.0.dist-info}/WHEEL +0 -0
- {relenv-0.21.1.dist-info → relenv-0.22.0.dist-info}/entry_points.txt +0 -0
- {relenv-0.21.1.dist-info → relenv-0.22.0.dist-info}/licenses/LICENSE.md +0 -0
- {relenv-0.21.1.dist-info → relenv-0.22.0.dist-info}/licenses/NOTICE +0 -0
- {relenv-0.21.1.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,19 +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"):
|
|
604
|
+
log.debug("Found tgz archive")
|
|
339
605
|
read_type = "r:gz"
|
|
340
|
-
elif
|
|
606
|
+
elif archive_str.endswith(".tar.gz"):
|
|
607
|
+
log.debug("Found tar.gz archive")
|
|
608
|
+
read_type = "r:gz"
|
|
609
|
+
elif archive_str.endswith(".xz"):
|
|
610
|
+
log.debug("Found xz archive")
|
|
341
611
|
read_type = "r:xz"
|
|
342
|
-
elif
|
|
612
|
+
elif archive_str.endswith(".bz2"):
|
|
613
|
+
log.debug("Found bz2 archive")
|
|
343
614
|
read_type = "r:bz2"
|
|
344
615
|
else:
|
|
345
|
-
|
|
346
|
-
with tarfile.open(
|
|
347
|
-
|
|
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))
|
|
348
619
|
|
|
349
620
|
|
|
350
|
-
def get_download_location(url, dest):
|
|
621
|
+
def get_download_location(url: str, dest: Union[str, os.PathLike[str]]) -> str:
|
|
351
622
|
"""
|
|
352
623
|
Get the full path to where the url will be downloaded to.
|
|
353
624
|
|
|
@@ -359,10 +630,10 @@ def get_download_location(url, dest):
|
|
|
359
630
|
:return: The path to where the url will be downloaded to
|
|
360
631
|
:rtype: str
|
|
361
632
|
"""
|
|
362
|
-
return os.path.join(dest, os.path.basename(url))
|
|
633
|
+
return os.path.join(os.fspath(dest), os.path.basename(url))
|
|
363
634
|
|
|
364
635
|
|
|
365
|
-
def check_url(url, timestamp=None, timeout=30):
|
|
636
|
+
def check_url(url: str, timestamp: Optional[float] = None, timeout: float = 30) -> bool:
|
|
366
637
|
"""
|
|
367
638
|
Check that the url returns a 200.
|
|
368
639
|
"""
|
|
@@ -393,52 +664,79 @@ def check_url(url, timestamp=None, timeout=30):
|
|
|
393
664
|
return True
|
|
394
665
|
|
|
395
666
|
|
|
396
|
-
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:
|
|
397
674
|
"""
|
|
398
675
|
Fetch the contents of a url.
|
|
399
676
|
|
|
400
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]]
|
|
401
681
|
"""
|
|
402
682
|
# Late import so we do not import hashlib before runtime.bootstrap is called.
|
|
403
683
|
import urllib.error
|
|
404
684
|
import urllib.request
|
|
405
685
|
|
|
406
686
|
last = time.time()
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
while n < backoff:
|
|
411
|
-
n += 1
|
|
687
|
+
attempts = max(backoff, 1)
|
|
688
|
+
response: http.client.HTTPResponse | None = None
|
|
689
|
+
for attempt in range(1, attempts + 1):
|
|
412
690
|
try:
|
|
413
|
-
|
|
691
|
+
response = urllib.request.urlopen(url, timeout=timeout)
|
|
692
|
+
break
|
|
414
693
|
except (
|
|
415
694
|
urllib.error.HTTPError,
|
|
416
695
|
urllib.error.URLError,
|
|
417
696
|
http.client.RemoteDisconnected,
|
|
418
697
|
) as exc:
|
|
419
|
-
if
|
|
698
|
+
if attempt >= attempts:
|
|
420
699
|
raise RelenvException(f"Error fetching url {url} {exc}")
|
|
421
700
|
log.debug("Unable to connect %s", url)
|
|
422
|
-
time.sleep(
|
|
701
|
+
time.sleep(attempt * 10)
|
|
702
|
+
if response is None:
|
|
703
|
+
raise RelenvException(f"Unable to open url {url}")
|
|
423
704
|
log.info("url opened %s", url)
|
|
705
|
+
|
|
706
|
+
# Get content length from headers
|
|
707
|
+
content_length = 0
|
|
424
708
|
try:
|
|
425
|
-
|
|
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
|
|
426
721
|
size = 1024 * 300
|
|
427
|
-
block =
|
|
722
|
+
block = response.read(size)
|
|
428
723
|
while block:
|
|
429
|
-
|
|
724
|
+
block_size = len(block)
|
|
725
|
+
downloaded += block_size
|
|
430
726
|
if time.time() - last > 10:
|
|
431
|
-
log.info("%s > %d", url,
|
|
727
|
+
log.info("%s > %d", url, downloaded)
|
|
432
728
|
last = time.time()
|
|
433
729
|
fp.write(block)
|
|
434
|
-
|
|
730
|
+
# Report progress
|
|
731
|
+
if progress_callback and content_length > 0:
|
|
732
|
+
progress_callback(downloaded, content_length)
|
|
733
|
+
block = response.read(10240)
|
|
435
734
|
finally:
|
|
436
|
-
|
|
437
|
-
# fp.close()
|
|
735
|
+
response.close()
|
|
438
736
|
log.info("Download complete %s", url)
|
|
439
737
|
|
|
440
738
|
|
|
441
|
-
def fetch_url_content(url, backoff=3, timeout=30):
|
|
739
|
+
def fetch_url_content(url: str, backoff: int = 3, timeout: float = 30) -> str:
|
|
442
740
|
"""
|
|
443
741
|
Fetch the contents of a url.
|
|
444
742
|
|
|
@@ -446,55 +744,47 @@ def fetch_url_content(url, backoff=3, timeout=30):
|
|
|
446
744
|
"""
|
|
447
745
|
# Late import so we do not import hashlib before runtime.bootstrap is called.
|
|
448
746
|
import gzip
|
|
449
|
-
import io
|
|
450
747
|
import urllib.error
|
|
451
748
|
import urllib.request
|
|
452
749
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if backoff < 1:
|
|
457
|
-
backoff = 1
|
|
458
|
-
n = 0
|
|
459
|
-
while n < backoff:
|
|
460
|
-
n += 1
|
|
750
|
+
attempts = max(backoff, 1)
|
|
751
|
+
response: http.client.HTTPResponse | None = None
|
|
752
|
+
for attempt in range(1, attempts + 1):
|
|
461
753
|
try:
|
|
462
|
-
|
|
754
|
+
response = urllib.request.urlopen(url, timeout=timeout)
|
|
755
|
+
break
|
|
463
756
|
except (
|
|
464
757
|
urllib.error.HTTPError,
|
|
465
758
|
urllib.error.URLError,
|
|
466
759
|
http.client.RemoteDisconnected,
|
|
467
760
|
) as exc:
|
|
468
|
-
if
|
|
761
|
+
if attempt >= attempts:
|
|
469
762
|
raise RelenvException(f"Error fetching url {url} {exc}")
|
|
470
763
|
log.debug("Unable to connect %s", url)
|
|
471
|
-
time.sleep(
|
|
764
|
+
time.sleep(attempt * 10)
|
|
765
|
+
if response is None:
|
|
766
|
+
raise RelenvException(f"Unable to open url {url}")
|
|
472
767
|
log.info("url opened %s", url)
|
|
473
768
|
try:
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
block = fin.read(size)
|
|
477
|
-
while block:
|
|
478
|
-
total += size
|
|
479
|
-
if time.time() - last > 10:
|
|
480
|
-
log.info("%s > %d", url, total)
|
|
481
|
-
last = time.time()
|
|
482
|
-
fp.write(block)
|
|
483
|
-
block = fin.read(10240)
|
|
769
|
+
data = response.read()
|
|
770
|
+
encoding = response.headers.get("content-encoding", "").lower()
|
|
484
771
|
finally:
|
|
485
|
-
|
|
486
|
-
|
|
772
|
+
response.close()
|
|
773
|
+
if encoding == "gzip":
|
|
774
|
+
log.debug("Found gzipped content")
|
|
775
|
+
data = gzip.decompress(data)
|
|
487
776
|
log.info("Download complete %s", url)
|
|
488
|
-
|
|
489
|
-
info = fin.info()
|
|
490
|
-
if "content-encoding" in info:
|
|
491
|
-
if info["content-encoding"] == "gzip":
|
|
492
|
-
log.debug("Found gzipped content")
|
|
493
|
-
fp = gzip.GzipFile(fileobj=fp)
|
|
494
|
-
return fp.read().decode()
|
|
777
|
+
return data.decode()
|
|
495
778
|
|
|
496
779
|
|
|
497
|
-
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:
|
|
498
788
|
"""
|
|
499
789
|
Download the url to the provided destination.
|
|
500
790
|
|
|
@@ -506,6 +796,8 @@ def download_url(url, dest, verbose=True, backoff=3, timeout=60):
|
|
|
506
796
|
:type dest: str
|
|
507
797
|
:param verbose: Print download url and destination to stdout
|
|
508
798
|
:type verbose: bool
|
|
799
|
+
:param progress_callback: Optional callback(downloaded_bytes, total_bytes)
|
|
800
|
+
:type progress_callback: Optional[Callable[[int, int], None]]
|
|
509
801
|
|
|
510
802
|
:raises urllib.error.HTTPError: If the url was unable to be downloaded
|
|
511
803
|
|
|
@@ -514,22 +806,24 @@ def download_url(url, dest, verbose=True, backoff=3, timeout=60):
|
|
|
514
806
|
"""
|
|
515
807
|
local = get_download_location(url, dest)
|
|
516
808
|
if verbose:
|
|
517
|
-
|
|
518
|
-
fout = open(local, "wb")
|
|
809
|
+
log.debug(f"Downloading {url} -> {local}")
|
|
519
810
|
try:
|
|
520
|
-
|
|
811
|
+
with open(local, "wb") as fout:
|
|
812
|
+
fetch_url(url, fout, backoff, timeout, progress_callback)
|
|
521
813
|
except Exception as exc:
|
|
522
814
|
if verbose:
|
|
523
|
-
|
|
815
|
+
log.error("Unable to download: %s\n%s", url, exc)
|
|
524
816
|
try:
|
|
525
817
|
os.unlink(local)
|
|
526
818
|
except OSError:
|
|
527
819
|
pass
|
|
528
820
|
raise
|
|
821
|
+
finally:
|
|
822
|
+
log.debug(f"Finished downloading {url} -> {local}")
|
|
529
823
|
return local
|
|
530
824
|
|
|
531
825
|
|
|
532
|
-
def runcmd(*args, **kwargs):
|
|
826
|
+
def runcmd(*args: Any, **kwargs: Any) -> subprocess.Popen[str]:
|
|
533
827
|
"""
|
|
534
828
|
Run a command.
|
|
535
829
|
|
|
@@ -541,7 +835,9 @@ def runcmd(*args, **kwargs):
|
|
|
541
835
|
|
|
542
836
|
:raises RelenvException: If the command finishes with a non zero exit code
|
|
543
837
|
"""
|
|
544
|
-
|
|
838
|
+
if not args:
|
|
839
|
+
raise RelenvException("No command provided to runcmd")
|
|
840
|
+
log.debug("Running command: %s", " ".join(map(str, args[0])))
|
|
545
841
|
# if "stdout" not in kwargs:
|
|
546
842
|
kwargs["stdout"] = subprocess.PIPE
|
|
547
843
|
# if "stderr" not in kwargs:
|
|
@@ -551,44 +847,63 @@ def runcmd(*args, **kwargs):
|
|
|
551
847
|
if sys.platform != "win32":
|
|
552
848
|
|
|
553
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")
|
|
554
855
|
# Read both stdout and stderr simultaneously
|
|
555
856
|
sel = selectors.DefaultSelector()
|
|
556
|
-
sel.register(
|
|
557
|
-
sel.register(
|
|
857
|
+
sel.register(stdout_stream, selectors.EVENT_READ)
|
|
858
|
+
sel.register(stderr_stream, selectors.EVENT_READ)
|
|
558
859
|
ok = True
|
|
559
860
|
while ok:
|
|
560
861
|
for key, val1 in sel.select():
|
|
561
|
-
|
|
862
|
+
del val1 # unused
|
|
863
|
+
stream = cast(IO[str], key.fileobj)
|
|
864
|
+
line = stream.readline()
|
|
562
865
|
if not line:
|
|
563
866
|
ok = False
|
|
564
867
|
break
|
|
565
868
|
if line.endswith("\n"):
|
|
566
869
|
line = line[:-1]
|
|
567
|
-
if
|
|
870
|
+
if stream is stdout_stream:
|
|
568
871
|
log.info(line)
|
|
569
872
|
else:
|
|
570
873
|
log.error(line)
|
|
571
874
|
|
|
572
875
|
else:
|
|
573
876
|
|
|
574
|
-
def enqueue_stream(
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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 == "":
|
|
578
885
|
break
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
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)
|
|
582
889
|
stream.close()
|
|
583
890
|
|
|
584
|
-
def enqueue_process(
|
|
891
|
+
def enqueue_process(
|
|
892
|
+
process: subprocess.Popen[str],
|
|
893
|
+
item_queue: "queue.Queue[tuple[int | str, str]]",
|
|
894
|
+
) -> None:
|
|
585
895
|
process.wait()
|
|
586
|
-
|
|
896
|
+
item_queue.put(("x", ""))
|
|
587
897
|
|
|
588
898
|
p = subprocess.Popen(*args, **kwargs)
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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))
|
|
592
907
|
tp = threading.Thread(target=enqueue_process, args=(p, q))
|
|
593
908
|
te.start()
|
|
594
909
|
to.start()
|
|
@@ -610,11 +925,15 @@ def runcmd(*args, **kwargs):
|
|
|
610
925
|
|
|
611
926
|
p.wait()
|
|
612
927
|
if p.returncode != 0:
|
|
613
|
-
raise
|
|
928
|
+
raise BuildCommandError("Build cmd '{}' failed".format(" ".join(args[0])))
|
|
614
929
|
return p
|
|
615
930
|
|
|
616
931
|
|
|
617
|
-
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:
|
|
618
937
|
"""
|
|
619
938
|
Return a relativized path to the given scripts_dir and interpreter.
|
|
620
939
|
"""
|
|
@@ -632,7 +951,7 @@ def relative_interpreter(root_dir, scripts_dir, interpreter):
|
|
|
632
951
|
return relscripts / relinterp
|
|
633
952
|
|
|
634
953
|
|
|
635
|
-
def makepath(*paths):
|
|
954
|
+
def makepath(*paths: Union[str, os.PathLike[str]]) -> tuple[str, str]:
|
|
636
955
|
"""
|
|
637
956
|
Make a normalized path name from paths.
|
|
638
957
|
"""
|
|
@@ -644,31 +963,33 @@ def makepath(*paths):
|
|
|
644
963
|
return dir, os.path.normcase(dir)
|
|
645
964
|
|
|
646
965
|
|
|
647
|
-
def addpackage(sitedir, name):
|
|
966
|
+
def addpackage(sitedir: str, name: Union[str, os.PathLike[str]]) -> list[str] | None:
|
|
648
967
|
"""
|
|
649
968
|
Add editable package to path.
|
|
650
969
|
"""
|
|
651
970
|
import io
|
|
652
971
|
import stat
|
|
653
972
|
|
|
654
|
-
fullname = os.path.join(sitedir, name)
|
|
655
|
-
paths = []
|
|
973
|
+
fullname = os.path.join(sitedir, os.fspath(name))
|
|
974
|
+
paths: list[str] = []
|
|
656
975
|
try:
|
|
657
976
|
st = os.lstat(fullname)
|
|
658
977
|
except OSError:
|
|
659
|
-
return
|
|
660
|
-
|
|
661
|
-
|
|
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
|
|
662
983
|
):
|
|
663
984
|
# print(f"Skipping hidden .pth file: {fullname!r}")
|
|
664
|
-
return
|
|
985
|
+
return None
|
|
665
986
|
# print(f"Processing .pth file: {fullname!r}")
|
|
666
987
|
try:
|
|
667
988
|
# locale encoding is not ideal especially on Windows. But we have used
|
|
668
989
|
# it for a long time. setuptools uses the locale encoding too.
|
|
669
990
|
f = io.TextIOWrapper(io.open_code(fullname), encoding="locale")
|
|
670
991
|
except OSError:
|
|
671
|
-
return
|
|
992
|
+
return None
|
|
672
993
|
with f:
|
|
673
994
|
for n, line in enumerate(f):
|
|
674
995
|
if line.startswith("#"):
|
|
@@ -698,11 +1019,11 @@ def addpackage(sitedir, name):
|
|
|
698
1019
|
return paths
|
|
699
1020
|
|
|
700
1021
|
|
|
701
|
-
def sanitize_sys_path(sys_path_entries):
|
|
1022
|
+
def sanitize_sys_path(sys_path_entries: Iterable[str]) -> list[str]:
|
|
702
1023
|
"""
|
|
703
1024
|
Sanitize `sys.path` to only include paths relative to the onedir environment.
|
|
704
1025
|
"""
|
|
705
|
-
__sys_path = []
|
|
1026
|
+
__sys_path: list[str] = []
|
|
706
1027
|
__valid_path_prefixes = tuple(
|
|
707
1028
|
{
|
|
708
1029
|
pathlib.Path(sys.prefix).resolve(),
|
|
@@ -723,6 +1044,8 @@ def sanitize_sys_path(sys_path_entries):
|
|
|
723
1044
|
for known_path in __sys_path[:]:
|
|
724
1045
|
for _ in pathlib.Path(known_path).glob("__editable__.*.pth"):
|
|
725
1046
|
paths = addpackage(known_path, _)
|
|
1047
|
+
if not paths:
|
|
1048
|
+
continue
|
|
726
1049
|
for p in paths:
|
|
727
1050
|
if p not in __sys_path:
|
|
728
1051
|
__sys_path.append(p)
|
|
@@ -734,23 +1057,26 @@ class Version:
|
|
|
734
1057
|
Version comparisons.
|
|
735
1058
|
"""
|
|
736
1059
|
|
|
737
|
-
def __init__(self, data):
|
|
738
|
-
|
|
739
|
-
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
|
|
740
1066
|
|
|
741
|
-
def __str__(self):
|
|
1067
|
+
def __str__(self: "Version") -> str:
|
|
742
1068
|
"""
|
|
743
1069
|
Version as string.
|
|
744
1070
|
"""
|
|
745
|
-
|
|
1071
|
+
result = f"{self.major}"
|
|
746
1072
|
if self.minor is not None:
|
|
747
|
-
|
|
1073
|
+
result += f".{self.minor}"
|
|
748
1074
|
if self.micro is not None:
|
|
749
|
-
|
|
1075
|
+
result += f".{self.micro}"
|
|
750
1076
|
# XXX What if minor was None but micro was an int.
|
|
751
|
-
return
|
|
1077
|
+
return result
|
|
752
1078
|
|
|
753
|
-
def __hash__(self):
|
|
1079
|
+
def __hash__(self: "Version") -> int:
|
|
754
1080
|
"""
|
|
755
1081
|
Hash of the version.
|
|
756
1082
|
|
|
@@ -759,7 +1085,7 @@ class Version:
|
|
|
759
1085
|
return hash((self.major, self.minor, self.micro))
|
|
760
1086
|
|
|
761
1087
|
@staticmethod
|
|
762
|
-
def parse_string(data):
|
|
1088
|
+
def parse_string(data: str) -> tuple[int, Optional[int], Optional[int]]:
|
|
763
1089
|
"""
|
|
764
1090
|
Parse a version string into major, minor, and micro integers.
|
|
765
1091
|
"""
|
|
@@ -773,10 +1099,12 @@ class Version:
|
|
|
773
1099
|
else:
|
|
774
1100
|
raise RuntimeError("Too many parts to parse")
|
|
775
1101
|
|
|
776
|
-
def __eq__(self, other):
|
|
1102
|
+
def __eq__(self: "Version", other: object) -> bool:
|
|
777
1103
|
"""
|
|
778
1104
|
Equality comparisons.
|
|
779
1105
|
"""
|
|
1106
|
+
if not isinstance(other, Version):
|
|
1107
|
+
return NotImplemented
|
|
780
1108
|
mymajor = 0 if self.major is None else self.major
|
|
781
1109
|
myminor = 0 if self.minor is None else self.minor
|
|
782
1110
|
mymicro = 0 if self.micro is None else self.micro
|
|
@@ -785,10 +1113,12 @@ class Version:
|
|
|
785
1113
|
micro = 0 if other.micro is None else other.micro
|
|
786
1114
|
return mymajor == major and myminor == minor and mymicro == micro
|
|
787
1115
|
|
|
788
|
-
def __lt__(self, other):
|
|
1116
|
+
def __lt__(self: "Version", other: object) -> bool:
|
|
789
1117
|
"""
|
|
790
1118
|
Less than comparrison.
|
|
791
1119
|
"""
|
|
1120
|
+
if not isinstance(other, Version):
|
|
1121
|
+
return NotImplemented
|
|
792
1122
|
mymajor = 0 if self.major is None else self.major
|
|
793
1123
|
myminor = 0 if self.minor is None else self.minor
|
|
794
1124
|
mymicro = 0 if self.micro is None else self.micro
|
|
@@ -804,10 +1134,12 @@ class Version:
|
|
|
804
1134
|
return True
|
|
805
1135
|
return False
|
|
806
1136
|
|
|
807
|
-
def __le__(self, other):
|
|
1137
|
+
def __le__(self: "Version", other: object) -> bool:
|
|
808
1138
|
"""
|
|
809
1139
|
Less than or equal to comparrison.
|
|
810
1140
|
"""
|
|
1141
|
+
if not isinstance(other, Version):
|
|
1142
|
+
return NotImplemented
|
|
811
1143
|
mymajor = 0 if self.major is None else self.major
|
|
812
1144
|
myminor = 0 if self.minor is None else self.minor
|
|
813
1145
|
mymicro = 0 if self.micro is None else self.micro
|
|
@@ -820,14 +1152,18 @@ class Version:
|
|
|
820
1152
|
return True
|
|
821
1153
|
return False
|
|
822
1154
|
|
|
823
|
-
def __gt__(self, other):
|
|
1155
|
+
def __gt__(self: "Version", other: object) -> bool:
|
|
824
1156
|
"""
|
|
825
1157
|
Greater than comparrison.
|
|
826
1158
|
"""
|
|
1159
|
+
if not isinstance(other, Version):
|
|
1160
|
+
return NotImplemented
|
|
827
1161
|
return not self.__le__(other)
|
|
828
1162
|
|
|
829
|
-
def __ge__(self, other):
|
|
1163
|
+
def __ge__(self: "Version", other: object) -> bool:
|
|
830
1164
|
"""
|
|
831
1165
|
Greater than or equal to comparrison.
|
|
832
1166
|
"""
|
|
1167
|
+
if not isinstance(other, Version):
|
|
1168
|
+
return NotImplemented
|
|
833
1169
|
return not self.__lt__(other)
|