changeset 52664:f5091286b10c

packaging: modernize (compat PEP 517) with less distutils and setup.py calls - setup.py: less distutils imports and setuptools required distutils is deprecated and one should import commands from setuptools to support modern workflows depending on PEP 517 and 518. Moreover, for Python >=3.12, distutils comes from setuptools. It corresponds to old and unmaintain code that do not support PEP 517. The PEP 517 frontends (pip, build, pipx, PDM, UV, etc.) are responsible for creating a venv just for the build. The build dependencies (currently only setuptools) are specified in the pyproject.toml file. Therefore, there is no reason to support building without setuptools. Calling directly setup.py is deprecated and we have to use a PEP 517 frontend. For this commit we use pip with venv. - run-tests.py: install with pip instead of direct call of setup.py Mercurial is then built in an isolated environment. - Makefile: use venv+pip instead of setup.py
author paugier <pierre.augier@univ-grenoble-alpes.fr>
date Wed, 08 Jan 2025 05:07:00 +0100
parents 25bb409da058
children 022c91426022
files .hgignore Makefile pyproject.toml setup.py tests/run-tests.py
diffstat 5 files changed, 78 insertions(+), 73 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Wed Dec 04 23:09:32 2024 +0100
+++ b/.hgignore	Wed Jan 08 05:07:00 2025 +0100
@@ -19,6 +19,7 @@
 *.zip
 \#*\#
 .\#*
+.venv*
 result/
 tests/artifacts/cache/big-file-churn.hg
 tests/.coverage*
--- a/Makefile	Wed Dec 04 23:09:32 2024 +0100
+++ b/Makefile	Wed Jan 08 05:07:00 2025 +0100
@@ -36,6 +36,9 @@
 COMPILERFLAG_tmp_${COMPILER} ?= -c $(COMPILER)
 COMPILERFLAG=${COMPILERFLAG_tmp_${COMPILER}}
 
+VENV_NAME=$(shell $(PYTHON) -c "import sys; v = sys.version_info; print(f'.venv_{sys.implementation.name}{v.major}.{v.minor}')")
+PYBINDIRNAME=$(shell $(PYTHON) -c "import os; print('Scripts' if os.name == 'nt' else 'bin')")
+
 help:
 	@echo 'Commonly used make targets:'
 	@echo '  all          - build program and documentation'
@@ -58,12 +61,10 @@
 all: build doc
 
 local:
-	MERCURIAL_SETUP_MAKE_LOCAL=1 $(PYTHON) setup.py $(PURE) \
-	  build_py -c -d . \
-	  build_ext $(COMPILERFLAG) -i \
-	  build_hgexe $(COMPILERFLAG) -i \
-	  build_mo
-	env HGRCPATH= $(PYTHON) hg version
+	$(PYTHON) -m venv $(VENV_NAME) --clear --upgrade-deps
+	MERCURIAL_SETUP_MAKE_LOCAL=1 $(VENV_NAME)/$(PYBINDIRNAME)/python -m \
+	  pip install -e . -v --config-settings --global-option=$(PURE)
+	env HGRCPATH= $(VENV_NAME)/$(PYBINDIRNAME)/hg version
 
 build:
 	$(PYTHON) setup.py $(PURE) build $(COMPILERFLAG)
@@ -75,7 +76,7 @@
 	(cd rust/rhg; cargo build --release)
 
 wheel:
-	FORCE_SETUPTOOLS=1 $(PYTHON) setup.py $(PURE) bdist_wheel $(COMPILERFLAG)
+	$(PYTHON) setup.py $(PURE) bdist_wheel $(COMPILERFLAG)
 
 doc:
 	$(MAKE) -C doc
@@ -85,6 +86,7 @@
 	-$(PYTHON) setup.py clean --all # ignore errors from this command
 	find contrib doc hgext hgext3rd i18n mercurial tests hgdemandimport \
 		\( -name '*.py[cdo]' -o -name '*.so' \) -exec rm -f '{}' ';'
