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,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)