Repository: pyinstaller/pyinstaller

Issue:
Implement support for importlib.resources and ResourceReader
Implement support for [importlib.resources](https://docs.python.org/3/whatsnew/3.7.html#importlib-resources) and a respective 
ResourceReader.

See  `importlib.resources.contents()` for a usage example and `importlib.resources._ZipImportResourceReader` for a implementation sample.

Using this function it should be possible to get all modules and resources of a package - both in a PYZ and on filesystem. Please mind: In Python 3.9 there is also [`importlib.resources.files()`](https://docs.python.org/3/library/importlib.html#importlib.resources.files), which only returns "resources" and "subdirectories".
Pyinstaller unable to package correctly some dependencies on Linux
I have tried to export a script with pyinstaller on Linux reading the warnings I have fixed many issues adding hidden imports in spec file but despite I have added hidden imports for all the warning reported in logs with
tf_hidden_imports += collect_submodules('imodulename')

When I execute it crashes with 
```
Unknown Resource Path: <class importlib_resources_adapters.CompatibilityFiles.SpecPath>;
<importlib_resources_adapters.CompatibilityFiles.SpecPath.object at 0x7ff7b945>;
```

The error happens at this line `import from tflite_model_maker import configs`
I have spent 3 days trying to fix this error with no result. 
There is something seriously wrong in how pyinstaller packages dependencies.

pyinstaller 4.4


Discussion:

What version of python? And what version of packages involved (or rather, what's the output of `pip freeze`?).

I can freeze a simple script containing
```from tflite_model_maker import configs```
and it starts without errors, once `tensorflow`'s source (.py) files are explicitly collected. (But even prior to that, the error was explicit `OSError: could not get source code`).
@rokm Python 3.8 on Ubuntu x64

The output of pip freeze is 
```
absl-py==0.10.0
altgraph==0.17
appdirs==1.4.4
apt-xapian-index==0.49
apturl==0.5.2
astunparse==1.6.3
attrs==21.2.0
audioread==2.1.9
beautifulsoup4==4.9.1
blinker==1.4
cachetools==4.1.1
certifi==2020.6.20
cffi==1.14.2
chardet==3.0.4
click==7.1.2
colorama==0.4.3
command-not-found==0.3
cryptography==3.1
cupshelpers==1.0
cycler==0.10.0
Cython==0.29.21
dataclasses==0.6
dbus-python==1.2.16
decorator==5.0.9
dill==0.3.4
distro==1.5.0
distro-info===0.23ubuntu1
dm-tree==0.1.6
empy==3.3.4
entrypoints==0.3
fire==0.4.0
flatbuffers==1.12
future==0.18.2
gast==0.3.3
genmsg==0.5.14
genpy==0.6.9
gin-config==0.4.0
google-api-core==1.31.1
google-api-python-client==2.15.0
google-auth==1.21.0
google-auth-httplib2==0.1.0
google-auth-oauthlib==0.4.1
google-cloud-bigquery==2.23.2
google-cloud-core==1.7.2
google-crc32c==1.1.2
google-pasta==0.2.0
google-resumable-media==1.3.3
googleapis-common-protos==1.53.0
gpg===1.13.1-unknown
grpcio==1.32.0
h5py==2.10.0
html5lib==1.1
httplib2==0.18.1
idna==2.10
importlib-resources==5.2.2
jeepney==0.4.3
joblib==1.0.1
kaggle==1.5.12
keras-nightly==2.5.0.dev2021032900
Keras-Preprocessing==1.1.2
keyring==21.4.0
kiwisolver==1.3.1
language-selector==0.1
launchpadlib==1.10.13
lazr.restfulclient==0.14.3
lazr.uri==1.0.5
librosa==0.8.1
llvmlite==0.36.0
lxml==4.6.3
Markdown==3.2.2
matplotlib==3.4.2
netifaces==0.10.9
neural-structured-learning==1.3.1
numba==0.53.1
numpy==1.19.5
oauthlib==3.1.0
olefile==0.46
opencv-python-headless==4.5.3.56
opt-einsum==3.3.0
packaging==20.4
pandas==1.3.1
pbr==5.4.5
pexpect==4.8.0
Pillow==7.2.0
pkgconfig==1.5.1
ply==3.11
pooch==1.4.0
promise==2.3
proto-plus==1.19.0
protobuf==3.13.0
psutil==5.8.0
ptyprocess==0.6.0
py-cpuinfo==8.0.0
pyasn1==0.4.8
pyasn1-modules==0.2.8
pybind11==2.7.0
pycairo==1.16.2
pycocotools==2.0.2
pycparser==2.20
pycrypto==2.6.1
pycups==1.9.73
Pygments==2.6.1
PyGObject==3.36.0
pyinstaller==4.4
pyinstaller-hooks-contrib==2021.2
PyJWT==1.7.1
pyparsing==2.4.7
PyQt5==5.15.0
PyQt5-sip==12.8.1
python-apt==2.0.0+ubuntu0.20.4.5
python-dateutil==2.8.2
python-debian==0.1.37
python-slugify==5.0.2
pytz==2021.1
PyYAML==5.3.1
reportlab==3.5.48
requests==2.24.0
requests-oauthlib==1.3.0
requests-unixsocket==0.2.0
resampy==0.2.2
rsa==4.6
scikit-learn==0.24.2
scipy==1.5.2
SecretStorage==3.1.2
sentencepiece==0.1.96
simplejson==3.17.2
sip==5.4.0
six==1.15.0
SoundFile==0.10.3.post1
soupsieve==2.0.1
systemd-python==234
tensorboard==2.5.0
tensorboard-data-server==0.6.1
tensorboard-plugin-wit==1.7.0
tensorflow==2.4.1
tensorflow-addons==0.13.0
tensorflow-datasets==4.4.0
tensorflow-estimator==2.4.0
tensorflow-hub==0.11.0
tensorflow-metadata==1.2.0
tensorflow-model-optimization==0.6.0
tensorflowjs==3.8.0
termcolor==1.1.0
testresources==2.0.1
text-unidecode==1.3
tf-models-official==2.3.0
tf-slim==1.1.0
tflite-model-maker==0.2.5
tflite-support==0.1.0rc4
threadpoolctl==2.2.0
tinyaes==1.0.1
toml==0.10.1
tqdm==4.62.0
typeguard==2.12.1
typing-extensions==3.7.4.3
ubuntu-advantage-tools==27.2
ubuntu-drivers-common==0.0.0
ufw==0.36
unattended-upgrades==0.1
uritemplate==3.0.1
urllib3==1.25.10
usb-creator==0.3.7
wadllib==1.3.4
webencodings==0.5.1
Werkzeug==1.0.1
wrapt==1.12.1
xkit==0.0.0
zipp==3.5.0
```

Ah, this looks like lack of proper support for `importlib_resources` / `importlib.resources` in PyInstaller, which makes it a duplicate of #5616. 

That said, you can work around it by adding `--debug noarchive` to your PyInstaller command (if building directly from .py file) or setting `noarchive=True` argument to `EXE()˙ in the spec file. This will cause collected .pyc modules to be stored directly in dist directory instead of in executable-embedded archive, which in turn allows existing importlib facilities to deal with them.

PS: you should also remove `dataclasses` from your environment. It is probably listed as dependency by one of other packages, but it is unnecessary with python 3.7 and later, and is actually causing issues that result in `pandas` modules not being properly collected.

Resolving patch:
diff --git a/PyInstaller/loader/pyimod03_importers.py b/PyInstaller/loader/pyimod03_importers.py
--- a/PyInstaller/loader/pyimod03_importers.py
+++ b/PyInstaller/loader/pyimod03_importers.py
@@ -492,6 +492,65 @@ def exec_module(self, module):
 
         exec(bytecode, module.__dict__)
 
+    def get_resource_reader(self, fullname):
+        """
+        Return importlib.resource-compatible resource reader.
+        """
+        return FrozenResourceReader(self, fullname)
+
+
+class FrozenResourceReader:
+    """
+    Resource reader for importlib.resources / importlib_resources support.
+
+    Currently supports only on-disk resources (support for resources from the embedded archive is missing).
+    However, this should cover the typical use cases (access to data files), as PyInstaller collects data files onto
+    filesystem, and only .pyc modules are collected into embedded archive. One exception are resources collected from
+    zipped eggs (which end up collected into embedded archive), but those should be rare anyway.
+
+    When listing resources, source .py files will not be listed as they are not collected by default. Similarly,
+    sub-directories that contained only .py files are not reconstructed on filesystem, so they will not be listed,
+    either. If access to .py files is required for whatever reason, they need to be explicitly collected as data files
+    anyway, which will place them on filesystem and make them appear as resources.
+
+    For on-disk resources, we *must* return path compatible with pathlib.Path() in order to avoid copy to a temporary
+    file, which might break under some circumstances, e.g., metpy with importlib_resources back-port, due to:
+    https://github.com/Unidata/MetPy/blob/a3424de66a44bf3a92b0dcacf4dff82ad7b86712/src/metpy/plots/wx_symbols.py#L24-L25
+    (importlib_resources tries to use 'fonts/wx_symbols.ttf' as a temporary filename suffix, which fails as it contains
+    a separator).
+
+    Furthermore, some packages expect files() to return either pathlib.Path or zipfile.Path, e.g.,
+    https://github.com/tensorflow/datasets/blob/master/tensorflow_datasets/core/utils/resource_utils.py#L81-L97
+    This makes implementation of mixed support for on-disk and embedded resources using importlib.abc.Traversable
+    protocol rather difficult.
+
+    So in order to maximize compatibility with unfrozen behavior, the below implementation is basically equivalent of
+    importlib.readers.FileReader from python 3.10:
+      https://github.com/python/cpython/blob/839d7893943782ee803536a47f1d4de160314f85/Lib/importlib/readers.py#L11
+    and its underlying classes, importlib.abc.TraversableResources and importlib.abc.ResourceReader:
+      https://github.com/python/cpython/blob/839d7893943782ee803536a47f1d4de160314f85/Lib/importlib/abc.py#L422
+      https://github.com/python/cpython/blob/839d7893943782ee803536a47f1d4de160314f85/Lib/importlib/abc.py#L312
+    """
+    def __init__(self, importer, name):
+        import pathlib  # Local import to avoid bootstrap issues.
+        self.importer = importer
+        self.path = pathlib.Path(sys._MEIPASS).joinpath(*name.split('.'))
+
+    def open_resource(self, resource):
+        return self.files().joinpath(resource).open('rb')
+
+    def resource_path(self, resource):
+        return str(self.path.joinpath(resource))
+
+    def is_resource(self, path):
+        return self.files().joinpath(path).is_file()
+
+    def contents(self):
+        return (item.name for item in self.files().iterdir())
+
+    def files(self):
+        return self.path
+
 
 def install():
     """


Test patch:
diff --git a/tests/functional/scripts/pyi_importlib_resources.py b/tests/functional/scripts/pyi_importlib_resources.py
new file mode 100644
--- /dev/null
+++ b/tests/functional/scripts/pyi_importlib_resources.py
@@ -0,0 +1,323 @@
+#-----------------------------------------------------------------------------
+# Copyright (c) 2021, PyInstaller Development Team.
+#
+# Distributed under the terms of the GNU General Public License (version 2
+# or later) with exception for distributing the bootloader.
+#
+# The full license is in the file COPYING.txt, distributed with this software.
+#
+# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
+#-----------------------------------------------------------------------------
+#
+# A test script for validation of importlib.resources / importlib_resources resource reader implementation.
+#
+# The test package has the following structure:
+#
+# pyi_pkgres_testpkg/
+# ├── a.py
+# ├── b.py
+# ├── __init__.py
+# ├── subpkg1
+# │   ├── c.py
+# │   ├── data
+# │   │   ├── entry1.txt
+# │   │   ├── entry2.md
+# │   │   ├── entry3.rst
+# │   │   └── extra
+# │   │       └── extra_entry1.json
+# │   ├── d.py
+# │   └── __init__.py
+# ├── subpkg2
+# │   ├── __init__.py
+# │   ├── mod.py
+# │   └── subsubpkg21
+# │       ├── __init__.py
+# │       └── mod.py
+# └── subpkg3
+#     ├── _datafile.json
+#     └── __init__.py
+#
+# When run as unfrozen script, this script can be used to check the behavior of "native" resource readers (or
+# compatbility adapters for python-provided loaders) that are provided by importlib.resources (or its
+# importlib_resources back-port for python 3.8 and earlier).
+#
+# When run as a frozen application, this script validates the behavior of the resource reader implemented by
+# PyInstaller. Due to transitivity of test results, this script running without errors both as a native script and
+# as a frozen application serves as proof of conformance for the PyInstaller's reader.
+#
+# NOTE: this test is modelled after pkg_resources test, but it does not support zipped eggs. Similarly, it assumes that
+# in the frozen version, only access to on-disk resources is provided (either by importlib.resources's adapter, or by
+# PyInstaller's custom resource reader, if/when implemented). Without custom resource reader, the frozen version of the
+# test seems to work with the importlib.resources (python 3.9 and later) but not with the importlib_resources (python
+# 3.8 and earlier), due to implementation differences.
+
+import sys
+import pathlib
+
+try:
+    from importlib.resources import (
+        as_file as importlib_resources_as_file,
+        contents as importlib_resources_contents,
+        is_resource as importlib_resources_is_resource,
+        files as importlib_resources_files,
+        path as importlib_resources_path,
+        read_binary as importlib_resources_read_binary,
+        read_text as importlib_resources_read_text,
+    )
+    is_builtin = True
+    print("Using built-in importlib.resources...")
+except ImportError:
+    from importlib_resources import (
+        as_file as importlib_resources_as_file,
+        contents as importlib_resources_contents,
+        is_resource as importlib_resources_is_resource,
+        files as importlib_resources_files,
+        path as importlib_resources_path,
+        read_binary as importlib_resources_read_binary,
+        read_text as importlib_resources_read_text,
+    )
+    is_builtin = False
+    print("Using backported importlib_resources...")
+
+is_frozen = getattr(sys, 'frozen', False)
+
+
+########################################################################
+#          Validate behavior of importlib.resources.contents()         #
+########################################################################
+def _contents_test(pkgname, expected):
+    # For frozen application, remove .py files from expected results.
+    if is_frozen:
+        expected = [x for x in expected if not x.endswith('.py')]
+    # List the content
+    content = list(importlib_resources_contents(pkgname))
+    # Ignore pycache
+    if '__pycache__' in content:
+        content.remove('__pycache__')
+    assert sorted(content) == sorted(expected), f"Content mismatch: {sorted(content)} vs. {sorted(expected)}"
+
+
+# NOTE: contents() seems to be broken in importlib.resources (python 3.9) for zipped eggs, as it also recursively lists
+# all subdirectories (but not their files) instead of just directories within the package directory.
+
+# Top-level package
+expected = ['__init__.py', 'a.py', 'b.py', 'subpkg1', 'subpkg2', 'subpkg3']
+if is_frozen:
+    expected.remove('subpkg2')  # FIXME: frozen reader does not list directories that are not on filesystem.
+_contents_test('pyi_pkgres_testpkg', expected)
+
+# Subpackage #1
+expected = ['__init__.py', 'c.py', 'd.py', 'data']
+_contents_test('pyi_pkgres_testpkg.subpkg1', expected)
+
+# Subpackage #2
+if not is_frozen:
+    # Cannot list non-existing directory.
+    expected = ['__init__.py', 'mod.py', 'subsubpkg21']
+    _contents_test('pyi_pkgres_testpkg.subpkg2', expected)
+
+# Sub-subpackage in subpackage #2
+if not is_frozen:
+    # Cannot list non-existing directory.
+    expected = ['__init__.py', 'mod.py']
+    _contents_test('pyi_pkgres_testpkg.subpkg2.subsubpkg21', expected)
+
+# Subpackage #3
+expected = ['__init__.py', '_datafile.json']
+_contents_test('pyi_pkgres_testpkg.subpkg3', expected)
+
+########################################################################
+#              Validate importlib.resources.is_resource()              #
+########################################################################
+# In general, files (source or data) count as resources, directories do not.
+
+# Querying non-existant resource: return False instead of raising FileNotFoundError
+assert not importlib_resources_is_resource('pyi_pkgres_testpkg', 'subpkg_nonexistant')
+assert not importlib_resources_is_resource('pyi_pkgres_testpkg.subpkg1', 'nonexistant.txt')
+
+# NOTE: frozen reader does not list .py files (nor equivalent .pyc ones)
+assert importlib_resources_is_resource('pyi_pkgres_testpkg', '__init__.py') is not is_frozen
+assert importlib_resources_is_resource('pyi_pkgres_testpkg', '__init__.py') is not is_frozen
+assert importlib_resources_is_resource('pyi_pkgres_testpkg', 'a.py') is not is_frozen
+assert importlib_resources_is_resource('pyi_pkgres_testpkg', 'b.py') is not is_frozen
+
+assert not importlib_resources_is_resource('pyi_pkgres_testpkg', 'subpkg1')
+assert not importlib_resources_is_resource('pyi_pkgres_testpkg', 'subpkg2')  # Non-existent in frozen variant.
+assert not importlib_resources_is_resource('pyi_pkgres_testpkg', 'subpkg3')
+
+assert importlib_resources_is_resource('pyi_pkgres_testpkg.subpkg1', '__init__.py') is not is_frozen
+assert not importlib_resources_is_resource('pyi_pkgres_testpkg.subpkg1', 'data')
+
+# Try to specify a sub-path; should raise ValueError.
+try:
+    ret = importlib_resources_is_resource('pyi_pkgres_testpkg.subpkg1', 'data/entry1.txt')
+except ValueError:
+    pass
+except Exception:
+    raise
+else:
+    assert False, "Expected a ValueError!"
+
+if not is_frozen:
+    assert importlib_resources_is_resource('pyi_pkgres_testpkg.subpkg2', 'mod.py')
+
+assert importlib_resources_is_resource('pyi_pkgres_testpkg.subpkg3', '_datafile.json')
+
+
+########################################################################
+#                  Validate importlib.resources.path()                 #
+########################################################################
+# The path() function returns on-disk path. If the resource was originally on disk, direct path to it is returned.
+# Otherwise, path to a temporary file is returned. This function is probably superseded by files() and as_file(), which
+# are more flexible; for example, path() does not allow access to files in sub-directories (only files that are directly
+# within a package directory).
+def _path_test(pkgname, resource, expected_data):
+    with importlib_resources_path(pkgname, resource) as pth:
+        assert isinstance(pth, pathlib.Path)
+        with open(pth, 'rb') as fp:
+            data = fp.read()
+        if expected_data is not None:
+            assert data.splitlines() == expected_data.splitlines()  # Split to avoid OS-specific newline discrepancies.
+
+
+if not is_frozen:
+    expected_data = b"""from . import a, b  # noqa: F401\nfrom . import subpkg1, subpkg2, subpkg3  # noqa: F401\n"""
+    _path_test('pyi_pkgres_testpkg', '__init__.py', expected_data)
+
+if not is_frozen:
+    expected_data = b"""#\n"""
+    _path_test('pyi_pkgres_testpkg.subpkg2', 'mod.py', expected_data)
+
+expected_data = b"""{\n  "_comment": "Data file in supbkg3."\n}\n"""
+_path_test('pyi_pkgres_testpkg.subpkg3', '_datafile.json', expected_data)
+
+# Try with a non-existant file; should raise FileNotFoundError.
+# NOTE: importlib.resources in python 3.9 seems to do so, but importlib_resources 5.2.2 does not...
+try:
+    _path_test('pyi_pkgres_testpkg.subpkg1', 'nonexistant.txt', None)
+except FileNotFoundError:
+    pass
+except Exception:
+    raise
+else:
+    assert not is_builtin, "Expected a FileNotFoundError!"
+
+# Try to specify a sub-path; should raise ValueError.
+try:
+    _path_test('pyi_pkgres_testpkg.subpkg1', 'data/entry1.txt', None)
+except ValueError:
+    pass
+except Exception:
+    raise
+else:
+    assert False, "Expected a ValueError!"
+
+########################################################################
+#               Validate importlib.resources.read_binary()             #
+########################################################################
+# Data file in pyi_pkgres_testpkg.subpkg3
+expected_data = b"""{\n  "_comment": "Data file in supbkg3."\n}\n"""
+data = importlib_resources_read_binary('pyi_pkgres_testpkg.subpkg3', '_datafile.json')
+assert data.splitlines() == expected_data.splitlines()
+
+# Source file in pyi_pkgres_testpkg
+if not is_frozen:
+    expected_data = b"""from . import a, b  # noqa: F401\nfrom . import subpkg1, subpkg2, subpkg3  # noqa: F401\n"""
+    data = importlib_resources_read_binary('pyi_pkgres_testpkg', '__init__.py')
+    assert data.splitlines() == expected_data.splitlines()
+
+# Try with non-existant file; should raise FileNotFoundError
+try:
+    importlib_resources_read_binary('pyi_pkgres_testpkg.subpkg1', 'nonexistant.txt')
+except FileNotFoundError:
+    pass
+except Exception:
+    raise
+else:
+    assert False, "Expected a FileNotFoundError!"
+
+# Try to specify sub-path; should raise ValueError
+try:
+    importlib_resources_read_binary('pyi_pkgres_testpkg.subpkg1', 'data/entry1.txt')
+except ValueError:
+    pass
+except Exception:
+    raise
+else:
+    assert False, "Expected a ValueError!"
+
+########################################################################
+#                Validate importlib.resources.read_text()              #
+########################################################################
+# Data file in pyi_pkgres_testpkg.subpkg3
+expected_data = """{\n  "_comment": "Data file in supbkg3."\n}\n"""
+data = importlib_resources_read_text('pyi_pkgres_testpkg.subpkg3', '_datafile.json', encoding='utf8')
+assert data.splitlines() == expected_data.splitlines()
+
+# Source file in pyi_pkgres_testpkg
+if not is_frozen:
+    expected_data = """from . import a, b  # noqa: F401\nfrom . import subpkg1, subpkg2, subpkg3  # noqa: F401\n"""
+    data = importlib_resources_read_text('pyi_pkgres_testpkg', '__init__.py', encoding='utf8')
+    assert data.splitlines() == expected_data.splitlines()
+
+# Try with non-existant file; should raise FileNotFoundError
+try:
+    importlib_resources_read_text('pyi_pkgres_testpkg.subpkg1', 'nonexistant.txt', encoding='utf8')
+except FileNotFoundError:
+    pass
+except Exception:
+    raise
+else:
+    assert False, "Expected a FileNotFoundError!"
+
+# Try to specify sub-path; should raise ValueError
+try:
+    importlib_resources_read_text('pyi_pkgres_testpkg.subpkg1', 'data/entry1.txt', encoding='utf8')
+except ValueError:
+    pass
+except Exception:
+    raise
+else:
+    assert False, "Expected a ValueError!"
+
+########################################################################
+#          Validate importlib.resources.files() and as_file()          #
+########################################################################
+# files() should return a Traversable (or just a plain pathlib.Path). For on-disk resources, as_file() should not be
+# opening a temporary-file copy.
+pkg_path = importlib_resources_files('pyi_pkgres_testpkg')
+subpkg1_path = importlib_resources_files('pyi_pkgres_testpkg.subpkg1')
+
+# assert subpkg1_path == pkg_path / 'subpkg1'  # True only for on-disk resources!
+
+# Try to get data file in a sub-directory with subpath consisting of single string. This simulates the use in
+# https://github.com/Unidata/MetPy/blob/a3424de66a44bf3a92b0dcacf4dff82ad7b86712/src/metpy/plots/wx_symbols.py#L24-L25
+# that seems to trip up the back-ported importlib_resources when we do not provide our own resource reader. In that
+# case, its compatibility adapter ends up triggering the copy-to-temporary-file codepath, which errors out because the
+# it ends up generating a temporary file name with separator (that would require creation of intermediate directory).
+# It works correctly with built-in importlib.resources in python 3.9 (even if we do not implement a resource reader).
+data_path = subpkg1_path / 'data/entry1.txt'
+expected_data = b"""Data entry #1 in subpkg1/data.\n"""
+
+with importlib_resources_as_file(data_path) as file_path:
+    with open(file_path, 'rb') as fp:
+        data = fp.read()
+assert data.splitlines() == expected_data.splitlines()
+
+# Try to get a data file in a sub-directory via two paths. The read contents should be the same.
+# If we do not provide our own resource reader, neither way of accessing works in frozen version with back-ported
+# importlib_resources (˙˙FileNotFoundError: Can't open orphan path˙˙).
+# It works correctly with built-in importlib.resources in python 3.9 (even if we do not implement a resource reader).
+expected_data = b"""Data entry #2 in `subpkg1/data`.\n"""
+
+data_path = pkg_path / 'subpkg1' / 'data' / 'entry2.md'
+with importlib_resources_as_file(data_path) as file_path:
+    with open(file_path, 'rb') as fp:
+        data = fp.read()
+assert data.splitlines() == expected_data.splitlines()
+
+data_path = subpkg1_path / 'data' / 'entry2.md'
+with importlib_resources_as_file(data_path) as file_path:
+    with open(file_path, 'rb') as fp:
+        data = fp.read()
+assert data.splitlines() == expected_data.splitlines()
diff --git a/tests/functional/test_importlib_resources.py b/tests/functional/test_importlib_resources.py
new file mode 100644
--- /dev/null
+++ b/tests/functional/test_importlib_resources.py
@@ -0,0 +1,100 @@
+#-----------------------------------------------------------------------------
+# Copyright (c) 2021, PyInstaller Development Team.
+#
+# Distributed under the terms of the GNU General Public License (version 2
+# or later) with exception for distributing the bootloader.
+#
+# The full license is in the file COPYING.txt, distributed with this software.
+#
+# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
+#-----------------------------------------------------------------------------
+#
+# These tests run a test script (scripts/pyi_importlib_resources.py) in unfrozen and frozen form, in combination with
+# a custom test package (modules/pyi_pkg_resources_provider/package). In contrast to test_pkg_resources_provider.py,
+# only source package form is used (zipped egg form is nor tested).
+#
+# Running the unfrozen test script allows us to verify the behavior of importlib.resources (or its importlib_resources
+# back-port for python 3.8 and earlier) and thereby also validate the test script itself. Running the frozen test
+# validates the behavior of the resource reader implemented by PyInstaller's FrozenImporter.
+#
+# For details on the structure of the test and the contents of the test package, see the top comment in the test script
+# itself.
+
+import os
+import shutil
+
+import pytest
+
+from PyInstaller.utils.tests import skipif
+from PyInstaller.compat import is_py39, exec_python, exec_python_rc
+from PyInstaller.utils.hooks import is_module_satisfies
+
+# Directory with testing modules used in some tests.
+_MODULES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'modules')
+
+
+def __exec_python_script(script_filename, pathex):
+    # Prepare the environment - default to 'os.environ'...
+    env = os.environ.copy()
+    # ... and prepend PYTHONPATH with pathex
+    if 'PYTHONPATH' in env:
+        pathex = os.pathsep.join([pathex, env['PYTHONPATH']])
+    env['PYTHONPATH'] = pathex
+    # Run the test script
+    return exec_python_rc(script_filename, env=env)
+
+
+def __get_test_package_path(package_type, tmpdir, monkeypatch):
+    # Same test package, in two different formats: source package or zipped egg (built on-the-fly)
+    src_path = os.path.join(_MODULES_DIR, 'pyi_pkg_resources_provider', 'package')
+    # Source package
+    if package_type == 'pkg':
+        return src_path
+    # Copy files to a tmpdir for building the egg.
+    dest_path = tmpdir.join('src')
+    shutil.copytree(src_path, dest_path.strpath)
+    monkeypatch.chdir(dest_path)
+    # Create an egg from the test package. For debug, show the output of the egg build.
+    print(exec_python('setup.py', 'bdist_egg'))
+    # Obtain the name of the egg, which depends on the Python version.
+    dist_path = dest_path.join('dist')
+    files = os.listdir(dist_path.strpath)
+    assert len(files) == 1
+    egg_name = files[0]
+    assert egg_name.endswith('.egg')
+    # Return the full path to the egg file
+    return dist_path.join(egg_name).strpath
+
+
+@skipif(
+    not is_py39 and not is_module_satisfies('importlib_resources'),
+    reason="Python prior to 3.9 requires importlib_resources."
+)
+@pytest.mark.parametrize('package_type', ['pkg'])  # Zipped egg ('egg') is not supported.
+def test_importlib_resources_source(package_type, tmpdir, script_dir, monkeypatch):
+    # Run the test script unfrozen - to validate it is working and to verify the behavior of importlib.resources
+    # (or importlib_resources back-port).
+    pathex = __get_test_package_path(package_type, tmpdir, monkeypatch)
+    test_script = 'pyi_importlib_resources.py'
+    test_script = os.path.join(
+        str(script_dir),  # not is_py36: str()
+        test_script
+    )
+    ret = __exec_python_script(test_script, pathex=pathex)
+    assert ret == 0, "Test script failed!"
+
+
+@skipif(
+    not is_py39 and not is_module_satisfies('importlib_resources'),
+    reason="Python prior to 3.9 requires importlib_resources."
+)
+@pytest.mark.parametrize('package_type', ['pkg'])  # Zipped egg ('egg') is not supported.
+def test_importlib_resources_frozen(pyi_builder, package_type, tmpdir, script_dir, monkeypatch):
+    # Run the test script as a frozen program
+    pathex = __get_test_package_path(package_type, tmpdir, monkeypatch)
+    test_script = 'pyi_importlib_resources.py'
+    hooks_dir = os.path.join(_MODULES_DIR, 'pyi_pkg_resources_provider', 'hooks')
+    pyi_builder.test_script(
+        test_script,
+        pyi_args=['--paths', pathex, '--hidden-import', 'pyi_pkgres_testpkg', '--additional-hooks-dir', hooks_dir]
+    )
diff --git a/tests/requirements-libraries.txt b/tests/requirements-libraries.txt
--- a/tests/requirements-libraries.txt
+++ b/tests/requirements-libraries.txt
@@ -8,6 +8,8 @@
 # include requirements for base testing
 -r requirements-tools.txt
 
+# Backport of importlib.resources for python 3.8 and earlier.
+importlib_resources==5.2.2; python_version < '3.9'
 
 # Needs work
 # ----------