diff --git a/contrib/packaging/hgpackaging/util.py b/contrib/packaging/hgpackaging/util.py
index d655382584c573fc1021789ed703b24235580742_Y29udHJpYi9wYWNrYWdpbmcvaGdwYWNrYWdpbmcvdXRpbC5weQ==..e2c239dae5a27cae33f542fa2df9d52baf116ccd_Y29udHJpYi9wYWNrYWdpbmcvaGdwYWNrYWdpbmcvdXRpbC5weQ== 100644
--- a/contrib/packaging/hgpackaging/util.py
+++ b/contrib/packaging/hgpackaging/util.py
@@ -182,7 +182,7 @@
     p = source_dir / 'mercurial' / '__version__.py'
 
     with p.open('r', encoding='utf-8') as fh:
-        m = re.search('version = b"([^"]+)"', fh.read(), re.MULTILINE)
+        m = re.search("version = '([^']+)'", fh.read(), re.MULTILINE)
 
         if not m:
             raise Exception('could not parse %s' % p)
diff --git a/contrib/packaging/requirements.txt b/contrib/packaging/requirements.txt
index d655382584c573fc1021789ed703b24235580742_Y29udHJpYi9wYWNrYWdpbmcvcmVxdWlyZW1lbnRzLnR4dA==..e2c239dae5a27cae33f542fa2df9d52baf116ccd_Y29udHJpYi9wYWNrYWdpbmcvcmVxdWlyZW1lbnRzLnR4dA== 100644
--- a/contrib/packaging/requirements.txt
+++ b/contrib/packaging/requirements.txt
@@ -1,4 +1,4 @@
 #
-# This file is autogenerated by pip-compile with python 3.7
-# To update, run:
+# This file is autogenerated by pip-compile with Python 3.9
+# by the following command:
 #
@@ -4,5 +4,5 @@
 #
-#    pip-compile --generate-hashes --no-reuse-hashes --output-file=contrib/packaging/requirements.txt contrib/packaging/requirements.txt.in
+#    pip-compile --allow-unsafe --generate-hashes --no-reuse-hashes --output-file=contrib/packaging/requirements.txt contrib/packaging/requirements.txt.in
 #
 docutils==0.16 \
     --hash=sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af \
@@ -66,3 +66,61 @@
     --hash=sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be \
     --hash=sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621
     # via jinja2
+packaging==24.2 \
+    --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \
+    --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f
+    # via setuptools-scm
+setuptools-scm==8.2.0 \
+    --hash=sha256:136e2b1d393d709d2bcf26f275b8dec06c48b811154167b0fd6bb002aad17d6d \
+    --hash=sha256:a18396a1bc0219c974d1a74612b11f9dce0d5bd8b1dc55c65f6ac7fd609e8c28
+    # via -r contrib/packaging/requirements.txt.in
+tomli==2.2.1 \
+    --hash=sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6 \
+    --hash=sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd \
+    --hash=sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c \
+    --hash=sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b \
+    --hash=sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8 \
+    --hash=sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6 \
+    --hash=sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77 \
+    --hash=sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff \
+    --hash=sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea \
+    --hash=sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192 \
+    --hash=sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249 \
+    --hash=sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee \
+    --hash=sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4 \
+    --hash=sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98 \
+    --hash=sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8 \
+    --hash=sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4 \
+    --hash=sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281 \
+    --hash=sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744 \
+    --hash=sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69 \
+    --hash=sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13 \
+    --hash=sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140 \
+    --hash=sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e \
+    --hash=sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e \
+    --hash=sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc \
+    --hash=sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff \
+    --hash=sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec \
+    --hash=sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2 \
+    --hash=sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222 \
+    --hash=sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106 \
+    --hash=sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272 \
+    --hash=sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a \
+    --hash=sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7
+    # via setuptools-scm
+typing-extensions==4.12.2 \
+    --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \
+    --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8
+    # via setuptools-scm
+wheel==0.45.1 \
+    --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \
+    --hash=sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248
+    # via -r contrib/packaging/requirements.txt.in
+
+# The following packages are considered to be unsafe in a requirements file:
+setuptools==75.8.0 \
+    --hash=sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6 \
+    --hash=sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3
+    # via
+    #   -r contrib/packaging/requirements.txt.in
+    #   setuptools-scm
diff --git a/contrib/packaging/requirements.txt.in b/contrib/packaging/requirements.txt.in
index d655382584c573fc1021789ed703b24235580742_Y29udHJpYi9wYWNrYWdpbmcvcmVxdWlyZW1lbnRzLnR4dC5pbg==..e2c239dae5a27cae33f542fa2df9d52baf116ccd_Y29udHJpYi9wYWNrYWdpbmcvcmVxdWlyZW1lbnRzLnR4dC5pbg== 100644
--- a/contrib/packaging/requirements.txt.in
+++ b/contrib/packaging/requirements.txt.in
@@ -3,3 +3,8 @@
 
 docutils
 jinja2
