relenv 0.19.4__py3-none-any.whl → 0.20.1__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/pyversions.py ADDED
@@ -0,0 +1,411 @@
1
+ # Copyright 2025 Broadcom.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """
4
+ Versions utility.
5
+ """
6
+ # try:
7
+ # from packaging.version import Version
8
+ # except ImportError:
9
+ # raise RuntimeError(
10
+ # "Required dependencies not found. Please pip install relenv[pyversions]"
11
+ # )
12
+ #
13
+
14
+ import hashlib
15
+ import json
16
+ import logging
17
+ import os
18
+ import pathlib
19
+ import re
20
+ import subprocess
21
+ import sys
22
+ import time
23
+
24
+ from relenv.common import check_url, download_url, fetch_url_content
25
+
26
+ log = logging.getLogger(__name__)
27
+
28
+ KEYSERVERS = [
29
+ "keyserver.ubuntu.com",
30
+ "keys.openpgp.org",
31
+ "pgp.mit.edu",
32
+ ]
33
+
34
+ ARCHIVE = "https://www.python.org/ftp/python/{version}/Python-{version}.{ext}"
35
+
36
+
37
+ def _ref_version(x):
38
+ _ = x.split("Python ", 1)[1].split("<", 1)[0]
39
+ return Version(_)
40
+
41
+
42
+ def _ref_path(x):
43
+ return x.split('href="')[1].split('"')[0]
44
+
45
+
46
+ class Version:
47
+ """
48
+ Version comparrisons.
49
+ """
50
+
51
+ def __init__(self, data):
52
+ self.major, self.minor, self.micro = self.parse_string(data)
53
+ self._data = data
54
+
55
+ def __str__(self):
56
+ """
57
+ Version as string.
58
+ """
59
+ _ = f"{self.major}"
60
+ if self.minor is not None:
61
+ _ += f".{self.minor}"
62
+ if self.micro is not None:
63
+ _ += f".{self.micro}"
64
+ # XXX What if minor was None but micro was an int.
65
+ return _
66
+
67
+ @staticmethod
68
+ def parse_string(data):
69
+ """
70
+ Parse a version string into major, minor, and micro integers.
71
+ """
72
+ parts = data.split(".")
73
+ if len(parts) == 1:
74
+ return int(parts[0]), None, None
75
+ elif len(parts) == 2:
76
+ return int(parts[0]), int(parts[1]), None
77
+ elif len(parts) == 3:
78
+ return int(parts[0]), int(parts[1]), int(parts[2])
79
+ else:
80
+ raise RuntimeError("Too many parts to parse")
81
+
82
+ def __eq__(self, other):
83
+ """
84
+ Equality comparrison.
85
+ """
86
+ mymajor = 0 if self.major is None else self.major
87
+ myminor = 0 if self.minor is None else self.minor
88
+ mymicro = 0 if self.micro is None else self.micro
89
+ major = 0 if other.major is None else other.major
90
+ minor = 0 if other.minor is None else other.minor
91
+ micro = 0 if other.micro is None else other.micro
92
+ return mymajor == major and myminor == minor and mymicro == micro
93
+
94
+ def __lt__(self, other):
95
+ """
96
+ Less than comparrison.
97
+ """
98
+ mymajor = 0 if self.major is None else self.major
99
+ myminor = 0 if self.minor is None else self.minor
100
+ mymicro = 0 if self.micro is None else self.micro
101
+ major = 0 if other.major is None else other.major
102
+ minor = 0 if other.minor is None else other.minor
103
+ micro = 0 if other.micro is None else other.micro
104
+ if mymajor < major:
105
+ return True
106
+ elif mymajor == major:
107
+ if myminor < minor:
108
+ return True
109
+ if myminor == minor and mymicro < micro:
110
+ return True
111
+ return False
112
+
113
+ def __le__(self, other):
114
+ """
115
+ Less than or equal to comparrison.
116
+ """
117
+ mymajor = 0 if self.major is None else self.major
118
+ myminor = 0 if self.minor is None else self.minor
119
+ mymicro = 0 if self.micro is None else self.micro
120
+ major = 0 if other.major is None else other.major
121
+ minor = 0 if other.minor is None else other.minor
122
+ micro = 0 if other.micro is None else other.micro
123
+ if mymajor <= major:
124
+ if myminor <= minor:
125
+ if mymicro <= micro:
126
+ return True
127
+ return False
128
+
129
+ def __gt__(self, other):
130
+ """
131
+ Greater than comparrison.
132
+ """
133
+ return not self.__le__(other)
134
+
135
+ def __ge__(self, other):
136
+ """
137
+ Greater than or equal to comparrison.
138
+ """
139
+ return not self.__lt__(other)
140
+
141
+
142
+ def _release_urls(version, gzip=False):
143
+ if gzip:
144
+ tarball = f"https://www.python.org/ftp/python/{version}/Python-{version}.tgz"
145
+ else:
146
+ tarball = f"https://www.python.org/ftp/python/{version}/Python-{version}.tar.xz"
147
+ # No signatures prior to 2.3
148
+ if version < Version("2.3"):
149
+ return tarball, None
150
+ return tarball, f"{tarball}.asc"
151
+
152
+
153
+ def _receive_key(keyid, server):
154
+ proc = subprocess.run(
155
+ ["gpg", "--keyserver", server, "--recv-keys", keyid], capture_output=True
156
+ )
157
+ if proc.returncode == 0:
158
+ return True
159
+ return False
160
+
161
+
162
+ def _get_keyid(proc):
163
+ try:
164
+ err = proc.stderr.decode()
165
+ return err.splitlines()[1].rsplit(" ", 1)[-1]
166
+ except (AttributeError, IndexError):
167
+ return False
168
+
169
+
170
+ def verify_signature(path, signature):
171
+ """
172
+ Verify gpg signature.
173
+ """
174
+ proc = subprocess.run(["gpg", "--verify", signature, path], capture_output=True)
175
+ keyid = _get_keyid(proc)
176
+ if proc.returncode == 0:
177
+ print(f"Valid signature {path} {keyid}")
178
+ return True
179
+ err = proc.stderr.decode()
180
+ if "No public key" in err:
181
+ for server in KEYSERVERS:
182
+ if _receive_key(keyid, server):
183
+ print(f"found public key {keyid} on {server}")
184
+ break
185
+ else:
186
+ print(f"Unable to find key {keyid} on any server")
187
+ else:
188
+ print(f"Signature verification failed {proc.stderr.decode()}")
189
+ return False
190
+ proc = subprocess.run(["gpg", "--verify", signature, path], capture_output=True)
191
+ if proc.returncode == 0:
192
+ print(f"Valid signature {path} {signature}")
193
+ return True
194
+ err = proc.stderr.decode()
195
+ print(f"Signature verification failed {proc.stderr.decode()}")
196
+ return False
197
+
198
+
199
+ PRINT = True
200
+ CHECK = True
201
+ VERSION = None # '3.13.2'
202
+ UPDATE = False
203
+
204
+
205
+ def digest(file):
206
+ """
207
+ SHA-256 digest of file.
208
+ """
209
+ hsh = hashlib.sha256()
210
+ with open(file, "rb") as fp:
211
+ hsh.update(fp.read())
212
+ return hsh.hexdigest()
213
+
214
+
215
+ def _main():
216
+
217
+ pyversions = {"versions": []}
218
+
219
+ vfile = pathlib.Path(".pyversions")
220
+ cfile = pathlib.Path(".content")
221
+ tsfile = pathlib.Path(".ts")
222
+ url = "https://www.python.org/downloads/"
223
+ if not cfile.exists() or not tsfile.exists():
224
+ print("Get downloads page")
225
+ ts = int(time.time())
226
+ content = fetch_url_content(url)
227
+ cfile.write_text(content)
228
+ tsfile.write_text(str(ts))
229
+ elif CHECK:
230
+ ts = int(tsfile.read_text())
231
+ if check_url(url, timestamp=ts):
232
+ print("Get downloads page")
233
+ ts = int(time.time())
234
+ content = fetch_url_content(url)
235
+ cfile.write_text(content)
236
+ tsfile.write_text(str(ts))
237
+ else:
238
+ pyversions = json.loads(vfile.read_text())
239
+ content = cfile.read_text()
240
+ else:
241
+ pyversions = json.loads(vfile.read_text())
242
+ content = cfile.read_text()
243
+
244
+ matched = re.findall(r'<a href="/downloads/.*">Python.*</a>', content)
245
+
246
+ parsed_versions = sorted([_ref_version(_) for _ in matched], reverse=True)
247
+
248
+ versions = [_ for _ in parsed_versions if _.major >= 3]
249
+ cwd = os.getcwd()
250
+
251
+ out = {}
252
+
253
+ for version in versions:
254
+ if VERSION and Version(VERSION) != version:
255
+ continue
256
+
257
+ if PRINT:
258
+ pyversions["versions"].append(str(version))
259
+ print(version)
260
+ continue
261
+
262
+ print(f"Check version {version}")
263
+
264
+ # Prior to 3.2.0 the url format only included major and minor.
265
+ if version <= Version("3.2") and version.micro == 0:
266
+ url_version = Version(f"{version.major}.{version.minor}")
267
+ else:
268
+ url_version = version
269
+
270
+ # No xz archives prior to 3.1.4
271
+ if version >= Version("3.1.4"):
272
+ url = ARCHIVE.format(version=url_version, ext="tar.xz")
273
+ if CHECK:
274
+ check_url(url)
275
+ check_url(f"{url}.asc")
276
+ else:
277
+ path = download_url(url, cwd)
278
+ sig_path = download_url(f"{url}.asc", cwd)
279
+ verified = verify_signature(path, sig_path)
280
+ if verified:
281
+ if str(version) in out:
282
+ out[str(version)][url] = digest(path)
283
+ else:
284
+ out[str(version)] = {url: digest(path)}
285
+
286
+ url = ARCHIVE.format(version=url_version, ext="tgz")
287
+ if CHECK:
288
+ check_url(url)
289
+ # No signatures prior to 2.3
290
+ if version >= Version("2.3"):
291
+ check_url(f"{url}.asc")
292
+ else:
293
+ path = download_url(url, cwd)
294
+ if version >= Version("2.3"):
295
+ sig_path = download_url(f"{url}.asc", cwd)
296
+ verified = verify_signature(path, sig_path)
297
+ if verified:
298
+ if str(version) in out:
299
+ out[str(version)][url] = digest(path)
300
+ else:
301
+ out[str(version)] = {url: digest(path)}
302
+
303
+ if PRINT:
304
+ vfile.write_text(json.dumps(pyversions))
305
+ elif not CHECK and out:
306
+ vfile.write_text(json.dumps(out))
307
+
308
+
309
+ def create_pyversions(path):
310
+ """
311
+ Create python-versions.json file.
312
+ """
313
+ url = "https://www.python.org/downloads/"
314
+ content = fetch_url_content(url)
315
+ matched = re.findall(r'<a href="/downloads/.*">Python.*</a>', content)
316
+ parsed_versions = sorted([_ref_version(_) for _ in matched], reverse=True)
317
+ versions = [_ for _ in parsed_versions if _.major >= 3]
318
+ path.write_text(json.dumps({"versions": [str(_) for _ in versions]}))
319
+
320
+
321
+ def python_versions(minor=None, create=False, update=False):
322
+ """
323
+ List python versions.
324
+ """
325
+ packaged = pathlib.Path(__file__).parent / "python-versions.json"
326
+ local = pathlib.Path("~/.local/relenv/python-versions.json")
327
+
328
+ if create:
329
+ create_pyversions(packaged)
330
+
331
+ if local.exists():
332
+ readfrom = local
333
+ elif packaged.exists():
334
+ readfrom = packaged
335
+ elif create:
336
+ readfrom = packaged
337
+ else:
338
+ raise RuntimeError("No versions file found")
339
+ pyversions = json.loads(readfrom.read_text())
340
+ versions = [Version(_) for _ in pyversions["versions"]]
341
+ if minor:
342
+ mv = Version(minor)
343
+ versions = [_ for _ in versions if _.major == mv.major and _.minor == mv.minor]
344
+ return versions
345
+
346
+
347
+ def setup_parser(subparsers):
348
+ """
349
+ Setup the subparser for the ``versions`` command.
350
+
351
+ :param subparsers: The subparsers object returned from ``add_subparsers``
352
+ :type subparsers: argparse._SubParsersAction
353
+ """
354
+ subparser = subparsers.add_parser(
355
+ "versions",
356
+ description=("Versions utility"),
357
+ )
358
+ subparser.set_defaults(func=main)
359
+ subparser.add_argument(
360
+ "-u",
361
+ "--update",
362
+ default=False,
363
+ action="store_true",
364
+ help="Update versions",
365
+ )
366
+ subparser.add_argument(
367
+ "-l",
368
+ "--list",
369
+ default=False,
370
+ action="store_true",
371
+ help="List versions",
372
+ )
373
+ subparser.add_argument(
374
+ "--version",
375
+ default="3.13",
376
+ type=str,
377
+ help="The python version [default: %(default)s]",
378
+ )
379
+
380
+
381
+ def main(args):
382
+ """
383
+ Versions utility main method.
384
+ """
385
+ if args.update:
386
+ python_versions(create=True)
387
+ if args.list:
388
+ for version in python_versions():
389
+ print(version)
390
+ sys.exit()
391
+ if args.version:
392
+ requested = Version(args.version)
393
+
394
+ if requested.micro:
395
+ pyversions = python_versions()
396
+ if requested not in pyversions:
397
+ print(f"Unknown version {requested}")
398
+ sys.exit(1)
399
+ build_version = requested
400
+ else:
401
+ pyversions = python_versions(args.version)
402
+ if not pyversions:
403
+ print(f"Unknown minor version {requested}")
404
+ sys.exit(1)
405
+ build_version = pyversions[0]
406
+ print(build_version)
407
+ sys.exit()
408
+
409
+
410
+ if __name__ == "__main__":
411
+ _main()
relenv/runtime.py CHANGED
@@ -336,6 +336,14 @@ def install_wheel_wrapper(func):
336
336
  if relocate().is_elf(file):
337
337
  debug(f"Relenv - Found elf {file}")
338
338
  relocate().handle_elf(plat / file, rootdir / "lib", True, rootdir)
339
+ elif relocate().is_macho(file):
340
+ otool_bin = shutil.which("otool")
341
+ if otool_bin:
342
+ relocate().handle_macho(str(plat / file), str(rootdir), True)
343
+ else:
344
+ debug(
345
+ "The otool command is not available, please run `xcode-select --install`"
346
+ )
339
347
 
340
348
  return wrapper
341
349
 
@@ -601,8 +609,19 @@ def wrap_pip_build_wheel(name):
601
609
  def wrap(func):
602
610
  @functools.wraps(func)
603
611
  def wrapper(*args, **kwargs):
604
- dirs = common().work_dirs()
605
- toolchain = dirs.toolchain / common().get_triplet()
612
+ if sys.platform != "linux":
613
+ return func(*args, **kwargs)
614
+ if not hasattr(install_cargo_config, "tmpdir") and os.environ.get(
615
+ "RELENV_BUILDENV", 0
616
+ ):
617
+ raise RuntimeError("No toolchain installed")
618
+ cargo_home = install_cargo_config.tmpdir.name
619
+ toolchain = common().get_toolchain()
620
+ if not toolchain:
621
+ if os.environ.get("RELENV_BUILDENV", 0):
622
+ raise RuntimeError("No toolchain installed")
623
+ return func(*args, **kwargs)
624
+
606
625
  if not toolchain.exists():
607
626
  debug("Unable to set CARGO_HOME no toolchain exists")
608
627
  else:
@@ -612,7 +631,6 @@ def wrap_pip_build_wheel(name):
612
631
  f"-C link-arg=-L{relenvroot}/lib "
613
632
  f"-C link-arg=-L{toolchain}/sysroot/lib"
614
633
  )
615
- cargo_home = str(toolchain / "cargo")
616
634
  set_env_if_not_set("CARGO_HOME", cargo_home)
617
635
  set_env_if_not_set("OPENSSL_DIR", relenvroot)
618
636
  set_env_if_not_set("RUSTFLAGS", rustflags)
@@ -697,7 +715,12 @@ def wrap_locations(name):
697
715
 
698
716
  return wrapper
699
717
 
718
+ # get_scheme is not available on pip-19.2.3
719
+ # try:
700
720
  mod.get_scheme = wrap(mod.get_scheme)
721
+ # except AttributeError:
722
+ # debug(f"Module {mod} does not have attribute get_scheme")
723
+
701
724
  return mod
702
725
 
703
726
 
@@ -825,13 +848,28 @@ def install_cargo_config():
825
848
  """
