You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ansible/test/sanity/code-smell/package-data.py

190 lines
5.8 KiB
Python

"""Verify the contents of the built sdist and wheel."""
from __future__ import annotations
import contextlib
import fnmatch
import os
import pathlib
import shutil
import subprocess
import sys
import tarfile
import tempfile
import typing as t
import zipfile
from ansible.release import __version__
def collect_sdist_files(complete_file_list: list[str]) -> list[str]:
"""Return a list of files which should be present in the sdist."""
ignore_patterns = (
'.azure-pipelines/*',
'.cherry_picker.toml',
'.git*',
'.mailmap',
'changelogs/README.md',
'changelogs/config.yaml',
'changelogs/fragments/*',
'hacking/*',
)
sdist_files = [path for path in complete_file_list if not any(fnmatch.fnmatch(path, ignore) for ignore in ignore_patterns)]
egg_info = (
'PKG-INFO',
'SOURCES.txt',
'dependency_links.txt',
'entry_points.txt',
'not-zip-safe',
'requires.txt',
'top_level.txt',
)
sdist_files.append('PKG-INFO')
sdist_files.extend(f'lib/ansible_core.egg-info/{name}' for name in egg_info)
return sdist_files
def collect_wheel_files(complete_file_list: list[str]) -> list[str]:
"""Return a list of files which should be present in the wheel."""
wheel_files = []
for path in complete_file_list:
if path.startswith('lib/ansible/'):
prefix = 'lib'
elif path.startswith('test/lib/ansible_test/'):
prefix = 'test/lib'
else:
continue
wheel_files.append(os.path.relpath(path, prefix))
dist_info = (
'COPYING',
'METADATA',
'RECORD',
'WHEEL',
'entry_points.txt',
'top_level.txt',
)
wheel_files.append(f'ansible_core-{__version__}.data/scripts/ansible-test')
wheel_files.extend(f'ansible_core-{__version__}.dist-info/{name}' for name in dist_info)
return wheel_files
@contextlib.contextmanager
def clean_repository(complete_file_list: list[str]) -> t.Generator[str, None, None]:
"""Copy the files to a temporary directory and yield the path."""
directories = sorted(set(os.path.dirname(path) for path in complete_file_list))
directories.remove('')
with tempfile.TemporaryDirectory() as temp_dir:
for directory in directories:
os.makedirs(os.path.join(temp_dir, directory))
for path in complete_file_list:
shutil.copy2(path, os.path.join(temp_dir, path), follow_symlinks=False)
yield temp_dir
def build(source_dir: str, tmp_dir: str) -> tuple[pathlib.Path, pathlib.Path]:
"""Create a sdist and wheel."""
create = subprocess.run(
[sys.executable, '-m', 'build', '--no-isolation', '--outdir', tmp_dir],
stdin=subprocess.DEVNULL,
capture_output=True,
text=True,
check=False,
cwd=source_dir,
)
if create.returncode != 0:
raise RuntimeError(f'build failed:\n{create.stderr}\n{create.stdout}')
tmp_dir_files = list(pathlib.Path(tmp_dir).iterdir())
if len(tmp_dir_files) != 2:
raise RuntimeError(f'build resulted in {len(tmp_dir_files)} items instead of 2')
sdist_path = [path for path in tmp_dir_files if path.suffix == '.gz'][0]
wheel_path = [path for path in tmp_dir_files if path.suffix == '.whl'][0]
return sdist_path, wheel_path
def list_sdist(path: pathlib.Path) -> list[str]:
"""Return a list of the files in the sdist."""
item: tarfile.TarInfo
with tarfile.open(path) as sdist:
paths = ['/'.join(pathlib.Path(item.path).parts[1:]) for item in sdist.getmembers() if not item.isdir()]
return paths
def list_wheel(path: pathlib.Path) -> list[str]:
"""Return a list of the files in the wheel."""
with zipfile.ZipFile(path) as wheel:
paths = [item.filename for item in wheel.filelist if not item.is_dir()]
return paths
def check_files(source: str, expected: list[str], actual: list[str]) -> list[str]:
"""Verify the expected files exist and no extra files exist."""
missing = sorted(set(expected) - set(actual))
extra = sorted(set(actual) - set(expected))
errors = (
[f'{path}: missing from {source}' for path in missing] +
[f'{path}: unexpected in {source}' for path in extra]
)
return errors
def main() -> None:
"""Main program entry point."""
complete_file_list = sys.argv[1:] or sys.stdin.read().splitlines()
errors = []
# Limit visible files to those reported by ansible-test.
# This avoids including files which are not committed to git.
with clean_repository(complete_file_list) as clean_repo_dir:
if __version__.endswith('.dev0'):
# Make sure a changelog exists for this version when testing from devel.
# When testing from a stable branch the changelog will already exist.
major_minor_version = '.'.join(__version__.split('.')[:2])
changelog_paths = [
f'changelogs/CHANGELOG-v{major_minor_version}.rst',
f'changelogs/CHANGELOG-v{major_minor_version}.md',
]
for changelog_path in changelog_paths:
pathlib.Path(clean_repo_dir, changelog_path).touch()
complete_file_list.append(changelog_path)
expected_sdist_files = collect_sdist_files(complete_file_list)
expected_wheel_files = collect_wheel_files(complete_file_list)
with tempfile.TemporaryDirectory() as tmp_dir:
sdist_path, wheel_path = build(clean_repo_dir, tmp_dir)
actual_sdist_files = list_sdist(sdist_path)
actual_wheel_files = list_wheel(wheel_path)
errors.extend(check_files('sdist', expected_sdist_files, actual_sdist_files))
errors.extend(check_files('wheel', expected_wheel_files, actual_wheel_files))
for error in errors:
print(error)
if __name__ == '__main__':
main()