+
+# Keep these in sync with pyproject.toml
+setuptools >= 64
+setuptools-scm >= 8.1.0
+wheel
diff --git a/contrib/win32/ReadMe.html b/contrib/win32/ReadMe.html
index d655382584c573fc1021789ed703b24235580742_Y29udHJpYi93aW4zMi9SZWFkTWUuaHRtbA==..e2c239dae5a27cae33f542fa2df9d52baf116ccd_Y29udHJpYi93aW4zMi9SZWFkTWUuaHRtbA== 100644
--- a/contrib/win32/ReadMe.html
+++ b/contrib/win32/ReadMe.html
@@ -124,7 +124,7 @@
 
     <p>
       If you are IRC-savvy, that's usually the fastest way to get
-      help. Go to <tt>#mercurial</tt> on <tt>irc.freenode.net</tt>.
+      help. Go to <tt>#mercurial</tt> on <tt>libra.chat</tt>.
     </p>
 
     <h1>Author and copyright information</h1>
diff --git a/setup.py b/setup.py
index d655382584c573fc1021789ed703b24235580742_c2V0dXAucHk=..e2c239dae5a27cae33f542fa2df9d52baf116ccd_c2V0dXAucHk= 100644
--- a/setup.py
+++ b/setup.py
@@ -967,87 +967,6 @@
             self.copy_file(os.path.join('contrib', src), dest)
 
 
-# virtualenv installs custom distutils/__init__.py and
-# distutils/distutils.cfg files which essentially proxy back to the
-# "real" distutils in the main Python install. The presence of this
-# directory causes py2exe to pick up the "hacked" distutils package
-# from the virtualenv and "import distutils" will fail from the py2exe
-# build because the "real" distutils files can't be located.
-#
-# We work around this by monkeypatching the py2exe code finding Python
-# modules to replace the found virtualenv distutils modules with the
-# original versions via filesystem scanning. This is a bit hacky. But
-# it allows us to use virtualenvs for py2exe packaging, which is more
-# deterministic and reproducible.
-#
-# It's worth noting that the common StackOverflow suggestions for this
-# problem involve copying the original distutils files into the
-# virtualenv or into the staging directory after setup() is invoked.
-# The former is very brittle and can easily break setup(). Our hacking
-# of the found modules routine has a similar result as copying the files
-# manually. But it makes fewer assumptions about how py2exe works and
-# is less brittle.
-
-# This only catches virtualenvs made with virtualenv (as opposed to
-# venv, which is likely what Python 3 uses).
-py2exehacked = py2exeloaded and getattr(sys, 'real_prefix', None) is not None
-
-if py2exehacked:
-    from distutils.command.py2exe import py2exe as buildpy2exe
-    from py2exe.mf import Module as py2exemodule
-
-    class hgbuildpy2exe(buildpy2exe):
-        def find_needed_modules(self, mf, files, modules):
-            res = buildpy2exe.find_needed_modules(self, mf, files, modules)
-
-            # Replace virtualenv's distutils modules with the real ones.
-            modules = {}
-            for k, v in res.modules.items():
-                if k != 'distutils' and not k.startswith('distutils.'):
-                    modules[k] = v
-
-            res.modules = modules
-
-            import opcode
-
-            distutilsreal = os.path.join(
-                os.path.dirname(opcode.__file__), 'distutils'
-            )
-
-            for root, dirs, files in os.walk(distutilsreal):
-                for f in sorted(files):
-                    if not f.endswith('.py'):
-                        continue
-
-                    full = os.path.join(root, f)
-
-                    parents = ['distutils']
-
-                    if root != distutilsreal:
-                        rel = os.path.relpath(root, distutilsreal)
-                        parents.extend(p for p in rel.split(os.sep))
-
-                    modname = '%s.%s' % ('.'.join(parents), f[:-3])
-
-                    if modname.startswith('distutils.tests.'):
-                        continue
-
-                    if modname.endswith('.__init__'):
-                        modname = modname[: -len('.__init__')]
-                        path = os.path.dirname(full)
-                    else:
-                        path = None
-
-                    res.modules[modname] = py2exemodule(
-                        modname, full, path=path
-                    )
-
-            if 'distutils' not in res.modules:
-                raise SystemExit('could not find distutils modules')
-
-            return res
-
-
 cmdclass = {
     'build': hgbuild,
     'build_doc': hgbuilddoc,
@@ -1062,9 +981,6 @@
     'build_hgexe': buildhgexe,
 }
 
-if py2exehacked:
-    cmdclass['py2exe'] = hgbuildpy2exe
-
 packages = [
     'mercurial',
     'mercurial.admin',