From 77d221c0ac408af0609d6778c62d31a4be0320c8 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Wed, 29 Mar 2023 02:14:07 +0200 Subject: =?UTF-8?q?[backport-2.13]=20=20=F0=9F=93=A6=20Integrate=20manpage?= =?UTF-8?q?=20builds=20into=20PEP=20517=20build=20backend=20(#80130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change includes backports of the PRs #79606, #80098, #80253, #80255 and #80262, with a number of backward compatibility changes that make it work and be testable in older CI environments. This patch creates a thin wrapper around the `setuptools`' PEP 517 build backend in-tree. It features an ability to request generating the manpage files in-memory, in the process of building a source distribution. The build is happening in an isolated temporary directory. This toggle is implemented using the `config_settings` mechanism of PEP 517. One must explicitly pass it a CLI option to the build front-end to trigger said behavior. The packagers are expected to use the following call: python -m build --config-setting=--build-manpages This option has no effect on building wheels. ๐Ÿงช The change includes integration tests This test runs building and re-building sdists and wheels with and without the `--build-manpages` config setting under the oldest-supported and new `setuptools` pinned. It is intended to preserve the interoperability of the packaging setup across Python runtimes. An extra smoke test also verifies that non PEP 517 interfaces remain functional. ๐Ÿ“ฆ The sdist build-system is switched to pure setuptools upon the first build from Git checkout This patch modifies the in-tree build backend to build sdists that swap out pointers to it in the `pyproject.toml`'s `[build-system]` section. The effect of this is that the first build from source (for example, from a Git checkout) uses our PEP 517 in-tree build backend. But the produced tarball has `build-backend` set to `setuptools.build_meta` which is the native build backend of `setuptools`. So any following builds from that sdist will skip using the in-tree build backend, calling the setuptools' one. The good news is that if the first build generated the manpages, they will be included and won't go anywhere even though, a different build system is in place. Combined with #80253, this makes sure not to modify the current source checkout on that first build. Co-Authored-By: Matt Clay Co-Authored-By: Matt Davis <6775756+nitzmahone@users.noreply.github.com> (cherry picked from commit 56036013cd09db0f1b20402a69be44345ddafb22) (cherry picked from commit 67bafafbc0d61751d2ec10b0aeccb6b01482a074) (cherry picked from commit 888abf5d6e13880ba82d02c5f8a57ca1f90a992f) (cherry picked from commit 7097df3eed979446830fc579613ffb9b7e7c19bf) (cherry picked from commit eebfd71a6da396b011e664604c9991543205d780) --- packaging/pep517_backend/__init__.py | 1 + packaging/pep517_backend/_backend.py | 173 +++++++++ packaging/pep517_backend/hooks.py | 9 + pyproject.toml | 3 +- .../canonical-pep517-self-packaging/aliases | 2 + .../minimum-build-constraints.txt | 20 + .../modernish-build-constraints.txt | 11 + .../canonical-pep517-self-packaging/runme.sh | 31 ++ .../canonical-pep517-self-packaging/runme_test.py | 424 +++++++++++++++++++++ 9 files changed, 673 insertions(+), 1 deletion(-) create mode 100644 packaging/pep517_backend/__init__.py create mode 100644 packaging/pep517_backend/_backend.py create mode 100644 packaging/pep517_backend/hooks.py create mode 100644 test/integration/targets/canonical-pep517-self-packaging/aliases create mode 100644 test/integration/targets/canonical-pep517-self-packaging/minimum-build-constraints.txt create mode 100644 test/integration/targets/canonical-pep517-self-packaging/modernish-build-constraints.txt create mode 100755 test/integration/targets/canonical-pep517-self-packaging/runme.sh create mode 100644 test/integration/targets/canonical-pep517-self-packaging/runme_test.py diff --git a/packaging/pep517_backend/__init__.py b/packaging/pep517_backend/__init__.py new file mode 100644 index 0000000000..1d3bc14c82 --- /dev/null +++ b/packaging/pep517_backend/__init__.py @@ -0,0 +1 @@ +"""PEP 517 build backend for optionally pre-building docs before setuptools.""" diff --git a/packaging/pep517_backend/_backend.py b/packaging/pep517_backend/_backend.py new file mode 100644 index 0000000000..01281d8f99 --- /dev/null +++ b/packaging/pep517_backend/_backend.py @@ -0,0 +1,173 @@ +"""PEP 517 build backend wrapper for optionally pre-building docs for sdist.""" + +from __future__ import annotations + +import os +import re +import subprocess +import sys +import typing as t +from configparser import ConfigParser +from contextlib import contextmanager, suppress +from importlib.metadata import import_module +from io import StringIO +from pathlib import Path +from shutil import copytree +from tempfile import TemporaryDirectory + +try: + from contextlib import chdir as _chdir_cm +except ImportError: + @contextmanager + def _chdir_cm(path: os.PathLike) -> t.Iterator[None]: + original_wd = Path.cwd() + os.chdir(path) + try: + yield + finally: + os.chdir(original_wd) + +from setuptools.build_meta import ( + build_sdist as _setuptools_build_sdist, + get_requires_for_build_sdist as _setuptools_get_requires_for_build_sdist, +) + +with suppress(ImportError): + # NOTE: Only available for sdist builds that bundle manpages. Declared by + # NOTE: `get_requires_for_build_sdist()` when `--build-manpages` is passed. + from docutils.core import publish_file + from docutils.writers import manpage + + +__all__ = ( # noqa: WPS317, WPS410 + 'build_sdist', 'get_requires_for_build_sdist', +) + + +BUILD_MANPAGES_CONFIG_SETTING = '--build-manpages' +"""Config setting name toggle that is used to request manpage in sdists.""" + + +@contextmanager +def _run_in_temporary_directory() -> t.Iterator[Path]: + with TemporaryDirectory(prefix='.tmp-ansible-pep517-') as tmp_dir: + with _chdir_cm(tmp_dir): + yield Path(tmp_dir) + + +def _make_in_tree_ansible_importable() -> None: + """Add the library directory to module lookup paths.""" + lib_path = str(Path.cwd() / 'lib/') + sys.path.insert(0, lib_path) # NOTE: for the current runtime session + + +def _get_package_distribution_version() -> str: + """Retrieve the current version number from setuptools config.""" + setup_cfg_path = Path.cwd() / 'setup.cfg' + setup_cfg = ConfigParser() + setup_cfg.read_string(setup_cfg_path.read_text()) + cfg_version = setup_cfg.get('metadata', 'version') + importable_version_str = cfg_version[len('attr: '):] + version_mod_str, version_var_str = importable_version_str.rsplit('.', 1) + _make_in_tree_ansible_importable() + return getattr(import_module(version_mod_str), version_var_str) + + +def _generate_rst_in_templates() -> t.Iterable[Path]: + """Create ``*.1.rst.in`` files out of CLI Python modules.""" + generate_man_cmd = ( + sys.executable, + 'hacking/build-ansible.py', + 'generate-man', + '--template-file=docs/templates/man.j2', + '--output-dir=docs/man/man1/', + '--output-format=man', + *Path('lib/ansible/cli/').glob('*.py'), + ) + subprocess.check_call(tuple(map(str, generate_man_cmd))) + return Path('docs/man/man1/').glob('*.1.rst.in') + + +def _convert_rst_in_template_to_manpage( + rst_doc_template: str, + destination_path: os.PathLike, + version_number: str, +) -> None: + """Render pre-made ``*.1.rst.in`` templates into manpages. + + This includes pasting the hardcoded version into the resulting files. + The resulting ``in``-files are wiped in the process. + """ + templated_rst_doc = rst_doc_template.replace('%VERSION%', version_number) + + with StringIO(templated_rst_doc) as in_mem_rst_doc: + publish_file( + source=in_mem_rst_doc, + destination_path=destination_path, + writer=manpage.Writer(), + ) + + +def build_sdist( # noqa: WPS210, WPS430 + sdist_directory: os.PathLike, + config_settings: dict[str, str] | None = None, +) -> str: + build_manpages_requested = BUILD_MANPAGES_CONFIG_SETTING in ( + config_settings or {} + ) + original_src_dir = Path.cwd().resolve() + with _run_in_temporary_directory() as tmp_dir: + tmp_src_dir = Path(tmp_dir) / 'src' + copytree(original_src_dir, tmp_src_dir) + os.chdir(tmp_src_dir) + + if build_manpages_requested: + Path('docs/man/man1/').mkdir(exist_ok=True, parents=True) + version_number = _get_package_distribution_version() + for rst_in in _generate_rst_in_templates(): + _convert_rst_in_template_to_manpage( + rst_doc_template=rst_in.read_text(), + destination_path=rst_in.with_suffix('').with_suffix(''), + version_number=version_number, + ) + rst_in.unlink() + + Path('pyproject.toml').write_text( + re.sub( + r"""(?x) + backend-path\s=\s\[ # value is a list of double-quoted strings + [^]]+ + ].*\n + build-backend\s=\s"[^"]+".*\n # value is double-quoted + """, + 'build-backend = "setuptools.build_meta"\n', + Path('pyproject.toml').read_text(), + ) + ) + + built_sdist_basename = _setuptools_build_sdist( + sdist_directory=sdist_directory, + config_settings=config_settings, + ) + + return built_sdist_basename + + +def get_requires_for_build_sdist( + config_settings: dict[str, str] | None = None, +) -> list[str]: + build_manpages_requested = BUILD_MANPAGES_CONFIG_SETTING in ( + config_settings or {} + ) + build_manpages_requested = True # FIXME: Once pypa/build#559 is addressed. + + manpage_build_deps = [ + 'docutils', # provides `rst2man` + 'jinja2', # used in `hacking/build-ansible.py generate-man` + 'straight.plugin', # used in `hacking/build-ansible.py` for subcommand + 'pyyaml', # needed for importing in-tree `ansible-core` from `lib/` + ] if build_manpages_requested else [] + + return _setuptools_get_requires_for_build_sdist( + config_settings=config_settings, + ) + manpage_build_deps diff --git a/packaging/pep517_backend/hooks.py b/packaging/pep517_backend/hooks.py new file mode 100644 index 0000000000..b834338a6d --- /dev/null +++ b/packaging/pep517_backend/hooks.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- + +"""PEP 517 build backend for optionally pre-building docs before setuptools.""" + +from setuptools.build_meta import * # Re-exporting PEP 517 hooks # pylint: disable=unused-wildcard-import,wildcard-import + +from ._backend import ( # noqa: WPS436 # Re-exporting PEP 517 hooks + build_sdist, get_requires_for_build_sdist, +) diff --git a/pyproject.toml b/pyproject.toml index 38c5a47d62..4a583ee5a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,4 @@ [build-system] requires = ["setuptools >= 39.2.0", "wheel"] -build-backend = "setuptools.build_meta" +backend-path = ["packaging"] # requires 'Pip>=20' or 'pep517>=0.6.0' +build-backend = "pep517_backend.hooks" # wraps `setuptools.build_meta` diff --git a/test/integration/targets/canonical-pep517-self-packaging/aliases b/test/integration/targets/canonical-pep517-self-packaging/aliases new file mode 100644 index 0000000000..8278ec8bcc --- /dev/null +++ b/test/integration/targets/canonical-pep517-self-packaging/aliases @@ -0,0 +1,2 @@ +shippable/posix/group3 +context/controller diff --git a/test/integration/targets/canonical-pep517-self-packaging/minimum-build-constraints.txt b/test/integration/targets/canonical-pep517-self-packaging/minimum-build-constraints.txt new file mode 100644 index 0000000000..22b5e5cde7 --- /dev/null +++ b/test/integration/targets/canonical-pep517-self-packaging/minimum-build-constraints.txt @@ -0,0 +1,20 @@ +# Lowest set in `pyproject.toml`: +setuptools == 39.2.0; python_version < "3.9" + +# Lowest supporting Python 3.9 and 3.10: +setuptools == 57.0.0; python_version == "3.9" or python_version == "3.10" + +# Lowest supporting Python 3.11: +setuptools == 60.0.0; python_version >= "3.11" + + +# An arbitrary old version that was released before Python 3.9.0: +wheel == 0.33.6 + +# Conditional dependencies: +docutils == 0.16 +Jinja2 == 3.0.0 +MarkupSafe == 2.0.0 +PyYAML == 5.3; python_version >= "3.9" +PyYAML == 5.4; python_version < "3.9" +straight.plugin == 1.4.2 diff --git a/test/integration/targets/canonical-pep517-self-packaging/modernish-build-constraints.txt b/test/integration/targets/canonical-pep517-self-packaging/modernish-build-constraints.txt new file mode 100644 index 0000000000..e5f5e02203 --- /dev/null +++ b/test/integration/targets/canonical-pep517-self-packaging/modernish-build-constraints.txt @@ -0,0 +1,11 @@ +setuptools == 67.4.0 + +# Wheel-only build dependency +wheel == 0.38.4 + +# Conditional dependencies: +docutils == 0.19 +Jinja2 == 3.1.2 +MarkupSafe == 2.1.2 +PyYAML == 6.0 +straight.plugin == 1.4.2 # WARNING: v1.5.0 doesn't have a Git tag / src diff --git a/test/integration/targets/canonical-pep517-self-packaging/runme.sh b/test/integration/targets/canonical-pep517-self-packaging/runme.sh new file mode 100755 index 0000000000..028348f8fe --- /dev/null +++ b/test/integration/targets/canonical-pep517-self-packaging/runme.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +if [[ "${ANSIBLE_DEBUG}" == true ]] # `ansible-test` invoked with `--debug` +then + PYTEST_VERY_VERBOSE_FLAG=-vvvvv + SET_DEBUG_MODE=-x +else + ANSIBLE_DEBUG=false + PYTEST_VERY_VERBOSE_FLAG= + SET_DEBUG_MODE=+x +fi + + +set -eEuo pipefail + +source virtualenv.sh + +set "${SET_DEBUG_MODE}" + +export PIP_DISABLE_PIP_VERSION_CHECK=true +export PIP_NO_PYTHON_VERSION_WARNING=true +export PIP_NO_WARN_SCRIPT_LOCATION=true + +python -Im pip install 'pytest ~= 7.2.0' +python -Im pytest ${PYTEST_VERY_VERBOSE_FLAG} \ + --basetemp="${OUTPUT_DIR}/pytest-tmp" \ + --color=yes \ + --showlocals \ + -p no:forked \ + -p no:mock \ + -ra diff --git a/test/integration/targets/canonical-pep517-self-packaging/runme_test.py b/test/integration/targets/canonical-pep517-self-packaging/runme_test.py new file mode 100644 index 0000000000..ba7c9b11f3 --- /dev/null +++ b/test/integration/targets/canonical-pep517-self-packaging/runme_test.py @@ -0,0 +1,424 @@ +"""Smoke tests for the in-tree PEP 517 backend.""" + +from __future__ import annotations + +from difflib import unified_diff +from filecmp import dircmp, DEFAULT_IGNORES as _DIR_CMP_DEFAULT_IGNORES +from os import chdir, environ, PathLike +from pathlib import Path +from shutil import rmtree +from subprocess import check_call, check_output, PIPE +from sys import executable as current_interpreter, version_info +from tarfile import TarFile +import typing as t + +try: + from contextlib import chdir as _chdir_cm +except ImportError: + from contextlib import contextmanager as _contextmanager + + @_contextmanager + def _chdir_cm(path: PathLike) -> t.Iterator[None]: + original_wd = Path.cwd() + chdir(path) + try: + yield + finally: + chdir(original_wd) + +import pytest + + +DIST_NAME = 'ansible_core' +DIST_FILENAME_BASE = 'ansible-core' +OUTPUT_DIR = Path(environ['OUTPUT_DIR']).resolve().absolute() +SRC_ROOT_DIR = OUTPUT_DIR.parents[3] +GENERATED_MANPAGES_SUBDIR = SRC_ROOT_DIR / 'docs' / 'man' / 'man1' +LOWEST_SUPPORTED_BUILD_DEPS_FILE = ( + Path(__file__).parent / 'minimum-build-constraints.txt' +).resolve().absolute() +MODERNISH_BUILD_DEPS_FILE = ( + Path(__file__).parent / 'modernish-build-constraints.txt' +).resolve().absolute() +RELEASE_MODULE = SRC_ROOT_DIR / 'lib' / 'ansible' / 'release.py' +VERSION_LINE_PREFIX = "__version__ = '" +PKG_DIST_VERSION = next( + line[len(VERSION_LINE_PREFIX):-1] + for line in RELEASE_MODULE.read_text().splitlines() + if line.startswith(VERSION_LINE_PREFIX) +) +EXPECTED_SDIST_NAME_BASE = f'{DIST_FILENAME_BASE}-{PKG_DIST_VERSION}' +EXPECTED_SDIST_NAME = f'{EXPECTED_SDIST_NAME_BASE}.tar.gz' +EXPECTED_WHEEL_NAME = f'{DIST_NAME}-{PKG_DIST_VERSION}-py3-none-any.whl' + +IS_PYTHON39_PLUS = version_info[:2] >= (3, 9) +IS_PYTHON310_PLUS = version_info[:2] >= (3, 10) + + +def wipe_generated_manpages() -> None: + """Ensure man1 pages aren't present in the source checkout.""" + # Cleaning up the gitignored manpages... + if not GENERATED_MANPAGES_SUBDIR.exists(): + return + + rmtree(GENERATED_MANPAGES_SUBDIR) + # Removed the generated manpages... + + +def contains_man1_pages(sdist_tarball: Path) -> Path: + """Check if the man1 pages are present in given tarball.""" + with sdist_tarball.open(mode='rb') as tarball_fd: + with TarFile.gzopen(fileobj=tarball_fd, name=None) as tarball: + try: + tarball.getmember( + name=f'{EXPECTED_SDIST_NAME_BASE}/docs/man/man1', + ) + except KeyError: + return False + + return True + + +def unpack_sdist(sdist_tarball: Path, target_directory: Path) -> Path: + """Unarchive given tarball. + + :returns: Path of the package source checkout. + """ + with sdist_tarball.open(mode='rb') as tarball_fd: + with TarFile.gzopen(fileobj=tarball_fd, name=None) as tarball: + tarball.extractall(path=target_directory) + return target_directory / EXPECTED_SDIST_NAME_BASE + + +def assert_dirs_equal(*dir_paths: t.List[Path]) -> None: + dir_comparison = dircmp( + *dir_paths, + ignore=None if IS_PYTHON39_PLUS + else ['PKG-INFO'] + _DIR_CMP_DEFAULT_IGNORES, + ) + assert not dir_comparison.left_only + assert not dir_comparison.right_only + diff_txt = '' + for filename in dir_comparison.diff_files: + file_l = dir_paths[0] / filename + file_r = dir_paths[1] / filename + diff_txt += ''.join( + unified_diff( + file_l.read_text().splitlines(keepends=True), + file_r.read_text().splitlines(keepends=True), + fromfile=str(file_l), + tofile=str(file_r), + ) + ) + assert not dir_comparison.diff_files, diff_txt + assert not dir_comparison.funny_files + + +def normalize_unpacked_rebuilt_sdist(sdist_path: Path) -> None: + top_pkg_info_path = sdist_path / 'PKG-INFO' + nested_pkg_info_path = ( + sdist_path / 'lib' / f'{DIST_NAME}.egg-info' / 'PKG-INFO' + ) + entry_points_path = nested_pkg_info_path.parent / 'entry_points.txt' + + # setuptools v39 write out two trailing empty lines and an unknown platform + # while the recent don't + top_pkg_info_path.write_text( + top_pkg_info_path.read_text().replace( + 'Classifier: Development Status :: 5', + 'Platform: UNKNOWN\nClassifier: Development Status :: 5', + ) + '\n\n' + ) + nested_pkg_info_path.write_text( + nested_pkg_info_path.read_text().replace( + 'Classifier: Development Status :: 5', + 'Platform: UNKNOWN\nClassifier: Development Status :: 5', + ) + '\n\n' + ) + + # setuptools v39 write out one trailing empty line while the recent don't + entry_points_path.write_text(entry_points_path.read_text() + '\n') + + +@pytest.fixture +def venv_python_exe(tmp_path: Path) -> t.Iterator[Path]: + venv_path = tmp_path / 'pytest-managed-venv' + mkvenv_cmd = ( + current_interpreter, '-m', 'venv', str(venv_path), + ) + check_call(mkvenv_cmd, env={}, stderr=PIPE, stdout=PIPE) + yield venv_path / 'bin' / 'python' + rmtree(venv_path) + + +def run_with_venv_python( + python_exe: Path, *cli_args: t.Iterable[str], + env_vars: t.Dict[str, str] = None, +) -> str: + if env_vars is None: + env_vars = {} + full_cmd = str(python_exe), *cli_args + return check_output(full_cmd, env=env_vars, stderr=PIPE) + + +def build_dists( + python_exe: Path, *cli_args: t.Iterable[str], + env_vars: t.Dict[str, str], +) -> str: + return run_with_venv_python( + python_exe, '-m', 'build', + *cli_args, env_vars=env_vars, + ) + + +def pip_install( + python_exe: Path, *cli_args: t.Iterable[str], + env_vars: t.Dict[str, str] = None, +) -> str: + return run_with_venv_python( + python_exe, '-m', 'pip', 'install', + *cli_args, env_vars=env_vars, + ) + + +def test_installing_sdist_build_with_modern_deps_to_old_env( + venv_python_exe: Path, tmp_path: Path, +) -> None: + pip_install(venv_python_exe, 'build ~= 0.10.0') + tmp_dir_sdist_w_modern_tools = tmp_path / 'sdist-w-modern-tools' + build_dists( + venv_python_exe, '--sdist', + '--config-setting=--build-manpages', + f'--outdir={tmp_dir_sdist_w_modern_tools!s}', + str(SRC_ROOT_DIR), + env_vars={ + 'PIP_CONSTRAINT': str(MODERNISH_BUILD_DEPS_FILE), + }, + ) + tmp_path_sdist_w_modern_tools = ( + tmp_dir_sdist_w_modern_tools / EXPECTED_SDIST_NAME + ) + + # Downgrading pip, because v20+ supports in-tree build backends + pip_install(venv_python_exe, 'pip ~= 19.3.1') + + # Smoke test โ€” installing an sdist with pip that does not support + # in-tree build backends. + pip_install( + venv_python_exe, str(tmp_path_sdist_w_modern_tools), '--no-deps', + ) + + # Downgrading pip, because versions that support PEP 517 don't allow + # disabling it with `--no-use-pep517` when `build-backend` is set in + # the `[build-system]` section of `pyproject.toml`, considering this + # an explicit opt-in. + if not IS_PYTHON310_PLUS: + pip_install(venv_python_exe, 'pip == 18.0') + + # Smoke test โ€” installing an sdist with pip that does not support invoking + # PEP 517 interface at all. + # In this scenario, pip will run `setup.py install` since `wheel` is not in + # the environment. + if IS_PYTHON310_PLUS: + tmp_dir_unpacked_sdist_root = tmp_path / 'unpacked-sdist' + tmp_dir_unpacked_sdist_path = tmp_dir_unpacked_sdist_root / EXPECTED_SDIST_NAME_BASE + with TarFile.gzopen(tmp_path_sdist_w_modern_tools) as sdist_fd: + sdist_fd.extractall(path=tmp_dir_unpacked_sdist_root) + + pip_install( + venv_python_exe, 'setuptools', + env_vars={ + 'PIP_CONSTRAINT': str(LOWEST_SUPPORTED_BUILD_DEPS_FILE), + }, + ) + with _chdir_cm(tmp_dir_unpacked_sdist_path): + run_with_venv_python( + venv_python_exe, 'setup.py', 'sdist', + env_vars={'PATH': environ['PATH']}, + ) + else: + pip_install( + venv_python_exe, str(tmp_path_sdist_w_modern_tools), '--no-deps', + env_vars={ + 'PIP_CONSTRAINT': str(LOWEST_SUPPORTED_BUILD_DEPS_FILE), + }, + ) + + # Smoke test โ€” installing an sdist with pip that does not support invoking + # PEP 517 interface at all. + # With `wheel` present, pip will run `setup.py bdist_wheel` and then, + # unpack the result. + pip_install(venv_python_exe, 'wheel') + if IS_PYTHON310_PLUS: + with _chdir_cm(tmp_dir_unpacked_sdist_path): + run_with_venv_python( + venv_python_exe, 'setup.py', 'bdist_wheel', + env_vars={'PATH': environ['PATH']}, + ) + else: + pip_install( + venv_python_exe, str(tmp_path_sdist_w_modern_tools), '--no-deps', + ) + + +def test_dist_rebuilds_with_manpages_premutations( + venv_python_exe: Path, tmp_path: Path, +) -> None: + """Test a series of sdist rebuilds under different conditions. + + This check builds sdists right from the Git checkout with and without + the manpages. It also does this using different versions of the setuptools + PEP 517 build backend being pinned. Finally, it builds a wheel out of one + of the rebuilt sdists. + As intermediate assertions, this test makes simple smoke tests along + the way. + """ + pip_install(venv_python_exe, 'build ~= 0.10.0') + + # Test building an sdist without manpages from the Git checkout + tmp_dir_sdist_without_manpages = tmp_path / 'sdist-without-manpages' + wipe_generated_manpages() + build_dists( + venv_python_exe, '--sdist', + f'--outdir={tmp_dir_sdist_without_manpages!s}', + str(SRC_ROOT_DIR), + env_vars={ + 'PIP_CONSTRAINT': str(MODERNISH_BUILD_DEPS_FILE), + }, + ) + tmp_path_sdist_without_manpages = ( + tmp_dir_sdist_without_manpages / EXPECTED_SDIST_NAME + ) + assert tmp_path_sdist_without_manpages.exists() + assert not contains_man1_pages(tmp_path_sdist_without_manpages) + sdist_without_manpages_path = unpack_sdist( + tmp_path_sdist_without_manpages, + tmp_dir_sdist_without_manpages / 'src', + ) + + # Test building an sdist with manpages from the Git checkout + # and lowest supported build deps + wipe_generated_manpages() + tmp_dir_sdist_with_manpages = tmp_path / 'sdist-with-manpages' + if not IS_PYTHON310_PLUS: + pip_install(venv_python_exe, 'wheel') + with _chdir_cm(tmp_path): + run_with_venv_python( + venv_python_exe, '-m', + 'pip', 'wheel', '--no-deps', 'MarkupSafe == 2.0.0', + ) + run_with_venv_python( + venv_python_exe, '-m', + 'pip', 'wheel', '--no-deps', 'straight.plugin == 1.4.2', + ) + run_with_venv_python( + venv_python_exe, '-m', + 'pip', 'wheel', '--no-deps', 'straight.plugin == 1.5.0', + ) + run_with_venv_python( + venv_python_exe, '-m', + 'pip', 'uninstall', 'wheel', '-y', + ) + build_dists( + venv_python_exe, '--sdist', + '--config-setting=--build-manpages', + f'--outdir={tmp_dir_sdist_with_manpages!s}', + str(SRC_ROOT_DIR), + env_vars={ + 'PIP_CONSTRAINT': str(LOWEST_SUPPORTED_BUILD_DEPS_FILE), + }, + ) + tmp_path_sdist_with_manpages = ( + tmp_dir_sdist_with_manpages / EXPECTED_SDIST_NAME + ) + assert tmp_path_sdist_with_manpages.exists() + assert contains_man1_pages(tmp_path_sdist_with_manpages) + sdist_with_manpages_path = unpack_sdist( + tmp_path_sdist_with_manpages, + tmp_dir_sdist_with_manpages / 'src', + ) + + # Test re-building an sdist with manpages from the + # sdist contents that does not include the manpages + tmp_dir_rebuilt_sdist = tmp_path / 'rebuilt-sdist' + build_dists( + venv_python_exe, '--sdist', + '--config-setting=--build-manpages', + f'--outdir={tmp_dir_rebuilt_sdist!s}', + str(sdist_without_manpages_path), + env_vars={ + 'PIP_CONSTRAINT': str(MODERNISH_BUILD_DEPS_FILE), + }, + ) + tmp_path_rebuilt_sdist = tmp_dir_rebuilt_sdist / EXPECTED_SDIST_NAME + # Checking that the expected sdist got created + # from the previous unpacked sdist... + assert tmp_path_rebuilt_sdist.exists() + # NOTE: The following assertion is disabled due to the fact that, when + # NOTE: building an sdist from the original source checkout, the build + # NOTE: backend replaces itself with pure setuptools in the resulting + # NOTE: sdist, and the following rebuilds from that sdist are no longer + # NOTE: able to process the custom config settings that are implemented in + # NOTE: the in-tree build backend. It is expected that said + # NOTE: `pyproject.toml` mutation change will be reverted once all of the + # NOTE: supported `ansible-core` versions ship wheels, meaning that the + # NOTE: end-users won't be building the distribution from sdist on install. + # NOTE: Another case, when it can be reverted is declaring pip below v20 + # NOTE: unsupported โ€” it is the first version to support in-tree build + # NOTE: backends natively. + # assert contains_man1_pages(tmp_path_rebuilt_sdist) # FIXME: See #80255 + rebuilt_sdist_path = unpack_sdist( + tmp_path_rebuilt_sdist, + tmp_dir_rebuilt_sdist / 'src', + ) + assert rebuilt_sdist_path.exists() + assert rebuilt_sdist_path.is_dir() + normalize_unpacked_rebuilt_sdist(rebuilt_sdist_path) + assert_dirs_equal(rebuilt_sdist_path, sdist_with_manpages_path) + + # Test building a wheel from the rebuilt sdist with manpages contents + # and lowest supported build deps + tmp_dir_rebuilt_wheel = tmp_path / 'rebuilt-wheel' + build_dists( + venv_python_exe, '--wheel', + f'--outdir={tmp_dir_rebuilt_wheel!s}', + str(sdist_with_manpages_path), + env_vars={ + 'PIP_CONSTRAINT': str(LOWEST_SUPPORTED_BUILD_DEPS_FILE), + }, + ) + tmp_path_rebuilt_wheel = tmp_dir_rebuilt_wheel / EXPECTED_WHEEL_NAME + # Checking that the expected wheel got created... + assert tmp_path_rebuilt_wheel.exists() + + +def test_pep660_editable_install_smoke(venv_python_exe: Path) -> None: + """Smoke-test PEP 660 editable install. + + This verifies that the in-tree build backend wrapper + does not break any required interfaces. + """ + pip_install(venv_python_exe, 'pip >= 20') + + pip_install(venv_python_exe, '-e', str(SRC_ROOT_DIR)) + + pip_show_cmd = ( + str(venv_python_exe), '-m', + 'pip', 'show', DIST_FILENAME_BASE, + ) + installed_ansible_meta = check_output( + pip_show_cmd, + env={}, stderr=PIPE, text=True, + ).splitlines() + assert f'Name: {DIST_FILENAME_BASE}' in installed_ansible_meta + assert f'Version: {PKG_DIST_VERSION}' in installed_ansible_meta + + pip_runtime_version_cmd = ( + str(venv_python_exe), '-c', + 'from ansible import __version__; print(__version__)', + ) + runtime_ansible_version = check_output( + pip_runtime_version_cmd, + env={}, stderr=PIPE, text=True, + ).strip() + assert runtime_ansible_version == PKG_DIST_VERSION -- cgit v1.2.1