hatchling 1.31.0 ships only .gitignore in the sdist; older releases also bundled .hgignore. check_sdist required an exact member set including .hgignore, so the Validate workflow's package job failed on main once CI resolved the newer hatchling. Require the tracked source set plus the fixed metadata files, and accept .gitignore/.hgignore as optional members either way.
129 lines
4.2 KiB
Python
Executable File
129 lines
4.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Verify that SmartThings-Local wheel and sdist contents are intentional."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import subprocess
|
|
import tarfile
|
|
import zipfile
|
|
from pathlib import Path, PurePosixPath
|
|
|
|
|
|
class DistributionError(RuntimeError):
|
|
"""An artifact contains a missing, unexpected, or unsafe member."""
|
|
|
|
|
|
def _tracked_files() -> set[str]:
|
|
proc = subprocess.run(
|
|
["git", "ls-files", "-z", "--", "smartthings_local", "tests"],
|
|
capture_output=True,
|
|
check=True,
|
|
)
|
|
return {value.decode("utf-8") for value in proc.stdout.split(b"\0") if value}
|
|
|
|
|
|
def _safe_member(name: str) -> bool:
|
|
path = PurePosixPath(name)
|
|
return bool(name) and not path.is_absolute() and ".." not in path.parts
|
|
|
|
|
|
def _expected_package_files() -> set[str]:
|
|
tracked = {
|
|
path for path in _tracked_files() if path.startswith("smartthings_local/")
|
|
}
|
|
tracked.add("smartthings_local/_version.py")
|
|
return tracked
|
|
|
|
|
|
def check_wheel(path: Path) -> None:
|
|
with zipfile.ZipFile(path) as archive:
|
|
names = set(archive.namelist())
|
|
if not names or any(not _safe_member(name) for name in names):
|
|
raise DistributionError("wheel has an unsafe member")
|
|
|
|
package_files = {name for name in names if name.startswith("smartthings_local/")}
|
|
if package_files != _expected_package_files():
|
|
raise DistributionError(
|
|
"wheel package contents differ from the tracked package"
|
|
)
|
|
|
|
metadata = names - package_files
|
|
roots = {name.split("/", 1)[0] for name in metadata}
|
|
if len(roots) != 1:
|
|
raise DistributionError("wheel must contain one dist-info directory")
|
|
dist_info = roots.pop()
|
|
if not dist_info.endswith(".dist-info"):
|
|
raise DistributionError("wheel metadata directory is invalid")
|
|
expected_metadata = {
|
|
f"{dist_info}/METADATA",
|
|
f"{dist_info}/WHEEL",
|
|
f"{dist_info}/licenses/LICENSE",
|
|
f"{dist_info}/RECORD",
|
|
}
|
|
if metadata != expected_metadata:
|
|
raise DistributionError("wheel metadata contents are unexpected")
|
|
|
|
|
|
def check_sdist(path: Path) -> None:
|
|
with tarfile.open(path, mode="r:gz") as archive:
|
|
members = archive.getmembers()
|
|
if not members or any(
|
|
not member.isfile() or member.issym() or member.islnk() for member in members
|
|
):
|
|
raise DistributionError("sdist must contain regular files only")
|
|
names = {member.name for member in members}
|
|
if any(not _safe_member(name) for name in names):
|
|
raise DistributionError("sdist has an unsafe member")
|
|
|
|
roots = {name.split("/", 1)[0] for name in names}
|
|
if len(roots) != 1:
|
|
raise DistributionError("sdist must contain one top-level directory")
|
|
root = roots.pop()
|
|
relative = {name[len(root) + 1 :] for name in names if name.startswith(f"{root}/")}
|
|
required = _tracked_files() | {
|
|
"LICENSE",
|
|
"PKG-INFO",
|
|
"README.md",
|
|
"pyproject.toml",
|
|
"smartthings_local/_version.py",
|
|
}
|
|
# hatchling bundles the VCS ignore files it finds, but which ones ship
|
|
# depends on the hatchling version (newer releases drop .hgignore), so
|
|
# treat them as optional rather than exact members.
|
|
optional = {".gitignore", ".hgignore"}
|
|
if not required <= relative <= required | optional:
|
|
raise DistributionError("sdist contents differ from the intended source set")
|
|
|
|
|
|
def check_directory(directory: Path) -> None:
|
|
wheels = sorted(directory.glob("*.whl"))
|
|
sdists = sorted(directory.glob("*.tar.gz"))
|
|
if len(wheels) != 1 or len(sdists) != 1:
|
|
raise DistributionError("expected exactly one wheel and one sdist")
|
|
check_wheel(wheels[0])
|
|
check_sdist(sdists[0])
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("directory", type=Path)
|
|
args = parser.parse_args()
|
|
try:
|
|
check_directory(args.directory)
|
|
except (
|
|
DistributionError,
|
|
OSError,
|
|
subprocess.SubprocessError,
|
|
tarfile.TarError,
|
|
zipfile.BadZipFile,
|
|
):
|
|
print("distribution check failed")
|
|
return 1
|
|
print("distribution contents verified")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|