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.
Files changed (49) hide show
  1. relenv/__init__.py +14 -2
  2. relenv/__main__.py +12 -6
  3. relenv/_resources/xz/config.h +148 -0
  4. relenv/_resources/xz/readme.md +4 -0
  5. relenv/build/__init__.py +28 -30
  6. relenv/build/common/__init__.py +50 -0
  7. relenv/build/common/_sysconfigdata_template.py +72 -0
  8. relenv/build/common/builder.py +907 -0
  9. relenv/build/common/builders.py +163 -0
  10. relenv/build/common/download.py +324 -0
  11. relenv/build/common/install.py +609 -0
  12. relenv/build/common/ui.py +432 -0
  13. relenv/build/darwin.py +128 -14
  14. relenv/build/linux.py +296 -78
  15. relenv/build/windows.py +259 -44
  16. relenv/buildenv.py +48 -17
  17. relenv/check.py +10 -5
  18. relenv/common.py +499 -163
  19. relenv/create.py +147 -7
  20. relenv/fetch.py +16 -4
  21. relenv/manifest.py +15 -7
  22. relenv/python-versions.json +329 -0
  23. relenv/pyversions.py +817 -30
  24. relenv/relocate.py +101 -55
  25. relenv/runtime.py +452 -253
  26. relenv/toolchain.py +9 -3
  27. {relenv-0.21.1.dist-info → relenv-0.22.0.dist-info}/METADATA +1 -1
  28. relenv-0.22.0.dist-info/RECORD +48 -0
  29. tests/__init__.py +2 -0
  30. tests/_pytest_typing.py +45 -0
  31. tests/conftest.py +42 -36
  32. tests/test_build.py +426 -9
  33. tests/test_common.py +311 -48
  34. tests/test_create.py +149 -6
  35. tests/test_downloads.py +19 -15
  36. tests/test_fips_photon.py +6 -3
  37. tests/test_module_imports.py +44 -0
  38. tests/test_pyversions_runtime.py +177 -0
  39. tests/test_relocate.py +45 -39
  40. tests/test_relocate_module.py +257 -0
  41. tests/test_runtime.py +1802 -6
  42. tests/test_verify_build.py +500 -34
  43. relenv/build/common.py +0 -1609
  44. relenv-0.21.1.dist-info/RECORD +0 -35
  45. {relenv-0.21.1.dist-info → relenv-0.22.0.dist-info}/WHEEL +0 -0
  46. {relenv-0.21.1.dist-info → relenv-0.22.0.dist-info}/entry_points.txt +0 -0
  47. {relenv-0.21.1.dist-info → relenv-0.22.0.dist-info}/licenses/LICENSE.md +0 -0
  48. {relenv-0.21.1.dist-info → relenv-0.22.0.dist-info}/licenses/NOTICE +0 -0
  49. {relenv-0.21.1.dist-info → relenv-0.22.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,163 @@
1
+ # Copyright 2022-2025 Broadcom.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """
4
+ Build functions for specific dependencies.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import pathlib
9
+ import shutil
10
+ import sys
11
+ from typing import IO, MutableMapping, TYPE_CHECKING
12
+
13
+ from relenv.common import PlatformError, runcmd
14
+
15
+ if TYPE_CHECKING:
16
+ from .builder import Dirs
17
+
18
+
19
+ def build_default(env: MutableMapping[str, str], dirs: Dirs, logfp: IO[str]) -> None:
20
+ """
21
+ The default build function if none is given during the build process.
22
+
23
+ :param env: The environment dictionary
24
+ :type env: dict
25
+ :param dirs: The working directories
26
+ :type dirs: ``relenv.build.common.Dirs``
27
+ :param logfp: A handle for the log file
28
+ :type logfp: file
29
+ """
30
+ cmd = [
31
+ "./configure",
32
+ "--prefix={}".format(dirs.prefix),
33
+ ]
34
+ if env["RELENV_HOST"].find("linux") > -1:
35
+ cmd += [
36
+ "--build={}".format(env["RELENV_BUILD"]),
37
+ "--host={}".format(env["RELENV_HOST"]),
38
+ ]
39
+ runcmd(cmd, env=env, stderr=logfp, stdout=logfp)
40
+ runcmd(["make", "-j8"], env=env, stderr=logfp, stdout=logfp)
41
+ runcmd(["make", "install"], env=env, stderr=logfp, stdout=logfp)
42
+
43
+
44
+ def build_openssl_fips(
45
+ env: MutableMapping[str, str], dirs: Dirs, logfp: IO[str]
46
+ ) -> None:
47
+ """Build OpenSSL with FIPS module."""
48
+ return build_openssl(env, dirs, logfp, fips=True)
49
+
50
+
51
+ def build_openssl(
52
+ env: MutableMapping[str, str],
53
+ dirs: Dirs,
54
+ logfp: IO[str],
55
+ fips: bool = False,
56
+ ) -> None:
57
+ """
58
+ Build openssl.
59
+
60
+ :param env: The environment dictionary
61
+ :type env: dict
62
+ :param dirs: The working directories
63
+ :type dirs: ``relenv.build.common.Dirs``
64
+ :param logfp: A handle for the log file
65
+ :type logfp: file
66
+ """
67
+ arch = "aarch64"
68
+ if sys.platform == "darwin":
69
+ plat = "darwin64"
70
+ if env["RELENV_HOST_ARCH"] == "x86_64":
71
+ arch = "x86_64-cc"
72
+ elif env["RELENV_HOST_ARCH"] == "arm64":
73
+ arch = "arm64-cc"
74
+ else:
75
+ raise PlatformError(f"Unable to build {env['RELENV_HOST_ARCH']}")
76
+ extended_cmd = []
77
+ else:
78
+ plat = "linux"
79
+ if env["RELENV_HOST_ARCH"] == "x86_64":
80
+ arch = "x86_64"
81
+ elif env["RELENV_HOST_ARCH"] == "aarch64":
82
+ arch = "aarch64"
83
+ else:
84
+ raise PlatformError(f"Unable to build {env['RELENV_HOST_ARCH']}")
85
+ extended_cmd = [
86
+ "-Wl,-z,noexecstack",
87
+ ]
88
+ if fips:
89
+ extended_cmd.append("enable-fips")
90
+ cmd = [
91
+ "./Configure",
92
+ f"{plat}-{arch}",
93
+ f"--prefix={dirs.prefix}",
94
+ "--openssldir=/etc/ssl",
95
+ "--libdir=lib",
96
+ "--api=1.1.1",
97
+ "--shared",
98
+ "--with-rand-seed=os,egd",
99
+ "enable-md2",
100
+ "enable-egd",
101
+ "no-idea",
102
+ ]
103
+ cmd.extend(extended_cmd)
104
+ runcmd(
105
+ cmd,
106
+ env=env,
107
+ stderr=logfp,
108
+ stdout=logfp,
109
+ )
110
+ runcmd(["make", "-j8"], env=env, stderr=logfp, stdout=logfp)
111
+ if fips:
112
+ shutil.copy(
113
+ pathlib.Path("providers") / "fips.so",
114
+ pathlib.Path(dirs.prefix) / "lib" / "ossl-modules",
115
+ )
116
+ else:
117
+ runcmd(["make", "install_sw"], env=env, stderr=logfp, stdout=logfp)
118
+
119
+
120
+ def build_sqlite(env: MutableMapping[str, str], dirs: Dirs, logfp: IO[str]) -> None:
121
+ """
122
+ Build sqlite.
123
+
124
+ :param env: The environment dictionary
125
+ :type env: dict
126
+ :param dirs: The working directories
127
+ :type dirs: ``relenv.build.common.Dirs``
128
+ :param logfp: A handle for the log file
129
+ :type logfp: file
130
+ """
131
+ # extra_cflags=('-Os '
132
+ # '-DSQLITE_ENABLE_FTS5 '
133
+ # '-DSQLITE_ENABLE_FTS4 '
134
+ # '-DSQLITE_ENABLE_FTS3_PARENTHESIS '
135
+ # '-DSQLITE_ENABLE_JSON1 '
136
+ # '-DSQLITE_ENABLE_RTREE '
137
+ # '-DSQLITE_TCL=0 '
138
+ # )
139
+ # configure_pre=[
140
+ # '--enable-threadsafe',
141
+ # '--enable-shared=no',
142
+ # '--enable-static=yes',
143
+ # '--disable-readline',
144
+ # '--disable-dependency-tracking',
145
+ # ]
146
+ cmd = [
147
+ "./configure",
148
+ # "--with-shared",
149
+ # "--without-static",
150
+ "--enable-threadsafe",
151
+ "--disable-readline",
152
+ "--disable-dependency-tracking",
153
+ "--prefix={}".format(dirs.prefix),
154
+ # "--enable-add-ons=nptl,ports",
155
+ ]
156
+ if env["RELENV_HOST"].find("linux") > -1:
157
+ cmd += [
158
+ "--build={}".format(env["RELENV_BUILD_ARCH"]),
159
+ "--host={}".format(env["RELENV_HOST"]),
160
+ ]
161
+ runcmd(cmd, env=env, stderr=logfp, stdout=logfp)
162
+ runcmd(["make", "-j8"], env=env, stderr=logfp, stdout=logfp)
163
+ runcmd(["make", "install"], env=env, stderr=logfp, stdout=logfp)
@@ -0,0 +1,324 @@
1
+ # Copyright 2022-2025 Broadcom.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """
4
+ Download utility class for fetching build dependencies.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import hashlib
9
+ import logging
10
+ import os
11
+ import pathlib
12
+ import subprocess
13
+ import sys
14
+ from typing import Callable, Optional, Tuple, Union
15
+
16
+ from relenv.common import (
17
+ RelenvException,
18
+ ConfigurationError,
19
+ ChecksumValidationError,
20
+ download_url,
21
+ get_download_location,
22
+ runcmd,
23
+ )
24
+
25
+ # Type alias for path-like objects
26
+ PathLike = Union[str, os.PathLike[str]]
27
+
28
+ # Environment flag for CI/CD detection
29
+ CICD = "CI" in os.environ
30
+
31
+ log = logging.getLogger(__name__)
32
+
33
+
34
+ def verify_checksum(file: PathLike, checksum: Optional[str]) -> bool:
35
+ """
36
+ Verify the checksum of a file.
37
+
38
+ Supports both SHA-1 (40 hex chars) and SHA-256 (64 hex chars) checksums.
39
+ The hash algorithm is auto-detected based on checksum length.
40
+
41
+ :param file: The path to the file to check.
42
+ :type file: str
43
+ :param checksum: The checksum to verify against (SHA-1 or SHA-256)
44
+ :type checksum: str
45
+
46
+ :raises RelenvException: If the checksum verification failed
47
+
48
+ :return: True if it succeeded, or False if the checksum was None
49
+ :rtype: bool
50
+ """
51
+ if checksum is None:
52
+ log.error("Can't verify checksum because none was given")
53
+ return False
54
+
55
+ # Auto-detect hash type based on length
56
+ # SHA-1: 40 hex chars, SHA-256: 64 hex chars
57
+ if len(checksum) == 64:
58
+ hash_algo = hashlib.sha256()
59
+ hash_name = "sha256"
60
+ elif len(checksum) == 40:
61
+ hash_algo = hashlib.sha1()
62
+ hash_name = "sha1"
63
+ else:
64
+ raise ChecksumValidationError(
65
+ f"Invalid checksum length {len(checksum)}. Expected 40 (SHA-1) or 64 (SHA-256)"
66
+ )
67
+
68
+ with open(file, "rb") as fp:
69
+ hash_algo.update(fp.read())
70
+ file_checksum = hash_algo.hexdigest()
71
+ if checksum != file_checksum:
72
+ raise ChecksumValidationError(
73
+ f"{hash_name} checksum verification failed. expected={checksum} found={file_checksum}"
74
+ )
75
+ return True
76
+
77
+
78
+ class Download:
79
+ """
80
+ A utility that holds information about content to be downloaded.
81
+
82
+ :param name: The name of the download
83
+ :type name: str
84
+ :param url: The url of the download
85
+ :type url: str
86
+ :param signature: The signature of the download, defaults to None
87
+ :type signature: str
88
+ :param destination: The path to download the file to
89
+ :type destination: str
90
+ :param version: The version of the content to download
91
+ :type version: str
92
+ :param sha1: The sha1 sum of the download
93
+ :type sha1: str
94
+
95
+ """
96
+
97
+ def __init__(
98
+ self,
99
+ name: str,
100
+ url: str,
101
+ fallback_url: Optional[str] = None,
102
+ signature: Optional[str] = None,
103
+ destination: PathLike = "",
104
+ version: str = "",
105
+ checksum: Optional[str] = None,
106
+ ) -> None:
107
+ self.name = name
108
+ self.url_tpl = url
109
+ self.fallback_url_tpl = fallback_url
110
+ self.signature_tpl = signature
111
+ self._destination: pathlib.Path = pathlib.Path()
112
+ if destination:
113
+ self._destination = pathlib.Path(destination)
114
+ self.version = version
115
+ self.checksum = checksum
116
+
117
+ def copy(self) -> "Download":
118
+ """Create a copy of this Download instance."""
119
+ return Download(
120
+ self.name,
121
+ self.url_tpl,
122
+ self.fallback_url_tpl,
123
+ self.signature_tpl,
124
+ self.destination,
125
+ self.version,
126
+ self.checksum,
127
+ )
128
+
129
+ @property
130
+ def destination(self) -> pathlib.Path:
131
+ """Get the destination directory path."""
132
+ return self._destination
133
+
134
+ @destination.setter
135
+ def destination(self, value: Optional[PathLike]) -> None:
136
+ """Set the destination directory path."""
137
+ if value:
138
+ self._destination = pathlib.Path(value)
139
+ else:
140
+ self._destination = pathlib.Path()
141
+
142
+ @property
143
+ def url(self) -> str:
144
+ """Get the formatted download URL."""
145
+ return self.url_tpl.format(version=self.version)
146
+
147
+ @property
148
+ def fallback_url(self) -> Optional[str]:
149
+ """Get the formatted fallback URL if configured."""
150
+ if self.fallback_url_tpl:
151
+ return self.fallback_url_tpl.format(version=self.version)
152
+ return None
153
+
154
+ @property
155
+ def signature_url(self) -> str:
156
+ """Get the formatted signature URL."""
157
+ if self.signature_tpl is None:
158
+ raise ConfigurationError("Signature template not configured")
159
+ return self.signature_tpl.format(version=self.version)
160
+
161
+ @property
162
+ def filepath(self) -> pathlib.Path:
163
+ """Get the full file path where the download will be saved."""
164
+ _, name = self.url.rsplit("/", 1)
165
+ return self.destination / name
166
+
167
+ @property
168
+ def formatted_url(self) -> str:
169
+ """Get the formatted URL (alias for url property)."""
170
+ return self.url_tpl.format(version=self.version)
171
+
172
+ def fetch_file(
173
+ self, progress_callback: Optional[Callable[[int, int], None]] = None
174
+ ) -> Tuple[str, bool]:
175
+ """
176
+ Download the file.
177
+
178
+ :param progress_callback: Optional callback(downloaded_bytes, total_bytes)
179
+ :type progress_callback: Optional[Callable[[int, int], None]]
180
+ :return: The path to the downloaded content, and whether it was downloaded.
181
+ :rtype: tuple(str, bool)
182
+ """
183
+ try:
184
+ return (
185
+ download_url(
186
+ self.url,
187
+ self.destination,
188
+ CICD,
189
+ progress_callback=progress_callback,
190
+ ),
191
+ True,
192
+ )
193
+ except Exception as exc:
194
+ fallback = self.fallback_url
195
+ if fallback:
196
+ print(f"Download failed {self.url} ({exc}); trying fallback url")
197
+ return (
198
+ download_url(
199
+ fallback,
200
+ self.destination,
201
+ CICD,
202
+ progress_callback=progress_callback,
203
+ ),
204
+ True,
205
+ )
206
+ raise
207
+
208
+ def fetch_signature(self, version: Optional[str] = None) -> Tuple[str, bool]:
209
+ """
210
+ Download the file signature.
211
+
212
+ :return: The path to the downloaded signature.
213
+ :rtype: str
214
+ """
215
+ return download_url(self.signature_url, self.destination, CICD), True
216
+
217
+ def exists(self) -> bool:
218
+ """
219
+ True when the artifact already exists on disk.
220
+
221
+ :return: True when the artifact already exists on disk
222
+ :rtype: bool
223
+ """
224
+ return self.filepath.exists()
225
+
226
+ def valid_hash(self) -> None:
227
+ """Validate the hash of the downloaded file (placeholder method)."""
228
+ pass
229
+
230
+ @staticmethod
231
+ def validate_signature(archive: PathLike, signature: Optional[PathLike]) -> bool:
232
+ """
233
+ True when the archive's signature is valid.
234
+
235
+ :param archive: The path to the archive to validate
236
+ :type archive: str
237
+ :param signature: The path to the signature to validate against
238
+ :type signature: str
239
+
240
+ :return: True if it validated properly, else False
241
+ :rtype: bool
242
+ """
243
+ if signature is None:
244
+ log.error("Can't check signature because none was given")
245
+ return False
246
+ try:
247
+ runcmd(
248
+ ["gpg", "--verify", signature, archive],
249
+ stderr=subprocess.PIPE,
250
+ stdout=subprocess.PIPE,
251
+ )
252
+ return True
253
+ except RelenvException as exc:
254
+ log.error("Signature validation failed on %s: %s", archive, exc)
255
+ return False
256
+
257
+ @staticmethod
258
+ def validate_checksum(archive: PathLike, checksum: Optional[str]) -> bool:
259
+ """
260
+ True when when the archive matches the sha1 hash.
261
+
262
+ :param archive: The path to the archive to validate
263
+ :type archive: str
264
+ :param checksum: The sha1 sum to validate against
265
+ :type checksum: str
266
+ :return: True if the sums matched, else False
267
+ :rtype: bool
268
+ """
269
+ try:
270
+ verify_checksum(archive, checksum)
271
+ return True
272
+ except RelenvException as exc:
273
+ log.error("sha1 validation failed on %s: %s", archive, exc)
274
+ return False
275
+
276
+ def __call__(
277
+ self,
278
+ force_download: bool = False,
279
+ show_ui: bool = False,
280
+ exit_on_failure: bool = False,
281
+ progress_callback: Optional[Callable[[int, int], None]] = None,
282
+ ) -> bool:
283
+ """
284
+ Downloads the url and validates the signature and sha1 sum.
285
+
286
+ :param progress_callback: Optional callback(downloaded_bytes, total_bytes)
287
+ :type progress_callback: Optional[Callable[[int, int], None]]
288
+ :return: Whether or not validation succeeded
289
+ :rtype: bool
290
+ """
291
+ os.makedirs(self.filepath.parent, exist_ok=True)
292
+
293
+ downloaded = False
294
+ if force_download:
295
+ _, downloaded = self.fetch_file(progress_callback)
296
+ else:
297
+ file_is_valid = False
298
+ dest = get_download_location(self.url, self.destination)
299
+ if self.checksum and os.path.exists(dest):
300
+ file_is_valid = self.validate_checksum(dest, self.checksum)
301
+ if file_is_valid:
302
+ log.debug("%s already downloaded, skipping.", self.url)
303
+ else:
304
+ _, downloaded = self.fetch_file(progress_callback)
305
+ valid = True
306
+ if downloaded:
307
+ if self.signature_tpl is not None:
308
+ sig, _ = self.fetch_signature()
309
+ valid_sig = self.validate_signature(self.filepath, sig)
310
+ valid = valid and valid_sig
311
+ if self.checksum is not None:
312
+ valid_checksum = self.validate_checksum(self.filepath, self.checksum)
313
+ valid = valid and valid_checksum
314
+
315
+ if not valid:
316
+ log.warning("Checksum did not match %s: %s", self.name, self.checksum)
317
+ if show_ui:
318
+ sys.stderr.write(
319
+ f"\nChecksum did not match {self.name}: {self.checksum}\n"
320
+ )
321
+ sys.stderr.flush()
322
+ if exit_on_failure and not valid:
323
+ sys.exit(1)
324
+ return valid