88import json
99import os
1010import re
11+ import subprocess
1112import sys
1213import tarfile
1314
@@ -19,7 +20,7 @@ def spdx_id(value: str) -> str:
1920
2021def calculate_package_verification_codes (sbom ) -> None :
2122 """
22- Calculate SPDX 'PackageVerificationCode ' values for
23+ Calculate SPDX 'packageVerificationCode ' values for
2324 each package with 'filesAnalyzed' set to 'true'.
2425 Mutates the values within the passed structure.
2526
@@ -90,6 +91,14 @@ def calculate_package_verification_codes(sbom) -> None:
9091 }
9192
9293
94+ def get_release_tools_commit_sha () -> str :
95+ """Gets the git commit SHA of the release-tools repository"""
96+ git_prefix = os .path .abspath (os .path .dirname (__file__ ))
97+ stdout = subprocess .check_output (["git" , "rev-parse" , "--prefix" , git_prefix , "HEAD" ]).decode ("ascii" )
98+ assert re .match (r"^[a-f0-9]{40,}$" , stdout )
99+ return stdout
100+
101+
93102def create_sbom_for_source_tarball (tarball_path : str ):
94103 """Stitches together an SBOM for a source tarball"""
95104 tarball_name = os .path .basename (tarball_path )
@@ -114,7 +123,8 @@ def create_sbom_for_source_tarball(tarball_path: str):
114123
115124 # There should be an SBOM included in the tarball.
116125 # If there's not we can't create an SBOM.
117- sbom_bytes = tarball .extractfile (tarball .getmember ("Misc/sbom.spdx.json" )).read ()
126+ sbom_tarball_member = tarball .getmember (f"Python-{ cpython_version } /Misc/sbom.spdx.json" )
127+ sbom_bytes = tarball .extractfile (sbom_tarball_member ).read ()
118128
119129 sbom = json .loads (sbom_bytes )
120130 sbom .update ({
@@ -132,7 +142,7 @@ def create_sbom_for_source_tarball(tarball_path: str):
132142 ),
133143 "creators" : [
134144 "Person: Python Release Managers" ,
135- "Tool: python/release-tools@f58cfa6611dd13f2fb4e4790a8c54f06dddab6bc " ,
145+ f "Tool: ReleaseTools- { get_release_tools_commit_sha () } " ,
136146 ],
137147 # Version of the SPDX License ID list.
138148 # This shouldn't need to be updated often, if ever.
@@ -161,26 +171,26 @@ def create_sbom_for_source_tarball(tarball_path: str):
161171 "downloadLocation" : tarball_download_location ,
162172 "checksums" : [{"algorithm" : "SHA256" , "checksumValue" : tarball_checksum_sha256 }],
163173 }
174+
175+ # The top-level CPython package depends on every vendored sub-package.
176+ for sbom_package in sbom ["packages" ]:
177+ sbom ["relationships" ].append ({
178+ "spdxElementId" : sbom_cpython_package ["SPDXID" ],
179+ "relatedSpdxElement" : sbom_package ["SPDXID" ],
180+ "relationshipType" : "DEPENDS_ON" ,
181+ })
182+
164183 sbom ["packages" ].append (sbom_cpython_package )
165184
166185 # Extract all currently known files from the SBOM with their checksums.
167186 known_sbom_files = {}
168187 for sbom_file in sbom ["files" ]:
169188 sbom_filename = sbom_file ["fileName" ]
170189
171- # We use the name we're expecting in the tarball here
172- # which is to prefix the name with 'Python-{version}/...'.
173- expected_tar_filename = f"Python-{ cpython_version } /{ sbom_filename } "
174-
175- # We also want to update our SBOM to use the same filenames
176- # as the ones in the tarball. We maintain the SPDXIDs though
177- # to not need to rewrite SBOM relationships.
178- sbom_file ["fileName" ] = expected_tar_filename
179-
180190 # Look for the expected SHA256 checksum.
181191 for sbom_file_checksum in sbom_file ["checksums" ]:
182192 if sbom_file_checksum ["algorithm" ] == "SHA256" :
183- known_sbom_files [expected_tar_filename ] = (
193+ known_sbom_files [sbom_filename ] = (
184194 sbom_file_checksum ["checksumValue" ]
185195 )
186196 break
@@ -206,21 +216,23 @@ def create_sbom_for_source_tarball(tarball_path: str):
206216 actual_file_checksum_sha1 = hashlib .sha1 (file_bytes ).hexdigest ()
207217 actual_file_checksum_sha256 = hashlib .sha256 (file_bytes ).hexdigest ()
208218
219+ # Remove the 'Python-{version}/...' prefix for the SPDXID and fileName.
220+ member_name_no_prefix = member .name .split ('/' , 1 )[1 ]
221+
209222 # We've already seen this file, so we check it hasn't been modified and continue on.
210- if member . name in known_sbom_files :
223+ if member_name_no_prefix in known_sbom_files :
211224 # If there's a hash mismatch we raise an error, something isn't right!
212- expected_file_checksum_sha256 = known_sbom_files .pop (member . name )
225+ expected_file_checksum_sha256 = known_sbom_files .pop (member_name_no_prefix )
213226 if expected_file_checksum_sha256 != actual_file_checksum_sha256 :
214- raise ValueError (f"Mismatched checksum for file '{ member . name } '" )
227+ raise ValueError (f"Mismatched checksum for file '{ member_name_no_prefix } '" )
215228
216229 # If this is a new file, then it's a part of the 'CPython' SBOM package.
217230 else :
218- # Remove the 'Python-{version}/...' prefix for the SPDXID.
219- sbom_file_spdx_id = spdx_id (f"SPDXRef-FILE-{ member .name .split ('/' , 1 )[1 ]} " )
231+ sbom_file_spdx_id = spdx_id (f"SPDXRef-FILE-{ member_name_no_prefix } " )
220232 sbom ["files" ].append (
221233 {
222234 "SPDXID" : sbom_file_spdx_id ,
223- "fileName" : member . name ,
235+ "fileName" : member_name_no_prefix ,
224236 "checksums" : [
225237 {
226238 "algorithm" : "SHA1" ,
0 commit comments