826
849
  if sys.platform != "linux":
827
850
  return
851
+
852
+ # We need this as a late import for python < 3.12 becuase importing it will
853
+ # load the ssl module. Causing out setup_openssl method to fail to load
854
+ # fips module.
855
+ import tempfile
856
+
857
+ install_cargo_config.tmpdir = tempfile.TemporaryDirectory(prefix="relenvcargo")
858
+ cargo_home = pathlib.Path(install_cargo_config.tmpdir.name)
859
+
828
860
  triplet = common().get_triplet()
829
- dirs = common().work_dirs()
830
- toolchain = dirs.toolchain / triplet
861
+ # dirs = common().work_dirs()
862
+
863
+ toolchain = common().get_toolchain()
864
+ if not toolchain:
865
+ debug("Unable to set CARGO_HOME ppbt package not installed")
866
+ return
867
+
831
868
  if not toolchain.exists():
832
869
  debug("Unable to set CARGO_HOME no toolchain exists")
833
870
  return
834
- cargo_home = toolchain / "cargo"
871
+
872
+ # cargo_home = dirs.data / "cargo"
835
873
  if not cargo_home.exists():
836
874
  cargo_home.mkdir()
837
875
  cargo_config = cargo_home / "config.toml"
@@ -856,6 +894,9 @@ def setup_openssl():
856
894
  """
857
895
  Configure openssl certificate locations.
858
896
  """
897
+ if sys.platform == "win32":
898
+ return
899
+
859
900
  openssl_bin = shutil.which("openssl")
860
901
  if not openssl_bin:
861
902
  debug("Could not find the 'openssl' binary in the path")
@@ -868,7 +909,7 @@ def setup_openssl():
868
909
 
869
910
  return
870
911
 
871
- if "OPENSSL_MODULES" not in os.environ and sys.platform != "win32":
912
+ if "OPENSSL_MODULES" not in os.environ:
872
913
  # First try and load the system's fips provider. Then load relenv's
873
914
  # legacy and default providers. The fips provider must be loaded first
874
915
  # in order OpenSSl to work properly..
@@ -908,7 +949,7 @@ def setup_openssl():
908
949
  # Use system openssl dirs
909
950
  # XXX Should we also setup SSL_CERT_FILE, OPENSSL_CONF &
910
951
  # OPENSSL_CONF_INCLUDE?
911
- if "SSL_CERT_DIR" not in os.environ and sys.platform != "win32":
952
+ if "SSL_CERT_DIR" not in os.environ:
912
953
  proc = subprocess.run(
913
954
  [openssl_bin, "version", "-d"],
914
955
  universal_newlines=True,