+	rm -rf .venv_*
 	rm -f MANIFEST MANIFEST.in hgext/__index__.py tests/*.err
 	rm -f mercurial/__modulepolicy__.py
 	if test -d .hg; then rm -f mercurial/__version__.py; fi
--- a/pyproject.toml	Wed Dec 04 23:09:32 2024 +0100
+++ b/pyproject.toml	Wed Jan 08 05:07:00 2025 +0100
@@ -38,6 +38,11 @@
 download_url = "https://mercurial-scm.org/release/"
 
 
+[tool.setuptools]
+# no automatic include
+include-package-data = false
+
+
 [tool.black]
 line-length = 80
 include = '\.py$'
--- a/setup.py	Wed Dec 04 23:09:32 2024 +0100
+++ b/setup.py	Wed Jan 08 05:07:00 2025 +0100
@@ -58,37 +58,29 @@
         "Couldn't import standard bz2 (incomplete Python install)."
     )
 
-ispypy = "PyPy" in sys.version
+from setuptools import setup, Command, Extension, Distribution
+from setuptools.command.build import build
+from setuptools.command.build_ext import build_ext
+from setuptools.command.build_py import build_py
+from setuptools.command.install import install
+from setuptools.command.install_lib import install_lib
+from setuptools.command.install_scripts import install_scripts
 
-# We have issues with setuptools on some platforms and builders. Until
-# those are resolved, setuptools is opt-in except for platforms where
-# we don't have issues.
-issetuptools = os.name == 'nt' or 'FORCE_SETUPTOOLS' in os.environ
-if issetuptools:
-    from setuptools import setup
-else:
-    try:
-        from distutils.core import setup
-    except ModuleNotFoundError:
-        from setuptools import setup
-from distutils.ccompiler import new_compiler
-from distutils.core import Command, Extension
-from distutils.dist import Distribution
-from distutils.command.build import build
-from distutils.command.build_ext import build_ext
-from distutils.command.build_py import build_py
+from setuptools.errors import (
+    CCompilerError,
+    BaseError as DistutilsError,
+    ExecError as DistutilsExecError,
+)
+
+# no setuptools.command.build_scripts
 from distutils.command.build_scripts import build_scripts
-from distutils.command.install import install
-from distutils.command.install_lib import install_lib
-from distutils.command.install_scripts import install_scripts
+
 from distutils.spawn import spawn
 from distutils import file_util
-from distutils.errors import (
-    CCompilerError,
-    DistutilsError,
-    DistutilsExecError,
-)
 from distutils.sysconfig import get_python_inc
+from distutils.ccompiler import new_compiler
+
+ispypy = "PyPy" in sys.version
 
 
 def sysstr(s):
@@ -913,9 +905,7 @@
             output_dir=self.build_temp,
             macros=[('_UNICODE', None), ('UNICODE', None)],
         )
-        self.compiler.link_executable(
-            objects, self.hgtarget, libraries=[], output_dir=self.build_temp
-        )
+        self.compiler.link_executable(objects, self.hgtarget, libraries=[])
 
         self.addlongpathsmanifest()
 
@@ -948,8 +938,9 @@
 
     @property
     def hgexepath(self):
-        dir = os.path.dirname(self.get_ext_fullpath('dummy'))
-        return os.path.join(self.build_temp, dir, 'hg.exe')
+        return os.path.join(
+            os.path.dirname(self.get_ext_fullpath('dummy')), 'hg.exe'
+        )
 
 
 class hgbuilddoc(Command):
--- a/tests/run-tests.py	Wed Dec 04 23:09:32 2024 +0100
+++ b/tests/run-tests.py	Wed Jan 08 05:07:00 2025 +0100
@@ -87,6 +87,7 @@
 MACOS = sys.platform == 'darwin'
 WINDOWS = os.name == r'nt'
 shellquote = shlex.quote
+BINDIRNAME = b"Scripts" if WINDOWS else b"bin"
 
 # The number of HGPORTx ports allocated to each test.
 HGPORT_COUNT = 4
@@ -752,7 +753,17 @@
             parser.error('--pyoxidized does not work with --local (yet)')
         testdir = os.path.dirname(_sys2bytes(canonpath(sys.argv[0])))
         reporootdir = os.path.dirname(testdir)
-        pathandattrs = [(b'hg', 'with_hg')]
+        venv_local = b'.venv_%s%d.%d' % (
+            sys.implementation.name.encode(),
+            sys.version_info.major,
+            sys.version_info.minor,
+        )
+        path_local_hg = os.path.join(reporootdir, venv_local, BINDIRNAME, b"hg")
+        if not os.path.exists(path_local_hg):
+            # no local environment but we can still use ./hg to please test-run-tests.t
+            path_local_hg = os.path.join(reporootdir, b"hg")
+
+        pathandattrs = [(path_local_hg, 'with_hg')]
         if options.chg:
             pathandattrs.append((b'contrib/chg/chg', 'with_chg'))
         if options.rhg:
@@ -2805,6 +2816,18 @@
         pass
 
 
+def get_site_packages_dir(python_exe):
+    return subprocess.run(
+        [
+            python_exe,
+            "-c",
+            "import sys; print([p for p in sys.path if p.startswith(sys.prefix) and p.endswith('site-packages')][0])",
+        ],
+        check=True,
+        capture_output=True,
+    ).stdout.strip()
+
+
 class TextTestRunner(unittest.TextTestRunner):
     """Custom unittest test runner that uses appropriate settings."""
 
@@ -3260,10 +3283,18 @@
             # the Mercurial modules are relative to its path and tell the tests
             # to load Python modules from its directory.
             with open(whg, 'rb') as fh:
-                initial = fh.read(1024)
-
-            if re.match(b'#!.*python', initial):
-                self._pythondir = self._bindir
+                first_line = fh.readline()
+
+            if re.match(b'#!.*python', first_line):
+                python_exe = first_line.split(b"#!")[1].strip()
+                try:
+                    self._pythondir = get_site_packages_dir(python_exe)
+                except (FileNotFoundError, subprocess.CalledProcessError):
+                    self._pythondir = self._bindir
+            elif self.options.local:
+                assert WINDOWS
+                python_exe = os.path.join(self._bindir, b"python.exe")
+                self._pythondir = get_site_packages_dir(python_exe)
             # If it looks like our in-repo Rust binary, use the source root.
             # This is a bit hacky. But rhg is still not supported outside the
             # source directory. So until it is, do the simple thing.
@@ -3291,16 +3322,7 @@
             bindir = b"Scripts" if WINDOWS else b"bin"
             self._bindir = os.path.join(self._installdir, bindir)
             self._python = _bytes2sys(os.path.join(self._bindir, b"python"))
-
-            self._pythondir = subprocess.run(
-                [
-                    self._python,
-                    "-c",
-                    "import sys; print([p for p in sys.path if p.startswith(sys.prefix) and p.endswith('site-packages')][0])",
-                ],
-                check=True,
-                capture_output=True,
-            ).stdout.strip()
+            self._pythondir = get_site_packages_dir(self._python)
 
         # Force the use of hg.exe instead of relying on MSYS to recognize hg is
         # a python script and feed it to python.exe.  Legacy stdio is force
@@ -3854,27 +3876,11 @@
         hgroot = os.path.dirname(os.path.dirname(script))
         self._hgroot = hgroot
         os.chdir(hgroot)
-        cmd = [
-            self._pythonb,
-            b"setup.py",
-        ]
+        cmd = [self._pythonb, b"-m", b"pip", b"install", b"."]
         if setup_opts:
-            cmd.append(setup_opts)
-        cmd.extend(
-            [
-                b"clean",
-                b"--all",
-                b"build",
-            ]
-        )
-        cmd.extend(
-            [
-                b"--build-base=%s" % os.path.join(self._hgtmp, b"build"),
-                b"install",
-                b"--force",
-            ]
-        )
-
+            cmd.extend(
+                [b"--config-settings", b"--global-option=%s" % setup_opts]
+            )
         return cmd
 
     def _installhg(self):