diff --git a/Makefile b/Makefile
index e2c239dae5a27cae33f542fa2df9d52baf116ccd_TWFrZWZpbGU=..53711003945d18e4808a6622c5a97d8623ba719d_TWFrZWZpbGU= 100644
--- a/Makefile
+++ b/Makefile
@@ -71,6 +71,10 @@
 	  pip install -e . -v $(PIP_OPTIONS_PURE)
 	env HGRCPATH= $(VENV_NAME)/$(PYBINDIRNAME)/hg version
 
+.PHONY: hgext_index
+hgext_index: local
+	$(VENV_NAME)/$(PYBINDIRNAME)/python ./contrib/packaging/build-hgext-index.py
+
 .PHONY: build-chg
 build-chg:
 	make -C contrib/chg
diff --git a/contrib/packaging/build-hgext-index.py b/contrib/packaging/build-hgext-index.py
new file mode 100755
index 0000000000000000000000000000000000000000..53711003945d18e4808a6622c5a97d8623ba719d_Y29udHJpYi9wYWNrYWdpbmcvYnVpbGQtaGdleHQtaW5kZXgucHk=
--- /dev/null
+++ b/contrib/packaging/build-hgext-index.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python3
+#
+# build-hgext-index.py - Generate the bundled extension index for frozen builds
+#
+# Copyright 2025 Matt Harbison <mharbison72@gmail.com>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+from __future__ import annotations
+
+import os
+import subprocess
+import sys
+
+_filename = 'hgext/__index__.py'
+
+
+def localhgenv(pure_python=True):
+    """Get an environment dictionary to use for invoking or importing
+    mercurial from the local repository."""
+    # Execute hg out of this directory with a custom environment which takes
+    # care to not use any hgrc files and do no localization.
+    env = {
+        'HGRCPATH': '',
+        'LANGUAGE': 'C',
+        'PATH': '',
+    }  # make pypi modules that use os.environ['PATH'] happy
+    if pure_python:
+        env['HGMODULEPOLICY'] = 'py'
+    if 'LD_LIBRARY_PATH' in os.environ:
+        env['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH']
+    if 'SystemRoot' in os.environ:
+        # SystemRoot is required by Windows to load various DLLs.  See:
+        # https://bugs.python.org/issue13524#msg148850
+        env['SystemRoot'] = os.environ['SystemRoot']
+    return env
+
+
+def main():
+    """generate prebuilt index of hgext (for frozen package)"""
+
+    # here no extension enabled, disabled() lists up everything
+    code = (
+        'import pprint; from mercurial import extensions; '
+        'ext = extensions.disabled();'
+        'ext.pop("__index__", None);'
+        'pprint.pprint(ext)'
+    )
+
+    out = subprocess.check_output(
+        args=[sys.executable, '-c', code],
+        env=localhgenv(),
+    )
+
+    with open(_filename, 'wb') as f:
+        f.write(b'# this file is autogenerated by setup.py\n')
+        f.write(b'docs = ')
+        f.write(out)
+
+    return 0
+
+
+if __name__ == "__main__":
+    main()
diff --git a/mercurial/extensions.py b/mercurial/extensions.py
index e2c239dae5a27cae33f542fa2df9d52baf116ccd_bWVyY3VyaWFsL2V4dGVuc2lvbnMucHk=..53711003945d18e4808a6622c5a97d8623ba719d_bWVyY3VyaWFsL2V4dGVuc2lvbnMucHk= 100644
--- a/mercurial/extensions.py
+++ b/mercurial/extensions.py
@@ -855,7 +855,10 @@
 
             # The extensions are filesystem based, so either an error occurred
             # or all are enabled.
-            if hasattr(hgext, '__file__'):
+            if (
+                hasattr(hgext, '__file__')
+                and getattr(sys, "frozen", None) != "console_exe"  # py2exe
+            ):
                 return
 
             if name in _order:  # enabled
diff --git a/setup.py b/setup.py
index e2c239dae5a27cae33f542fa2df9d52baf116ccd_c2V0dXAucHk=..53711003945d18e4808a6622c5a97d8623ba719d_c2V0dXAucHk= 100644
--- a/setup.py
+++ b/setup.py
@@ -522,41 +522,6 @@
         build_py.run(self)
 
 
-class buildhgextindex(Command):
-    description = 'generate prebuilt index of hgext (for frozen package)'
-    user_options = []
-    _indexfilename = 'hgext/__index__.py'
-
-    def initialize_options(self):
-        pass
-
-    def finalize_options(self):
-        pass
-
-    def run(self):
-        if os.path.exists(self._indexfilename):
-            with open(self._indexfilename, 'w') as f:
-                f.write('# empty\n')
-
-        # here no extension enabled, disabled() lists up everything
-        code = (
-            'import pprint; from mercurial import extensions; '
-            'ext = extensions.disabled();'
-            'ext.pop("__index__", None);'
-            'pprint.pprint(ext)'
-        )
-        returncode, out, err = runcmd(
-            [sys.executable, '-c', code], localhgenv()
-        )
-        if err or returncode != 0:
-            raise DistutilsExecError(err)
-
-        with open(self._indexfilename, 'wb') as f:
-            f.write(b'# this file is autogenerated by setup.py\n')
-            f.write(b'docs = ')
-            f.write(out)
-
-
 class buildhgexe(build_ext):
     description = 'compile hg.exe from mercurial/exewrapper.c'
 
@@ -974,7 +939,6 @@
     'build_ext': hgbuildext,
     'build_py': hgbuildpy,
     'build_scripts': hgbuildscripts,
-    'build_hgextindex': buildhgextindex,
     'install': hginstall,
     'install_completion': hginstallcompletion,
     'install_lib': hginstalllib,
@@ -1388,10 +1352,6 @@
             'product_version': version,
         }
     ]
-    # Sub command of 'build' because 'py2exe' does not handle sub_commands.
-    # Need to override hgbuild because it has a private copy of
-    # build.sub_commands.
-    hgbuild.sub_commands.insert(0, ('build_hgextindex', None))
     # put dlls in sub directory so that they won't pollute PATH
     extra['zipfile'] = 'lib/library.zip'
 
@@ -1414,9 +1374,6 @@
     if dllexcludes:
         py2exedllexcludes.extend(dllexcludes.split(' '))
 
-if os.environ.get('PYOXIDIZER'):
-    hgbuild.sub_commands.insert(0, ('build_hgextindex', None))
-
 setup(
     long_description=(
         'Mercurial is a distributed SCM tool written in Python.'