|
18 | 18 | import shutil |
19 | 19 | import sys |
20 | 20 | import tarfile |
| 21 | +import time |
21 | 22 | from types import ModuleType |
22 | | -from typing import IO, MutableMapping, Optional, Sequence, Union, TYPE_CHECKING |
| 23 | +from typing import ( |
| 24 | + Any, |
| 25 | + Dict, |
| 26 | + IO, |
| 27 | + List, |
| 28 | + MutableMapping, |
| 29 | + Optional, |
| 30 | + Sequence, |
| 31 | + Union, |
| 32 | + TYPE_CHECKING, |
| 33 | +) |
23 | 34 |
|
24 | 35 | from relenv.common import ( |
25 | 36 | LINUX, |
@@ -414,31 +425,201 @@ def copy_sbom_files(dirs: Dirs) -> None: |
414 | 425 | Copy SBOM files from Python source to the prefix directory. |
415 | 426 |
|
416 | 427 | SBOM files (Software Bill of Materials) document the build dependencies |
417 | | - and source file checksums. These files are available in Python 3.12+. |
| 428 | + and source file checksums. These files are available in Python 3.12.2+. |
418 | 429 |
|
419 | 430 | :param dirs: The working directories |
420 | 431 | :type dirs: ``relenv.build.common.Dirs`` |
421 | 432 | """ |
422 | | - # Find the Python source directory in dirs.sources |
423 | | - python_source = None |
424 | | - if dirs.sources.exists(): |
425 | | - # Look for Python-{version} directory |
426 | | - for entry in dirs.sources.iterdir(): |
427 | | - if entry.is_dir() and entry.name.startswith("Python-"): |
428 | | - python_source = entry |
429 | | - break |
430 | | - |
431 | | - if python_source: |
432 | | - sbom_files = ["sbom.spdx.json", "externals.spdx.json"] |
433 | | - source_misc_dir = python_source / "Misc" |
434 | | - for sbom_file in sbom_files: |
435 | | - source_sbom = source_misc_dir / sbom_file |
436 | | - if source_sbom.exists(): |
437 | | - dest_sbom = pathlib.Path(dirs.prefix) / sbom_file |
438 | | - shutil.copy2(str(source_sbom), str(dest_sbom)) |
439 | | - log.info("Copied %s to archive", sbom_file) |
440 | | - else: |
441 | | - log.debug("SBOM file %s not found (Python < 3.12?)", sbom_file) |
| 433 | + # Find the Python source directory for the specific version being built |
| 434 | + python_version = dirs.version |
| 435 | + python_source = dirs.sources / f"Python-{python_version}" |
| 436 | + |
| 437 | + if not python_source.exists(): |
| 438 | + log.debug("Python source directory not found: %s", python_source) |
| 439 | + return |
| 440 | + |
| 441 | + sbom_files = ["sbom.spdx.json", "externals.spdx.json"] |
| 442 | + source_misc_dir = python_source / "Misc" |
| 443 | + for sbom_file in sbom_files: |
| 444 | + source_sbom = source_misc_dir / sbom_file |
| 445 | + if source_sbom.exists(): |
| 446 | + dest_sbom = pathlib.Path(dirs.prefix) / sbom_file |
| 447 | + shutil.copy2(str(source_sbom), str(dest_sbom)) |
| 448 | + log.info("Copied %s to archive", sbom_file) |
| 449 | + else: |
| 450 | + log.debug("SBOM file %s not found (Python < 3.12.2?)", sbom_file) |
| 451 | + |
| 452 | + |
| 453 | +def generate_relenv_sbom(env: MutableMapping[str, str], dirs: Dirs) -> None: |
| 454 | + """ |
| 455 | + Generate relenv-sbom.spdx.json documenting actual dependencies in the build. |
| 456 | +
|
| 457 | + This SBOM documents what relenv actually built, which may differ from Python's |
| 458 | + externals.spdx.json (which documents Python's intended/bundled dependencies). |
| 459 | +
|
| 460 | + Only generates SBOM for Python 3.12+ (when Python started including SBOM files). |
| 461 | +
|
| 462 | + :param env: The environment dictionary |
| 463 | + :type env: dict |
| 464 | + :param dirs: The working directories |
| 465 | + :type dirs: ``relenv.build.common.Dirs`` |
| 466 | + """ |
| 467 | + # Only generate SBOM for Python 3.12+ |
| 468 | + python_version = dirs.version |
| 469 | + version_parts = python_version.split(".") |
| 470 | + if len(version_parts) >= 2: |
| 471 | + major = int(version_parts[0]) |
| 472 | + minor = int(version_parts[1]) |
| 473 | + if major < 3 or (major == 3 and minor < 12): |
| 474 | + log.debug( |
| 475 | + "Skipping relenv-sbom.spdx.json generation for Python %s (< 3.12)", |
| 476 | + python_version, |
| 477 | + ) |
| 478 | + return |
| 479 | + |
| 480 | + from .builder import get_dependency_version |
| 481 | + import relenv |
| 482 | + |
| 483 | + platform_map = { |
| 484 | + "linux": "linux", |
| 485 | + "darwin": "darwin", |
| 486 | + "win32": "win32", |
| 487 | + } |
| 488 | + platform = platform_map.get(sys.platform, sys.platform) |
| 489 | + |
| 490 | + # Build dependency list - get versions from python-versions.json |
| 491 | + packages: List[Dict[str, Any]] = [] |
| 492 | + |
| 493 | + # Define dependencies we build (these are the ones relenv compiles) |
| 494 | + # Order matters - list them in a logical grouping |
| 495 | + relenv_deps = [ |
| 496 | + # Compression libraries |
| 497 | + ("bzip2", "https://sourceware.org/pub/bzip2/bzip2-{version}.tar.gz"), |
| 498 | + ( |
| 499 | + "xz", |
| 500 | + "https://github.com/tukaani-project/xz/releases/download/v{version}/xz-{version}.tar.xz", |
| 501 | + ), |
| 502 | + ( |
| 503 | + "zlib", |
| 504 | + "https://github.com/madler/zlib/releases/download/v{version}/zlib-{version}.tar.gz", |
| 505 | + ), |
| 506 | + # Crypto and security |
| 507 | + ( |
| 508 | + "openssl", |
| 509 | + "https://github.com/openssl/openssl/releases/download/openssl-{version}/openssl-{version}.tar.gz", |
| 510 | + ), |
| 511 | + ( |
| 512 | + "libxcrypt", |
| 513 | + "https://github.com/besser82/libxcrypt/releases/download/v{version}/libxcrypt-{version}.tar.xz", |
| 514 | + ), |
| 515 | + # Database |
| 516 | + ("sqlite", "https://sqlite.org/{year}/sqlite-autoconf-{sqliteversion}.tar.gz"), |
| 517 | + ("gdbm", "https://ftp.gnu.org/gnu/gdbm/gdbm-{version}.tar.gz"), |
| 518 | + # Terminal libraries |
| 519 | + ("ncurses", "https://ftp.gnu.org/gnu/ncurses/ncurses-{version}.tar.gz"), |
| 520 | + ("readline", "https://ftp.gnu.org/gnu/readline/readline-{version}.tar.gz"), |
| 521 | + # Other libraries |
| 522 | + ( |
| 523 | + "libffi", |
| 524 | + "https://github.com/libffi/libffi/releases/download/v{version}/libffi-{version}.tar.gz", |
| 525 | + ), |
| 526 | + ( |
| 527 | + "uuid", |
| 528 | + "https://sourceforge.net/projects/libuuid/files/libuuid-{version}.tar.gz", |
| 529 | + ), |
| 530 | + ] |
| 531 | + |
| 532 | + # Linux-specific dependencies |
| 533 | + if sys.platform == "linux": |
| 534 | + relenv_deps.extend( |
| 535 | + [ |
| 536 | + ( |
| 537 | + "tirpc", |
| 538 | + "https://downloads.sourceforge.net/project/libtirpc/" |
| 539 | + "libtirpc/{version}/libtirpc-{version}.tar.bz2", |
| 540 | + ), |
| 541 | + ( |
| 542 | + "krb5", |
| 543 | + "https://kerberos.org/dist/krb5/{major_minor}/krb5-{version}.tar.gz", |
| 544 | + ), |
| 545 | + ] |
| 546 | + ) |
| 547 | + |
| 548 | + for dep_name, url_template in relenv_deps: |
| 549 | + dep_info = get_dependency_version(dep_name, platform) |
| 550 | + if dep_info: |
| 551 | + version = dep_info["version"] |
| 552 | + url = dep_info.get("url", url_template).format( |
| 553 | + version=version, |
| 554 | + sqliteversion=dep_info.get("sqliteversion", ""), |
| 555 | + year=dep_info.get("year", "2025"), |
| 556 | + major_minor=".".join(version.split(".")[:2]), |
| 557 | + ) |
| 558 | + checksum = dep_info.get("sha256", "") |
| 559 | + |
| 560 | + package: Dict[str, Any] = { |
| 561 | + "SPDXID": f"SPDXRef-PACKAGE-{dep_name}", |
| 562 | + "name": dep_name, |
| 563 | + "versionInfo": version, |
| 564 | + "downloadLocation": url, |
| 565 | + "primaryPackagePurpose": "SOURCE", |
| 566 | + "licenseConcluded": "NOASSERTION", |
| 567 | + } |
| 568 | + |
| 569 | + if checksum: |
| 570 | + package["checksums"] = [ |
| 571 | + { |
| 572 | + "algorithm": "SHA256", |
| 573 | + "checksumValue": checksum, |
| 574 | + } |
| 575 | + ] |
| 576 | + |
| 577 | + packages.append(package) |
| 578 | + |
| 579 | + # Add Python runtime packages installed via pip |
| 580 | + # These are determined at finalize time after pip install |
| 581 | + python_lib = pathlib.Path(dirs.prefix) / "lib" |
| 582 | + for entry in python_lib.glob("python*/site-packages/*.dist-info"): |
| 583 | + # Parse package name and version from dist-info directory |
| 584 | + # Format: package-version.dist-info |
| 585 | + dist_name = entry.name.replace(".dist-info", "") |
| 586 | + if "-" in dist_name: |
| 587 | + parts = dist_name.rsplit("-", 1) |
| 588 | + if len(parts) == 2: |
| 589 | + pkg_name, pkg_version = parts |
| 590 | + package2: Dict[str, Any] = { |
| 591 | + "SPDXID": f"SPDXRef-PACKAGE-python-{pkg_name}", |
| 592 | + "name": pkg_name, |
| 593 | + "versionInfo": pkg_version, |
| 594 | + "downloadLocation": "NOASSERTION", |
| 595 | + "primaryPackagePurpose": "LIBRARY", |
| 596 | + "licenseConcluded": "NOASSERTION", |
| 597 | + "comment": "Python package installed via pip", |
| 598 | + } |
| 599 | + packages.append(package2) |
| 600 | + |
| 601 | + # Create the SBOM document |
| 602 | + sbom = { |
| 603 | + "SPDXID": "SPDXRef-DOCUMENT", |
| 604 | + "spdxVersion": "SPDX-2.3", |
| 605 | + "name": f"relenv-{env.get('RELENV_PY_VERSION', 'unknown')}-{env.get('RELENV_HOST', 'unknown')}", |
| 606 | + "dataLicense": "CC0-1.0", |
| 607 | + "creationInfo": { |
| 608 | + "created": f"{time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}", |
| 609 | + "creators": [ |
| 610 | + f"Tool: relenv-{relenv.__version__}", |
| 611 | + ], |
| 612 | + "comment": "This SBOM documents the actual dependencies built and installed by relenv. " |
| 613 | + "It may differ from Python's externals.spdx.json which documents Python's intended dependencies.", |
| 614 | + }, |
| 615 | + "packages": packages, |
| 616 | + } |
| 617 | + |
| 618 | + # Write the SBOM file |
| 619 | + sbom_path = pathlib.Path(dirs.prefix) / "relenv-sbom.spdx.json" |
| 620 | + with io.open(sbom_path, "w") as fp: |
| 621 | + json.dump(sbom, fp, indent=2) |
| 622 | + log.info("Generated relenv-sbom.spdx.json with %d packages", len(packages)) |
442 | 623 |
|
443 | 624 |
|
444 | 625 | def finalize( |
@@ -587,6 +768,7 @@ def runpip(pkg: Union[str, os.PathLike[str]], upgrade: bool = False) -> None: |
587 | 768 | runpip("relenv", upgrade=True) |
588 | 769 |
|
589 | 770 | copy_sbom_files(dirs) |
| 771 | + generate_relenv_sbom(env, dirs) |
590 | 772 |
|
591 | 773 | globs = [ |
592 | 774 | "/bin/python*", |
|
0 commit comments