verify_sdist_contents.py 3.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. #!/usr/bin/env python3
  2. """Verify a built sdist ships only the intended configuration templates.
  3. The sdist is a published artifact in its own right. It must contain the two tracked
  4. configuration templates and must NOT contain a live configuration.py (which holds
  5. SECRET_KEY and database credentials), any other local configuration*.py variant, or
  6. any ldap_config*.py (which holds LDAP bind credentials). The wheel guard alone is not
  7. enough: a wheel rebuilt from the sdist re-applies the wheel excludes, so it can come
  8. out clean even when the sdist itself leaks a file.
  9. The sdist must also declare the Core Metadata version pinned for the sdist target in
  10. pyproject.toml, so a backend default change cannot silently alter the artifact format.
  11. """
  12. import sys
  13. import tarfile
  14. import tomllib
  15. from email.parser import Parser
  16. from pathlib import Path, PurePosixPath
  17. # Allowed members, relative to the sdist's netbox-<version>/ root directory. The sdist
  18. # keeps the full repository layout (no `sources` strip), unlike the wheel.
  19. ALLOWED = {
  20. 'netbox/netbox/configuration_example.py',
  21. 'netbox/netbox/configuration_testing.py',
  22. }
  23. def configuration_members(sdist_path):
  24. """Return the set of configuration*.py members anywhere inside the sdist."""
  25. with tarfile.open(sdist_path) as archive:
  26. names = archive.getnames()
  27. members = set()
  28. for name in names:
  29. path = PurePosixPath(name)
  30. if path.suffix == '.py' and (path.name.startswith('configuration') or path.name.startswith('ldap_config')):
  31. # Strip the leading netbox-<version>/ directory for a stable comparison.
  32. members.add(str(PurePosixPath(*path.parts[1:])))
  33. return members
  34. def expected_metadata_version():
  35. """Return the core-metadata-version pinned for the sdist target, or None."""
  36. pyproject = tomllib.loads((Path(__file__).resolve().parent.parent / 'pyproject.toml').read_text())
  37. return pyproject['tool']['hatch']['build']['targets'].get('sdist', {}).get('core-metadata-version')
  38. def read_pkg_info(sdist_path):
  39. """Return the sdist's parsed top-level PKG-INFO, or None when the file is absent."""
  40. with tarfile.open(sdist_path) as archive:
  41. for member in archive.getmembers():
  42. if PurePosixPath(member.name).parts[1:] == ('PKG-INFO',):
  43. return Parser().parsestr(archive.extractfile(member).read().decode())
  44. return None
  45. def main(argv):
  46. if len(argv) != 2:
  47. print('usage: verify_sdist_contents.py <sdist>')
  48. return 2
  49. errors = []
  50. found = configuration_members(argv[1])
  51. if missing := sorted(ALLOWED - found):
  52. errors.append(f'missing templates: {missing}')
  53. if unexpected := sorted(found - ALLOWED):
  54. errors.append(f'unexpected (possible secret leak): {unexpected}')
  55. expected = expected_metadata_version()
  56. if expected is None:
  57. errors.append('pyproject.toml does not pin core-metadata-version for the sdist target')
  58. elif (pkg_info := read_pkg_info(argv[1])) is None:
  59. errors.append('sdist is missing its top-level PKG-INFO')
  60. elif (actual := pkg_info['Metadata-Version']) is None:
  61. errors.append('sdist PKG-INFO does not declare Metadata-Version')
  62. elif actual != expected:
  63. errors.append(f'metadata version mismatch: sdist PKG-INFO has {actual}, pyproject.toml pins {expected}')
  64. if errors:
  65. print('Sdist contents are not as expected:')
  66. for error in errors:
  67. print(f' - {error}')
  68. return 1
  69. print(f'OK: sdist ships only the {len(ALLOWED)} configuration templates and declares Metadata-Version {expected}')
  70. return 0
  71. if __name__ == '__main__':
  72. sys.exit(main(sys.argv))