Repository: pyinstaller/pyinstaller

Issue:
pkgutil.iter_modules with arbitrary path
## Description of the issue
The iter_modules patch implemented in #5959 has a bug where the path must start with the _MEIPASS or it will throw an assertion error.

The normal iter_modules function can take any valid path. Your code first calls that:
https://github.com/pyinstaller/pyinstaller/blob/develop/PyInstaller/hooks/rthooks/pyi_rth_pkgutil.py#L37

and later asserts it starts with _MEIPASS
https://github.com/pyinstaller/pyinstaller/blob/develop/PyInstaller/hooks/rthooks/pyi_rth_pkgutil.py#L59

which means that a path outside of the executable will throw the assertion error.

I think when implementing it was overlooked that this function could be used to look at a path outside the executable path.

### Context information (for bug reports)

* PyInstaller Version 4.8
* All OS and python versions

I will have a look into creating a pull request to fix this issue.
I think the solution is to change the assertion to an if statement to only run the code below that if it starts with _MEIPASS and thus could be bundled in the executable.


Discussion:
> I think the solution is to change the assertion to an if statement to only run the code below that if it starts with _MEIPASS and thus could be bundled in the executable.

I agree. Changing the assertion into if-statement that returns immediately if path does not start with _MEIPASS sounds like a proper fix.
A few more things I would like to fix with the implementation:
If an empty list is given it will assume the root whereas the vanilla implementation will return an empty list
Was there a reason why only the first path is considered?

Resolving patch:
diff --git a/PyInstaller/hooks/rthooks/pyi_rth_pkgutil.py b/PyInstaller/hooks/rthooks/pyi_rth_pkgutil.py
--- a/PyInstaller/hooks/rthooks/pyi_rth_pkgutil.py
+++ b/PyInstaller/hooks/rthooks/pyi_rth_pkgutil.py
@@ -43,7 +43,7 @@ def _pyi_pkgutil_iter_modules(path=None, prefix=''):
     else:
         return
 
-    if not path:
+    if path is None:
         # Search for all top-level packages/modules. These will have no dots in their entry names.
         for entry in importer.toc:
             if entry.count('.') != 0:
@@ -54,25 +54,28 @@ def _pyi_pkgutil_iter_modules(path=None, prefix=''):
         # Declare SYS_PREFIX locally, to avoid clash with eponymous global symbol from pyi_rth_pkgutil hook.
         SYS_PREFIX = sys._MEIPASS + os.path.sep
         SYS_PREFIXLEN = len(SYS_PREFIX)
-        # Only single path is supported, and it must start with sys._MEIPASS.
-        pkg_path = os.path.normpath(path[0])
-        assert pkg_path.startswith(SYS_PREFIX)
-        # Construct package prefix from path...
-        pkg_prefix = pkg_path[SYS_PREFIXLEN:]
-        pkg_prefix = pkg_prefix.replace(os.path.sep, '.')
-        # ... and ensure it ends with a dot (so we can directly filter out the package itself).
-        if not pkg_prefix.endswith('.'):
-            pkg_prefix += '.'
-        pkg_prefix_len = len(pkg_prefix)
 
-        for entry in importer.toc:
-            if not entry.startswith(pkg_prefix):
-                continue
-            name = entry[pkg_prefix_len:]
-            if name.count('.') != 0:
+        for pkg_path in path:
+            pkg_path = os.path.normpath(pkg_path)
+            if not pkg_path.startswith(SYS_PREFIX):
+                # if the path does not start with sys._MEIPASS then it cannot be a bundled package.
                 continue
-            is_pkg = importer.is_package(entry)
-            yield pkgutil.ModuleInfo(importer, prefix + name, is_pkg)
+            # Construct package prefix from path...
+            pkg_prefix = pkg_path[SYS_PREFIXLEN:]
+            pkg_prefix = pkg_prefix.replace(os.path.sep, '.')
+            # ... and ensure it ends with a dot (so we can directly filter out the package itself).
+            if not pkg_prefix.endswith('.'):
+                pkg_prefix += '.'
+            pkg_prefix_len = len(pkg_prefix)
+
+            for entry in importer.toc:
+                if not entry.startswith(pkg_prefix):
+                    continue
+                name = entry[pkg_prefix_len:]
+                if name.count('.') != 0:
+                    continue
+                is_pkg = importer.is_package(entry)
+                yield pkgutil.ModuleInfo(importer, prefix + name, is_pkg)
 
 
 pkgutil.iter_modules = _pyi_pkgutil_iter_modules


Test patch: