diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 036ce43489..4b05e7cf93 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -144,9 +144,9 @@ jobs:
         run: |
           unset LD_LIBRARY_PATH  # Harmful; set by setup-python
           conda activate build
-          python pyinst.py --onedir
+          python -m bundle.pyinstaller --onedir
           (cd ./dist/yt-dlp_linux && zip -r ../yt-dlp_linux.zip .)
-          python pyinst.py
+          python -m bundle.pyinstaller
           mv ./dist/yt-dlp_linux ./yt-dlp_linux
           mv ./dist/yt-dlp_linux.zip ./yt-dlp_linux.zip
 
@@ -211,7 +211,7 @@ jobs:
             python3.8 -m pip install -U Pyinstaller secretstorage -r requirements.txt  # Cached version may be out of date
             python3.8 devscripts/update-version.py -c "${{ inputs.channel }}" -r "${{ needs.process.outputs.origin }}" "${{ inputs.version }}"
             python3.8 devscripts/make_lazy_extractors.py
-            python3.8 pyinst.py
+            python3.8 -m bundle.pyinstaller
 
             if ${{ vars.UPDATE_TO_VERIFICATION && 'true' || 'false' }}; then
               arch="${{ (matrix.architecture == 'armv7' && 'armv7l') || matrix.architecture }}"
@@ -250,9 +250,9 @@ jobs:
           python3 devscripts/make_lazy_extractors.py
       - name: Build
         run: |
-          python3 pyinst.py --target-architecture universal2 --onedir
+          python3 -m bundle.pyinstaller --target-architecture universal2 --onedir
           (cd ./dist/yt-dlp_macos && zip -r ../yt-dlp_macos.zip .)
-          python3 pyinst.py --target-architecture universal2
+          python3 -m bundle.pyinstaller --target-architecture universal2
 
       - name: Verify --update-to
         if: vars.UPDATE_TO_VERIFICATION
@@ -302,7 +302,7 @@ jobs:
           python3 devscripts/make_lazy_extractors.py
       - name: Build
         run: |
-          python3 pyinst.py
+          python3 -m bundle.pyinstaller
           mv dist/yt-dlp_macos dist/yt-dlp_macos_legacy
 
       - name: Verify --update-to
@@ -342,10 +342,10 @@ jobs:
           python devscripts/make_lazy_extractors.py
       - name: Build
         run: |
-          python setup.py py2exe
+          python -m bundle.py2exe
           Move-Item ./dist/yt-dlp.exe ./dist/yt-dlp_min.exe
-          python pyinst.py
-          python pyinst.py --onedir
+          python -m bundle.pyinstaller
+          python -m bundle.pyinstaller --onedir
           Compress-Archive -Path ./dist/yt-dlp/* -DestinationPath ./dist/yt-dlp_win.zip
 
       - name: Verify --update-to
@@ -391,7 +391,7 @@ jobs:
           python devscripts/make_lazy_extractors.py
       - name: Build
         run: |
-          python pyinst.py
+          python -m bundle.pyinstaller
 
       - name: Verify --update-to
         if: vars.UPDATE_TO_VERIFICATION
diff --git a/.github/workflows/release-master.yml b/.github/workflows/release-master.yml
index 0664137a94..af14b053ec 100644
--- a/.github/workflows/release-master.yml
+++ b/.github/workflows/release-master.yml
@@ -7,7 +7,7 @@ on:
       - "yt_dlp/**.py"
       - "!yt_dlp/version.py"
       - "setup.py"
-      - "pyinst.py"
+      - "bundle/*.py"
 concurrency:
   group: release-master
 permissions:
diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml
index 2e623a67c6..3f1418936a 100644
--- a/.github/workflows/release-nightly.yml
+++ b/.github/workflows/release-nightly.yml
@@ -18,7 +18,7 @@ jobs:
       - name: Check for new commits
         id: check_for_new_commits
         run: |
-          relevant_files=("yt_dlp/*.py" ':!yt_dlp/version.py' "setup.py" "pyinst.py")
+          relevant_files=("yt_dlp/*.py" ':!yt_dlp/version.py' "setup.py" "bundle/*.py")
           echo "commit=$(git log --format=%H -1 --since="24 hours ago" -- "${relevant_files[@]}")" | tee "$GITHUB_OUTPUT"
 
   release:
diff --git a/README.md b/README.md
index 7dc3bb2f6c..c74777d2f5 100644
--- a/README.md
+++ b/README.md
@@ -321,19 +321,21 @@ If you do not have the necessary dependencies for a task you are attempting, yt-
 ## COMPILE
 
 ### Standalone PyInstaller Builds
-To build the standalone executable, you must have Python and `pyinstaller` (plus any of yt-dlp's [optional dependencies](#dependencies) if needed). Once you have all the necessary dependencies installed, simply run `pyinst.py`. The executable will be built for the same architecture (x86/ARM, 32/64 bit) as the Python used.
+To build the standalone executable, you must have Python and `pyinstaller` (plus any of yt-dlp's [optional dependencies](#dependencies) if needed). The executable will be built for the same architecture (x86/ARM, 32/64 bit) as the Python used. You can run the following commands:
 
-    python3 -m pip install -U pyinstaller -r requirements.txt
-    python3 devscripts/make_lazy_extractors.py
-    python3 pyinst.py
+```
+python3 -m pip install -U pyinstaller -r requirements.txt
+python3 devscripts/make_lazy_extractors.py
+python3 -m bundle.pyinstaller
+```
 
 On some systems, you may need to use `py` or `python` instead of `python3`.
 
-`pyinst.py` accepts any arguments that can be passed to `pyinstaller`, such as `--onefile/-F` or `--onedir/-D`, which is further [documented here](https://pyinstaller.org/en/stable/usage.html#what-to-generate).
+`bundle/pyinstaller.py` accepts any arguments that can be passed to `pyinstaller`, such as `--onefile/-F` or `--onedir/-D`, which is further [documented here](https://pyinstaller.org/en/stable/usage.html#what-to-generate).
 
 **Note**: Pyinstaller versions below 4.4 [do not support](https://github.com/pyinstaller/pyinstaller#requirements-and-tested-platforms) Python installed from the Windows store without using a virtual environment.
 
-**Important**: Running `pyinstaller` directly **without** using `pyinst.py` is **not** officially supported. This may or may not work correctly.
+**Important**: Running `pyinstaller` directly **without** using `bundle/pyinstaller.py` is **not** officially supported. This may or may not work correctly.
 
 ### Platform-independent Binary (UNIX)
 You will need the build tools `python` (3.8+), `zip`, `make` (GNU), `pandoc`\* and `pytest`\*.
@@ -346,11 +348,13 @@ You can also run `make yt-dlp` instead to compile only the binary without updati
 
 While we provide the option to build with [py2exe](https://www.py2exe.org), it is recommended to build [using PyInstaller](#standalone-pyinstaller-builds) instead since the py2exe builds **cannot contain `pycryptodomex`/`certifi` and needs VC++14** on the target computer to run.
 
-If you wish to build it anyway, install Python and py2exe, and then simply run `setup.py py2exe`
+If you wish to build it anyway, install Python (if it is not already installed) and you can run the following commands:
 
-    py -m pip install -U py2exe -r requirements.txt
-    py devscripts/make_lazy_extractors.py
-    py setup.py py2exe
+```
+py -m pip install -U py2exe -r requirements.txt
+py devscripts/make_lazy_extractors.py
+py -m bundle.py2exe
+```
 
 ### Related scripts
 
diff --git a/bundle/__init__.py b/bundle/__init__.py
new file mode 100644
index 0000000000..932b79829c
--- /dev/null
+++ b/bundle/__init__.py
@@ -0,0 +1 @@
+# Empty file
diff --git a/bundle/py2exe.py b/bundle/py2exe.py
new file mode 100755
index 0000000000..a7e4113f1f
--- /dev/null
+++ b/bundle/py2exe.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python3
+
+# Allow execution from anywhere
+import os
+import sys
+
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+import warnings
+
+from py2exe import freeze
+
+from devscripts.utils import read_version
+
+VERSION = read_version()
+
+
+def main():
+    warnings.warn(
+        'py2exe builds do not support pycryptodomex and needs VC++14 to run. '
+        'It is recommended to run "pyinst.py" to build using pyinstaller instead')
+
+    return freeze(
+        console=[{
+            'script': './yt_dlp/__main__.py',
+            'dest_base': 'yt-dlp',
+            'icon_resources': [(1, 'devscripts/logo.ico')],
+        }],
+        version_info={
+            'version': VERSION,
+            'description': 'A youtube-dl fork with additional features and patches',
+            'comments': 'Official repository: <https://github.com/yt-dlp/yt-dlp>',
+            'product_name': 'yt-dlp',
+            'product_version': VERSION,
+        },
+        options={
+            'bundle_files': 0,
+            'compressed': 1,
+            'optimize': 2,
+            'dist_dir': './dist',
+            'excludes': [
+                # py2exe cannot import Crypto
+                'Crypto',
+                'Cryptodome',
+                # py2exe appears to confuse this with our socks library.
+                # We don't use pysocks and urllib3.contrib.socks would fail to import if tried.
+                'urllib3.contrib.socks'
+            ],
+            'dll_excludes': ['w9xpopen.exe', 'crypt32.dll'],
+            # Modules that are only imported dynamically must be added here
+            'includes': ['yt_dlp.compat._legacy', 'yt_dlp.compat._deprecated',
+                         'yt_dlp.utils._legacy', 'yt_dlp.utils._deprecated'],
+        },
+        zipfile=None,
+    )
+
+
+if __name__ == '__main__':
+    main()
diff --git a/pyinst.py b/bundle/pyinstaller.py
old mode 100644
new mode 100755
similarity index 98%
rename from pyinst.py
rename to bundle/pyinstaller.py
index c36f6acd4f..db9dbfde51
--- a/pyinst.py
+++ b/bundle/pyinstaller.py
@@ -4,7 +4,7 @@
 import os
 import sys
 
-sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
 
 import platform
 
diff --git a/pyproject.toml b/pyproject.toml
index 97718ec431..626d9aa133 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,3 +3,6 @@ build-backend = 'setuptools.build_meta'
 # https://github.com/yt-dlp/yt-dlp/issues/5941
 # https://github.com/pypa/distutils/issues/17
 requires = ['setuptools > 50']
+
+[project.entry-points.pyinstaller40]
+hook-dirs = "yt_dlp.__pyinstaller:get_hook_dirs"
diff --git a/setup.py b/setup.py
index 3d9a69d10c..fc5b504683 100644
--- a/setup.py
+++ b/setup.py
@@ -7,7 +7,6 @@ import sys
 sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
 
 import subprocess
-import warnings
 
 try:
     from setuptools import Command, find_packages, setup
@@ -39,46 +38,6 @@ def packages():
     ]
 
 
-def py2exe_params():
-    warnings.warn(
-        'py2exe builds do not support pycryptodomex and needs VC++14 to run. '
-        'It is recommended to run "pyinst.py" to build using pyinstaller instead')
-
-    return {
-        'console': [{
-            'script': './yt_dlp/__main__.py',
-            'dest_base': 'yt-dlp',
-            'icon_resources': [(1, 'devscripts/logo.ico')],
-        }],
-        'version_info': {
-            'version': VERSION,
-            'description': DESCRIPTION,
-            'comments': LONG_DESCRIPTION.split('\n')[0],
-            'product_name': 'yt-dlp',
-            'product_version': VERSION,
-        },
-        'options': {
-            'bundle_files': 0,
-            'compressed': 1,
-            'optimize': 2,
-            'dist_dir': './dist',
-            'excludes': [
-                # py2exe cannot import Crypto
-                'Crypto',
-                'Cryptodome',
-                # py2exe appears to confuse this with our socks library.
-                # We don't use pysocks and urllib3.contrib.socks would fail to import if tried.
-                'urllib3.contrib.socks'
-            ],
-            'dll_excludes': ['w9xpopen.exe', 'crypt32.dll'],
-            # Modules that are only imported dynamically must be added here
-            'includes': ['yt_dlp.compat._legacy', 'yt_dlp.compat._deprecated',
-                         'yt_dlp.utils._legacy', 'yt_dlp.utils._deprecated'],
-        },
-        'zipfile': None,
-    }
-
-
 def build_params():
     files_spec = [
         ('share/bash-completion/completions', ['completions/bash/yt-dlp']),
@@ -127,20 +86,7 @@ class build_lazy_extractors(Command):
 
 
 def main():
-    if sys.argv[1:2] == ['py2exe']:
-        params = py2exe_params()
-        try:
-            from py2exe import freeze
-        except ImportError:
-            import py2exe  # noqa: F401
-            warnings.warn('You are using an outdated version of py2exe. Support for this version will be removed in the future')
-            params['console'][0].update(params.pop('version_info'))
-            params['options'] = {'py2exe': params.pop('options')}
-        else:
-            return freeze(**params)
-    else:
-        params = build_params()
-
+    params = build_params()
     setup(
         name='yt-dlp',  # package name (do not change/remove comment)
         version=VERSION,