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
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
# Copyright 2022-2025 Broadcom.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""
|
|
4
|
+
Installation and finalization functions for the build process.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import fnmatch
|
|
9
|
+
import hashlib
|
|
10
|
+
import io
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import os.path
|
|
15
|
+
import pathlib
|
|
16
|
+
import pprint
|
|
17
|
+
import re
|
|
18
|
+
import shutil
|
|
19
|
+
import sys
|
|
20
|
+
import tarfile
|
|
21
|
+
from types import ModuleType
|
|
22
|
+
from typing import IO, MutableMapping, Optional, Sequence, Union, TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
from relenv.common import (
|
|
25
|
+
LINUX,
|
|
26
|
+
MODULE_DIR,
|
|
27
|
+
MissingDependencyError,
|
|
28
|
+
Version,
|
|
29
|
+
download_url,
|
|
30
|
+
format_shebang,
|
|
31
|
+
runcmd,
|
|
32
|
+
)
|
|
33
|
+
import relenv.relocate
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from .builder import Dirs
|
|
37
|
+
|
|
38
|
+
# Type alias for path-like objects
|
|
39
|
+
PathLike = Union[str, os.PathLike[str]]
|
|
40
|
+
|
|
41
|
+
# Relenv PTH file content for bootstrapping
|
|
42
|
+
RELENV_PTH = (
|
|
43
|
+
"import os; "
|
|
44
|
+
"import sys; "
|
|
45
|
+
"from importlib import util; "
|
|
46
|
+
"from pathlib import Path; "
|
|
47
|
+
"spec = util.spec_from_file_location("
|
|
48
|
+
"'relenv.runtime', str(Path(__file__).parent / 'site-packages' / 'relenv' / 'runtime.py')"
|
|
49
|
+
"); "
|
|
50
|
+
"mod = util.module_from_spec(spec); "
|
|
51
|
+
"sys.modules['relenv.runtime'] = mod; "
|
|
52
|
+
"spec.loader.exec_module(mod); mod.bootstrap();"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
log = logging.getLogger(__name__)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def patch_file(path: PathLike, old: str, new: str) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Search a file line by line for a string to replace.
|
|
61
|
+
|
|
62
|
+
:param path: Location of the file to search
|
|
63
|
+
:type path: str
|
|
64
|
+
:param old: The value that will be replaced
|
|
65
|
+
:type path: str
|
|
66
|
+
:param new: The value that will replace the 'old' value.
|
|
67
|
+
:type path: str
|
|
68
|
+
"""
|
|
69
|
+
log.debug("Patching file: %s", path)
|
|
70
|
+
with open(path, "r") as fp:
|
|
71
|
+
content = fp.read()
|
|
72
|
+
new_content = ""
|
|
73
|
+
for line in content.splitlines():
|
|
74
|
+
line = re.sub(old, new, line)
|
|
75
|
+
new_content += line + "\n"
|
|
76
|
+
with open(path, "w") as fp:
|
|
77
|
+
fp.write(new_content)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def update_sbom_checksums(
|
|
81
|
+
source_dir: PathLike, files_to_update: MutableMapping[str, PathLike]
|
|
82
|
+
) -> None:
|
|
83
|
+
"""
|
|
84
|
+
Update checksums in sbom.spdx.json for modified files.
|
|
85
|
+
|
|
86
|
+
Python 3.12+ includes an SBOM (Software Bill of Materials) that tracks
|
|
87
|
+
file checksums. When we update files (e.g., expat sources), we need to
|
|
88
|
+
recalculate their checksums.
|
|
89
|
+
|
|
90
|
+
:param source_dir: Path to the Python source directory
|
|
91
|
+
:type source_dir: PathLike
|
|
92
|
+
:param files_to_update: Mapping of SBOM relative paths to actual file paths
|
|
93
|
+
:type files_to_update: MutableMapping[str, PathLike]
|
|
94
|
+
"""
|
|
95
|
+
source_path = pathlib.Path(source_dir)
|
|
96
|
+
spdx_json = source_path / "Misc" / "sbom.spdx.json"
|
|
97
|
+
|
|
98
|
+
# SBOM only exists in Python 3.12+
|
|
99
|
+
if not spdx_json.exists():
|
|
100
|
+
log.debug("SBOM file not found, skipping checksum updates")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
# Read the SBOM JSON
|
|
104
|
+
with open(spdx_json, "r") as f:
|
|
105
|
+
data = json.load(f)
|
|
106
|
+
|
|
107
|
+
# Compute checksums for each file
|
|
108
|
+
checksums = {}
|
|
109
|
+
for relative_path, file_path in files_to_update.items():
|
|
110
|
+
file_path = pathlib.Path(file_path)
|
|
111
|
+
if not file_path.exists():
|
|
112
|
+
log.warning("File not found for checksum: %s", file_path)
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
# Compute SHA1 and SHA256
|
|
116
|
+
sha1 = hashlib.sha1()
|
|
117
|
+
sha256 = hashlib.sha256()
|
|
118
|
+
with open(file_path, "rb") as f:
|
|
119
|
+
content = f.read()
|
|
120
|
+
sha1.update(content)
|
|
121
|
+
sha256.update(content)
|
|
122
|
+
|
|
123
|
+
checksums[relative_path] = [
|
|
124
|
+
{
|
|
125
|
+
"algorithm": "SHA1",
|
|
126
|
+
"checksumValue": sha1.hexdigest(),
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"algorithm": "SHA256",
|
|
130
|
+
"checksumValue": sha256.hexdigest(),
|
|
131
|
+
},
|
|
132
|
+
]
|
|
133
|
+
log.debug(
|
|
134
|
+
"Computed checksums for %s: SHA1=%s, SHA256=%s",
|
|
135
|
+
relative_path,
|
|
136
|
+
sha1.hexdigest(),
|
|
137
|
+
sha256.hexdigest(),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Update the SBOM with new checksums
|
|
141
|
+
updated_count = 0
|
|
142
|
+
for file_entry in data.get("files", []):
|
|
143
|
+
file_name = file_entry.get("fileName")
|
|
144
|
+
if file_name in checksums:
|
|
145
|
+
file_entry["checksums"] = checksums[file_name]
|
|
146
|
+
updated_count += 1
|
|
147
|
+
log.info("Updated SBOM checksums for %s", file_name)
|
|
148
|
+
|
|
149
|
+
# Write back the updated SBOM
|
|
150
|
+
with open(spdx_json, "w") as f:
|
|
151
|
+
json.dump(data, f, indent=2)
|
|
152
|
+
|
|
153
|
+
log.info("Updated %d file checksums in SBOM", updated_count)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def patch_shebang(path: PathLike, old: str, new: str) -> bool:
|
|
157
|
+
"""
|
|
158
|
+
Replace a file's shebang.
|
|
159
|
+
|
|
160
|
+
:param path: The path of the file to patch
|
|
161
|
+
:type path: str
|
|
162
|
+
:param old: The old shebang, will only patch when this is found
|
|
163
|
+
:type old: str
|
|
164
|
+
:param name: The new shebang to be written
|
|
165
|
+
:type name: str
|
|
166
|
+
"""
|
|
167
|
+
with open(path, "rb") as fp:
|
|
168
|
+
try:
|
|
169
|
+
data = fp.read(len(old.encode())).decode()
|
|
170
|
+
except UnicodeError:
|
|
171
|
+
return False
|
|
172
|
+
except Exception as exc:
|
|
173
|
+
log.warning("Unhandled exception: %r", exc)
|
|
174
|
+
return False
|
|
175
|
+
if data != old:
|
|
176
|
+
log.warning("Shebang doesn't match: %s %r != %r", path, old, data)
|
|
177
|
+
return False
|
|
178
|
+
data = fp.read().decode()
|
|
179
|
+
with open(path, "w") as fp:
|
|
180
|
+
fp.write(new)
|
|
181
|
+
fp.write(data)
|
|
182
|
+
with open(path, "r") as fp:
|
|
183
|
+
data = fp.read()
|
|
184
|
+
log.info("Patched shebang of %s => %r", path, data)
|
|
185
|
+
return True
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def patch_shebangs(path: PathLike, old: str, new: str) -> None:
|
|
189
|
+
"""
|
|
190
|
+
Traverse directory and patch shebangs.
|
|
191
|
+
|
|
192
|
+
:param path: The of the directory to traverse
|
|
193
|
+
:type path: str
|
|
194
|
+
:param old: The old shebang, will only patch when this is found
|
|
195
|
+
:type old: str
|
|
196
|
+
:param name: The new shebang to be written
|
|
197
|
+
:type name: str
|
|
198
|
+
"""
|
|
199
|
+
for root, _dirs, files in os.walk(str(path)):
|
|
200
|
+
for file in files:
|
|
201
|
+
patch_shebang(os.path.join(root, file), old, new)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _load_sysconfigdata_template() -> str:
|
|
205
|
+
"""Load the sysconfigdata template from disk.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
The Python code template for sysconfigdata module.
|
|
209
|
+
|
|
210
|
+
Note:
|
|
211
|
+
This is loaded from a .py file rather than embedded as a string
|
|
212
|
+
to enable syntax checking, IDE support, and easier maintenance.
|
|
213
|
+
Follows CPython convention of separating data from code.
|
|
214
|
+
"""
|
|
215
|
+
template_path = pathlib.Path(__file__).parent / "_sysconfigdata_template.py"
|
|
216
|
+
template_content = template_path.read_text(encoding="utf-8")
|
|
217
|
+
|
|
218
|
+
# Extract only the code after the docstring
|
|
219
|
+
# Skip the copyright header and module docstring
|
|
220
|
+
lines = template_content.split("\n")
|
|
221
|
+
code_lines = []
|
|
222
|
+
found_code = False
|
|
223
|
+
|
|
224
|
+
for line in lines:
|
|
225
|
+
# Skip until we find the first import statement
|
|
226
|
+
if not found_code:
|
|
227
|
+
if line.startswith("import ") or line.startswith("from "):
|
|
228
|
+
found_code = True
|
|
229
|
+
else:
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
code_lines.append(line)
|
|
233
|
+
|
|
234
|
+
return "\n".join(code_lines)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def update_ensurepip(directory: pathlib.Path) -> None:
|
|
238
|
+
"""
|
|
239
|
+
Update bundled dependencies for ensurepip (pip & setuptools).
|
|
240
|
+
"""
|
|
241
|
+
# ensurepip bundle location
|
|
242
|
+
bundle_dir = directory / "ensurepip" / "_bundled"
|
|
243
|
+
|
|
244
|
+
# Make sure the destination directory exists
|
|
245
|
+
bundle_dir.mkdir(parents=True, exist_ok=True)
|
|
246
|
+
|
|
247
|
+
# Detect existing whl. Later versions of python don't include setuptools. We
|
|
248
|
+
# only want to update whl files that python expects to be there
|
|
249
|
+
pip_version = "25.2"
|
|
250
|
+
setuptools_version = "80.9.0"
|
|
251
|
+
update_pip = False
|
|
252
|
+
update_setuptools = False
|
|
253
|
+
for file in bundle_dir.glob("*.whl"):
|
|
254
|
+
|
|
255
|
+
log.debug("Checking whl: %s", str(file))
|
|
256
|
+
if file.name.startswith("pip-"):
|
|
257
|
+
found_version = file.name.split("-")[1]
|
|
258
|
+
log.debug("Found version %s", found_version)
|
|
259
|
+
if Version(found_version) >= Version(pip_version):
|
|
260
|
+
log.debug("Found correct pip version or newer: %s", found_version)
|
|
261
|
+
else:
|
|
262
|
+
file.unlink()
|
|
263
|
+
update_pip = True
|
|
264
|
+
if file.name.startswith("setuptools-"):
|
|
265
|
+
found_version = file.name.split("-")[1]
|
|
266
|
+
log.debug("Found version %s", found_version)
|
|
267
|
+
if Version(found_version) >= Version(setuptools_version):
|
|
268
|
+
log.debug(
|
|
269
|
+
"Found correct setuptools version or newer: %s", found_version
|
|
270
|
+
)
|
|
271
|
+
else:
|
|
272
|
+
file.unlink()
|
|
273
|
+
update_setuptools = True
|
|
274
|
+
|
|
275
|
+
# Download whl files and update __init__.py
|
|
276
|
+
init_file = directory / "ensurepip" / "__init__.py"
|
|
277
|
+
if update_pip:
|
|
278
|
+
whl = f"pip-{pip_version}-py3-none-any.whl"
|
|
279
|
+
whl_path = "b7/3f/945ef7ab14dc4f9d7f40288d2df998d1837ee0888ec3659c813487572faa"
|
|
280
|
+
url = f"https://files.pythonhosted.org/packages/{whl_path}/{whl}"
|
|
281
|
+
download_url(url=url, dest=bundle_dir)
|
|
282
|
+
assert (bundle_dir / whl).exists()
|
|
283
|
+
|
|
284
|
+
# Update __init__.py
|
|
285
|
+
old = "^_PIP_VERSION.*"
|
|
286
|
+
new = f'_PIP_VERSION = "{pip_version}"'
|
|
287
|
+
patch_file(path=init_file, old=old, new=new)
|
|
288
|
+
|
|
289
|
+
# setuptools
|
|
290
|
+
if update_setuptools:
|
|
291
|
+
whl = f"setuptools-{setuptools_version}-py3-none-any.whl"
|
|
292
|
+
whl_path = "a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772"
|
|
293
|
+
url = f"https://files.pythonhosted.org/packages/{whl_path}/{whl}"
|
|
294
|
+
download_url(url=url, dest=bundle_dir)
|
|
295
|
+
assert (bundle_dir / whl).exists()
|
|
296
|
+
|
|
297
|
+
# setuptools
|
|
298
|
+
old = "^_SETUPTOOLS_VERSION.*"
|
|
299
|
+
new = f'_SETUPTOOLS_VERSION = "{setuptools_version}"'
|
|
300
|
+
patch_file(path=init_file, old=old, new=new)
|
|
301
|
+
|
|
302
|
+
log.debug("ensurepip __init__.py contents:")
|
|
303
|
+
log.debug(init_file.read_text())
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def install_sysdata(
|
|
307
|
+
mod: ModuleType,
|
|
308
|
+
destfile: PathLike,
|
|
309
|
+
buildroot: PathLike,
|
|
310
|
+
toolchain: Optional[PathLike],
|
|
311
|
+
) -> None:
|
|
312
|
+
"""
|
|
313
|
+
Create a Relenv Python environment's sysconfigdata.
|
|
314
|
+
|
|
315
|
+
Helper method used by the `finalize` build method to create a Relenv
|
|
316
|
+
Python environment's sysconfigdata.
|
|
317
|
+
|
|
318
|
+
:param mod: The module to operate on
|
|
319
|
+
:type mod: ``types.ModuleType``
|
|
320
|
+
:param destfile: Path to the file to write the data to
|
|
321
|
+
:type destfile: str
|
|
322
|
+
:param buildroot: Path to the root of the build
|
|
323
|
+
:type buildroot: str
|
|
324
|
+
:param toolchain: Path to the root of the toolchain
|
|
325
|
+
:type toolchain: str
|
|
326
|
+
"""
|
|
327
|
+
data = {}
|
|
328
|
+
|
|
329
|
+
def fbuildroot(s: str) -> str:
|
|
330
|
+
return s.replace(str(buildroot), "{BUILDROOT}")
|
|
331
|
+
|
|
332
|
+
def ftoolchain(s: str) -> str:
|
|
333
|
+
return s.replace(str(toolchain), "{TOOLCHAIN}")
|
|
334
|
+
|
|
335
|
+
# XXX: keymap is not used, remove it?
|
|
336
|
+
# keymap = {
|
|
337
|
+
# "BINDIR": (fbuildroot,),
|
|
338
|
+
# "BINLIBDEST": (fbuildroot,),
|
|
339
|
+
# "CFLAGS": (fbuildroot, ftoolchain),
|
|
340
|
+
# "CPPLAGS": (fbuildroot, ftoolchain),
|
|
341
|
+
# "CXXFLAGS": (fbuildroot, ftoolchain),
|
|
342
|
+
# "datarootdir": (fbuildroot,),
|
|
343
|
+
# "exec_prefix": (fbuildroot,),
|
|
344
|
+
# "LDFLAGS": (fbuildroot, ftoolchain),
|
|
345
|
+
# "LDSHARED": (fbuildroot, ftoolchain),
|
|
346
|
+
# "LIBDEST": (fbuildroot,),
|
|
347
|
+
# "prefix": (fbuildroot,),
|
|
348
|
+
# "SCRIPTDIR": (fbuildroot,),
|
|
349
|
+
# }
|
|
350
|
+
for key in sorted(mod.build_time_vars):
|
|
351
|
+
val = mod.build_time_vars[key]
|
|
352
|
+
if isinstance(val, str):
|
|
353
|
+
for _ in (fbuildroot, ftoolchain):
|
|
354
|
+
val = _(val)
|
|
355
|
+
log.info("SYSCONFIG [%s] %s => %s", key, mod.build_time_vars[key], val)
|
|
356
|
+
data[key] = val
|
|
357
|
+
|
|
358
|
+
sysconfigdata_code = _load_sysconfigdata_template()
|
|
359
|
+
with open(destfile, "w", encoding="utf8") as f:
|
|
360
|
+
f.write(
|
|
361
|
+
"# system configuration generated and used by" " the relenv at runtime\n"
|
|
362
|
+
)
|
|
363
|
+
f.write("_build_time_vars = ")
|
|
364
|
+
pprint.pprint(data, stream=f)
|
|
365
|
+
f.write(sysconfigdata_code)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def find_sysconfigdata(pymodules: PathLike) -> str:
|
|
369
|
+
"""
|
|
370
|
+
Find sysconfigdata directory for python installation.
|
|
371
|
+
|
|
372
|
+
:param pymodules: Path to python modules (e.g. lib/python3.10)
|
|
373
|
+
:type pymodules: str
|
|
374
|
+
|
|
375
|
+
:return: The name of the sysconig data module
|
|
376
|
+
:rtype: str
|
|
377
|
+
"""
|
|
378
|
+
for root, dirs, files in os.walk(pymodules):
|
|
379
|
+
for file in files:
|
|
380
|
+
if file.find("sysconfigdata") > -1 and file.endswith(".py"):
|
|
381
|
+
return file[:-3]
|
|
382
|
+
raise MissingDependencyError("Unable to locate sysconfigdata module")
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def install_runtime(sitepackages: PathLike) -> None:
|
|
386
|
+
"""
|
|
387
|
+
Install a base relenv runtime.
|
|
388
|
+
"""
|
|
389
|
+
site_dir = pathlib.Path(sitepackages)
|
|
390
|
+
relenv_pth = site_dir / "relenv.pth"
|
|
391
|
+
with io.open(str(relenv_pth), "w") as fp:
|
|
392
|
+
fp.write(RELENV_PTH)
|
|
393
|
+
|
|
394
|
+
# Lay down relenv.runtime, we'll pip install the rest later
|
|
395
|
+
relenv = site_dir / "relenv"
|
|
396
|
+
os.makedirs(relenv, exist_ok=True)
|
|
397
|
+
|
|
398
|
+
for name in [
|
|
399
|
+
"runtime.py",
|
|
400
|
+
"relocate.py",
|
|
401
|
+
"common.py",
|
|
402
|
+
"buildenv.py",
|
|
403
|
+
"__init__.py",
|
|
404
|
+
]:
|
|
405
|
+
src = MODULE_DIR / name
|
|
406
|
+
dest = relenv / name
|
|
407
|
+
with io.open(src, "r") as rfp:
|
|
408
|
+
with io.open(dest, "w") as wfp:
|
|
409
|
+
wfp.write(rfp.read())
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def finalize(
|
|
413
|
+
env: MutableMapping[str, str],
|
|
414
|
+
dirs: Dirs,
|
|
415
|
+
logfp: IO[str],
|
|
416
|
+
) -> None:
|
|
417
|
+
"""
|
|
418
|
+
Run after we've fully built python.
|
|
419
|
+
|
|
420
|
+
This method enhances the newly created python with Relenv's runtime hacks.
|
|
421
|
+
|
|
422
|
+
:param env: The environment dictionary
|
|
423
|
+
:type env: dict
|
|
424
|
+
:param dirs: The working directories
|
|
425
|
+
:type dirs: ``relenv.build.common.Dirs``
|
|
426
|
+
:param logfp: A handle for the log file
|
|
427
|
+
:type logfp: file
|
|
428
|
+
"""
|
|
429
|
+
# Run relok8 to make sure the rpaths are relocatable.
|
|
430
|
+
relenv.relocate.main(dirs.prefix, log_file_name=str(dirs.logs / "relocate.py.log"))
|
|
431
|
+
# Install relenv-sysconfigdata module
|
|
432
|
+
libdir = pathlib.Path(dirs.prefix) / "lib"
|
|
433
|
+
|
|
434
|
+
def find_pythonlib(libdir: pathlib.Path) -> Optional[str]:
|
|
435
|
+
for _root, dirs, _files in os.walk(libdir):
|
|
436
|
+
for entry in dirs:
|
|
437
|
+
if entry.startswith("python"):
|
|
438
|
+
return entry
|
|
439
|
+
return None
|
|
440
|
+
|
|
441
|
+
python_lib = find_pythonlib(libdir)
|
|
442
|
+
if python_lib is None:
|
|
443
|
+
raise MissingDependencyError("Unable to locate python library directory")
|
|
444
|
+
|
|
445
|
+
pymodules = libdir / python_lib
|
|
446
|
+
|
|
447
|
+
# update ensurepip
|
|
448
|
+
update_ensurepip(pymodules)
|
|
449
|
+
|
|
450
|
+
cwd = os.getcwd()
|
|
451
|
+
modname = find_sysconfigdata(pymodules)
|
|
452
|
+
path = sys.path
|
|
453
|
+
sys.path = [str(pymodules)]
|
|
454
|
+
try:
|
|
455
|
+
mod = __import__(str(modname))
|
|
456
|
+
finally:
|
|
457
|
+
os.chdir(cwd)
|
|
458
|
+
sys.path = path
|
|
459
|
+
|
|
460
|
+
dest = pymodules / f"{modname}.py"
|
|
461
|
+
install_sysdata(mod, dest, dirs.prefix, dirs.toolchain)
|
|
462
|
+
|
|
463
|
+
# Lay down site customize
|
|
464
|
+
bindir = pathlib.Path(dirs.prefix) / "bin"
|
|
465
|
+
sitepackages = pymodules / "site-packages"
|
|
466
|
+
install_runtime(sitepackages)
|
|
467
|
+
|
|
468
|
+
# Install pip
|
|
469
|
+
python_exe = str(dirs.prefix / "bin" / "python3")
|
|
470
|
+
if env["RELENV_HOST_ARCH"] != env["RELENV_BUILD_ARCH"]:
|
|
471
|
+
env["RELENV_CROSS"] = str(dirs.prefix)
|
|
472
|
+
python_exe = env["RELENV_NATIVE_PY"]
|
|
473
|
+
logfp.write("\nRUN ENSURE PIP\n")
|
|
474
|
+
|
|
475
|
+
env.pop("RELENV_BUILDENV")
|
|
476
|
+
|
|
477
|
+
runcmd(
|
|
478
|
+
[python_exe, "-m", "ensurepip"],
|
|
479
|
+
env=env,
|
|
480
|
+
stderr=logfp,
|
|
481
|
+
stdout=logfp,
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
# Fix the shebangs in the scripts python layed down. Order matters.
|
|
485
|
+
shebangs = [
|
|
486
|
+
"#!{}".format(bindir / f"python{env['RELENV_PY_MAJOR_VERSION']}"),
|
|
487
|
+
"#!{}".format(
|
|
488
|
+
bindir / f"python{env['RELENV_PY_MAJOR_VERSION'].split('.', 1)[0]}"
|
|
489
|
+
),
|
|
490
|
+
]
|
|
491
|
+
newshebang = format_shebang("/python3")
|
|
492
|
+
for shebang in shebangs:
|
|
493
|
+
log.info("Patch shebang %r with %r", shebang, newshebang)
|
|
494
|
+
patch_shebangs(
|
|
495
|
+
str(pathlib.Path(dirs.prefix) / "bin"),
|
|
496
|
+
shebang,
|
|
497
|
+
newshebang,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
if sys.platform == "linux":
|
|
501
|
+
pyconf = f"config-{env['RELENV_PY_MAJOR_VERSION']}-{env['RELENV_HOST']}"
|
|
502
|
+
patch_shebang(
|
|
503
|
+
str(pymodules / pyconf / "python-config.py"),
|
|
504
|
+
"#!{}".format(str(bindir / f"python{env['RELENV_PY_MAJOR_VERSION']}")),
|
|
505
|
+
format_shebang("../../../bin/python3"),
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
toolchain_path = dirs.toolchain
|
|
509
|
+
if toolchain_path is None:
|
|
510
|
+
raise MissingDependencyError("Toolchain path is required for linux builds")
|
|
511
|
+
shutil.copy(
|
|
512
|
+
pathlib.Path(toolchain_path)
|
|
513
|
+
/ env["RELENV_HOST"]
|
|
514
|
+
/ "sysroot"
|
|
515
|
+
/ "lib"
|
|
516
|
+
/ "libstdc++.so.6",
|
|
517
|
+
libdir,
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
# Moved in python 3.13 or removed?
|
|
521
|
+
if (pymodules / "cgi.py").exists():
|
|
522
|
+
patch_shebang(
|
|
523
|
+
str(pymodules / "cgi.py"),
|
|
524
|
+
"#! /usr/local/bin/python",
|
|
525
|
+
format_shebang("../../bin/python3"),
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
def runpip(pkg: Union[str, os.PathLike[str]], upgrade: bool = False) -> None:
|
|
529
|
+
logfp.write(f"\nRUN PIP {pkg} {upgrade}\n")
|
|
530
|
+
target: Optional[pathlib.Path] = None
|
|
531
|
+
python_exe = str(dirs.prefix / "bin" / "python3")
|
|
532
|
+
if sys.platform == LINUX:
|
|
533
|
+
if env["RELENV_HOST_ARCH"] != env["RELENV_BUILD_ARCH"]:
|
|
534
|
+
target = pymodules / "site-packages"
|
|
535
|
+
python_exe = env["RELENV_NATIVE_PY"]
|
|
536
|
+
cmd = [
|
|
537
|
+
python_exe,
|
|
538
|
+
"-m",
|
|
539
|
+
"pip",
|
|
540
|
+
"install",
|
|
541
|
+
str(pkg),
|
|
542
|
+
]
|
|
543
|
+
if upgrade:
|
|
544
|
+
cmd.append("--upgrade")
|
|
545
|
+
if target:
|
|
546
|
+
cmd.append("--target={}".format(target))
|
|
547
|
+
runcmd(cmd, env=env, stderr=logfp, stdout=logfp)
|
|
548
|
+
|
|
549
|
+
runpip("wheel")
|
|
550
|
+
# This needs to handle running from the root of the git repo and also from
|
|
551
|
+
# an installed Relenv
|
|
552
|
+
if (MODULE_DIR.parent / ".git").exists():
|
|
553
|
+
runpip(MODULE_DIR.parent, upgrade=True)
|
|
554
|
+
else:
|
|
555
|
+
runpip("relenv", upgrade=True)
|
|
556
|
+
globs = [
|
|
557
|
+
"/bin/python*",
|
|
558
|
+
"/bin/pip*",
|
|
559
|
+
"/bin/relenv",
|
|
560
|
+
"/lib/python*/ensurepip/*",
|
|
561
|
+
"/lib/python*/site-packages/*",
|
|
562
|
+
"/include/*",
|
|
563
|
+
"*.so",
|
|
564
|
+
"/lib/*.so.*",
|
|
565
|
+
"*.py",
|
|
566
|
+
# Mac specific, factor this out
|
|
567
|
+
"*.dylib",
|
|
568
|
+
]
|
|
569
|
+
archive = f"{ dirs.prefix }.tar.xz"
|
|
570
|
+
log.info("Archive is %s", archive)
|
|
571
|
+
with tarfile.open(archive, mode="w:xz") as fp:
|
|
572
|
+
create_archive(fp, dirs.prefix, globs, logfp)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def create_archive(
|
|
576
|
+
tarfp: tarfile.TarFile,
|
|
577
|
+
toarchive: PathLike,
|
|
578
|
+
globs: Sequence[str],
|
|
579
|
+
logfp: Optional[IO[str]] = None,
|
|
580
|
+
) -> None:
|
|
581
|
+
"""
|
|
582
|
+
Create an archive.
|
|
583
|
+
|
|
584
|
+
:param tarfp: A pointer to the archive to be created
|
|
585
|
+
:type tarfp: file
|
|
586
|
+
:param toarchive: The path to the directory to archive
|
|
587
|
+
:type toarchive: str
|
|
588
|
+
:param globs: A list of filtering patterns to match against files to be added
|
|
589
|
+
:type globs: list
|
|
590
|
+
:param logfp: A pointer to the log file
|
|
591
|
+
:type logfp: file
|
|
592
|
+
"""
|
|
593
|
+
log.debug("Current directory %s", os.getcwd())
|
|
594
|
+
log.debug("Creating archive %s", tarfp.name)
|
|
595
|
+
for root, _dirs, files in os.walk(toarchive):
|
|
596
|
+
relroot = pathlib.Path(root).relative_to(toarchive)
|
|
597
|
+
for f in files:
|
|
598
|
+
relpath = relroot / f
|
|
599
|
+
matches = False
|
|
600
|
+
for g in globs:
|
|
601
|
+
candidate = pathlib.Path("/") / relpath
|
|
602
|
+
if fnmatch.fnmatch(str(candidate), g):
|
|
603
|
+
matches = True
|
|
604
|
+
break
|
|
605
|
+
if matches:
|
|
606
|
+
log.debug("Adding %s", relpath)
|
|
607
|
+
tarfp.add(relpath, arcname=str(relpath), recursive=False)
|
|
608
|
+
else:
|
|
609
|
+
log.debug("Skipping %s", relpath)
|