summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSviatoslav Sydorenko <webknjaz@redhat.com>2023-03-29 02:14:07 +0200
committerGitHub <noreply@github.com>2023-03-28 17:14:07 -0700
commit77d221c0ac408af0609d6778c62d31a4be0320c8 (patch)
tree8bccf8c5333f3eb9e0ee46db692fcab76b20e37c
parente9603d233c739f2cc86ac5453251f4fcf9a2ad34 (diff)
downloadansible-77d221c0ac408af0609d6778c62d31a4be0320c8.tar.gz
[backport-2.13] ๐Ÿ“ฆ Integrate manpage builds into PEP 517 build backend (#80130)
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 <matt@mystile.com> 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)
-rw-r--r--packaging/pep517_backend/__init__.py1
-rw-r--r--packaging/pep517_backend/_backend.py173
-rw-r--r--packaging/pep517_backend/hooks.py9
-rw-r--r--pyproject.toml3
-rw-r--r--test/integration/targets/canonical-pep517-self-packaging/aliases2
-rw-r--r--test/integration/targets/canonical-pep517-self-packaging/minimum-build-constraints.txt20
-rw-r--r--test/integration/targets/canonical-pep517-self-packaging/modernish-build-constraints.txt11
-rwxr-xr-xtest/integration/targets/canonical-pep517-self-packaging/runme.sh31
-rw-r--r--test/integration/targets/canonical-pep517-self-packaging/runme_test.py424
9 files changed, 673 insertions, 1 deletions
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