514 lines
19 KiB
Diff
514 lines
19 KiB
Diff
From 5a53dc58295b26eb8af1f146df990a657bc916d1 Mon Sep 17 00:00:00 2001
|
|
From: Rok Mandeljc <rok.mandeljc@gmail.com>
|
|
Date: Sat, 5 Aug 2023 12:50:28 +0200
|
|
Subject: [PATCH] rthooks: secure temp directories used by matplotlib and
|
|
win32com rthooks
|
|
|
|
The run-time hooks that relocate the package's configuration/cache
|
|
directory into isolated temporary directory create this directory using
|
|
the `tempfile.mkdtemp` function. According to its documentation, the
|
|
function creates the temporary directory "in the most secure manner
|
|
possible", and the created directory should be "readable, writable, and
|
|
searchable only by the creating user ID".
|
|
|
|
However, this does not apply to Windows, where the 0o700 POSIX
|
|
permissions mask passed to the underyling `os.mkdir` call has no effect.
|
|
Consequently, the access to the created temporary directory is in fact
|
|
gated only by the access to the parent directory. So as long as `TEMP`
|
|
and `TMP` point to `%LOCALAPPDATA%\Temp`, the created temporary
|
|
directories are typically inaccessible to other users, who do not have
|
|
access to the user's home directory. On the other hand, if the temporary
|
|
directory base is relocated to a system-wide location (e.g., `c:\temp`),
|
|
the temporary directories created by the run-time hooks might become
|
|
accessible to other users as well. A malicious user with local access
|
|
might thus modify the contents of the temporary directory, interfering
|
|
with the application. If the application is running in privileged mode
|
|
and developer mode is enabled on the system, they might also attempt
|
|
a symlink attack due to lack of hardened mode for `shutil.rmtree`
|
|
(used for clean up) on Windows.
|
|
|
|
Therefore, we replace the use of `tempfile.mkdtemp` with custom function
|
|
that uses original `mkdtemp` on POSIX and provides a Windows-specific
|
|
implementation that secures the access to created directory via security
|
|
descriptor passed to the `CreateDirectoryW` call. This is a
|
|
`ctypes`-based port of the code that we already have in bootloader for
|
|
mitigating the same issue with temporary directory in onefile builds.
|
|
|
|
In order to share the implementation among the two run-time hooks that
|
|
require it, the code is provided by a new `_pyi_rth_utils` PyInstaller
|
|
"fake" package, which is bundled with the frozen application on demand
|
|
(i.e., if it is referenced in any of collected run-time hooks).
|
|
|
|
Origin:
|
|
https://github.com/pyinstaller/pyinstaller/commit/5a53dc58295b26eb8af1f146df990a657bc916d1
|
|
---
|
|
MANIFEST.in | 2 +-
|
|
.../fake-modules/_pyi_rth_utils/__init__.py | 56 ++++
|
|
.../fake-modules/_pyi_rth_utils/_win32.py | 262 ++++++++++++++++++
|
|
.../hook-_pyi_rth_utils.py | 25 ++
|
|
.../hooks/rthooks/pyi_rth_mplconfig.py | 8 +-
|
|
.../hooks/rthooks/pyi_rth_win32comgenpy.py | 8 +-
|
|
news/7827.bugfix.rst | 5 +
|
|
setup.cfg | 1 +
|
|
setup.py | 1 +
|
|
9 files changed, 361 insertions(+), 7 deletions(-)
|
|
create mode 100644 PyInstaller/fake-modules/_pyi_rth_utils/__init__.py
|
|
create mode 100644 PyInstaller/fake-modules/_pyi_rth_utils/_win32.py
|
|
create mode 100644 PyInstaller/hooks/pre_find_module_path/hook-_pyi_rth_utils.py
|
|
create mode 100644 news/7827.bugfix.rst
|
|
|
|
diff --git a/MANIFEST.in b/MANIFEST.in
|
|
index a77f401..2c702a1 100755
|
|
--- a/MANIFEST.in
|
|
+++ b/MANIFEST.in
|
|
@@ -12,6 +12,6 @@ recursive-include PyInstaller/bootloader/Windows-64bit-intel *
|
|
recursive-include PyInstaller/bootloader/Darwin-64bit *
|
|
include pyproject.toml
|
|
# These files need to be explicitly included
|
|
-include PyInstaller/fake-modules/*.py
|
|
+recursive-include PyInstaller/fake-modules *.py
|
|
include PyInstaller/hooks/rthooks.dat
|
|
include PyInstaller/lib/README.rst
|
|
diff --git a/PyInstaller/fake-modules/_pyi_rth_utils/__init__.py b/PyInstaller/fake-modules/_pyi_rth_utils/__init__.py
|
|
new file mode 100644
|
|
index 0000000..2d3af0e
|
|
--- /dev/null
|
|
+++ b/PyInstaller/fake-modules/_pyi_rth_utils/__init__.py
|
|
@@ -0,0 +1,56 @@
|
|
+# -----------------------------------------------------------------------------
|
|
+# Copyright (c) 2023, 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)
|
|
+# -----------------------------------------------------------------------------
|
|
+
|
|
+import os
|
|
+import sys
|
|
+import errno
|
|
+import tempfile
|
|
+
|
|
+# Helper for creating temporary directories with access restricted to the user running the process.
|
|
+# On POSIX systems, this is already achieved by `tempfile.mkdtemp`, which uses 0o700 permissions mask.
|
|
+# On Windows, however, the POSIX permissions semantics have no effect, and we need to provide our own implementation
|
|
+# that restricts the access by passing appropriate security attributes to the `CreateDirectory` function.
|
|
+
|
|
+if os.name == 'nt':
|
|
+ from . import _win32
|
|
+
|
|
+ def secure_mkdtemp(suffix=None, prefix=None, dir=None):
|
|
+ """
|
|
+ Windows-specific replacement for `tempfile.mkdtemp` that restricts access to the user running the process.
|
|
+ Based on `mkdtemp` implementation from python 3.11 stdlib.
|
|
+ """
|
|
+
|
|
+ prefix, suffix, dir, output_type = tempfile._sanitize_params(prefix, suffix, dir)
|
|
+
|
|
+ names = tempfile._get_candidate_names()
|
|
+ if output_type is bytes:
|
|
+ names = map(os.fsencode, names)
|
|
+
|
|
+ for seq in range(tempfile.TMP_MAX):
|
|
+ name = next(names)
|
|
+ file = os.path.join(dir, prefix + name + suffix)
|
|
+ sys.audit("tempfile.mkdtemp", file)
|
|
+ try:
|
|
+ _win32.secure_mkdir(file)
|
|
+ except FileExistsError:
|
|
+ continue # try again
|
|
+ except PermissionError:
|
|
+ # This exception is thrown when a directory with the chosen name already exists on windows.
|
|
+ if (os.name == 'nt' and os.path.isdir(dir) and os.access(dir, os.W_OK)):
|
|
+ continue
|
|
+ else:
|
|
+ raise
|
|
+ return file
|
|
+
|
|
+ raise FileExistsError(errno.EEXIST, "No usable temporary directory name found")
|
|
+
|
|
+else:
|
|
+ secure_mkdtemp = tempfile.mkdtemp
|
|
diff --git a/PyInstaller/fake-modules/_pyi_rth_utils/_win32.py b/PyInstaller/fake-modules/_pyi_rth_utils/_win32.py
|
|
new file mode 100644
|
|
index 0000000..41237fb
|
|
--- /dev/null
|
|
+++ b/PyInstaller/fake-modules/_pyi_rth_utils/_win32.py
|
|
@@ -0,0 +1,262 @@
|
|
+# -----------------------------------------------------------------------------
|
|
+# Copyright (c) 2023, 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)
|
|
+# -----------------------------------------------------------------------------
|
|
+
|
|
+import ctypes
|
|
+import ctypes.wintypes
|
|
+
|
|
+# Constants from win32 headers
|
|
+TOKEN_QUERY = 0x0008
|
|
+
|
|
+TokenUser = 1 # from TOKEN_INFORMATION_CLASS enum
|
|
+
|
|
+ERROR_INSUFFICIENT_BUFFER = 122
|
|
+
|
|
+INVALID_HANDLE = -1
|
|
+
|
|
+FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100
|
|
+FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000
|
|
+
|
|
+SDDL_REVISION1 = 1
|
|
+
|
|
+# Structures for ConvertSidToStringSidW
|
|
+PSID = ctypes.wintypes.LPVOID
|
|
+
|
|
+
|
|
+class SID_AND_ATTRIBUTES(ctypes.Structure):
|
|
+ _fields_ = [
|
|
+ ("Sid", PSID),
|
|
+ ("Attributes", ctypes.wintypes.DWORD),
|
|
+ ]
|
|
+
|
|
+
|
|
+class TOKEN_USER(ctypes.Structure):
|
|
+ _fields_ = [
|
|
+ ("User", SID_AND_ATTRIBUTES),
|
|
+ ]
|
|
+
|
|
+
|
|
+PTOKEN_USER = ctypes.POINTER(TOKEN_USER)
|
|
+
|
|
+# SECURITY_ATTRIBUTES structure for CreateDirectoryW
|
|
+PSECURITY_DESCRIPTOR = ctypes.wintypes.LPVOID
|
|
+
|
|
+
|
|
+class SECURITY_ATTRIBUTES(ctypes.Structure):
|
|
+ _fields_ = [
|
|
+ ("nLength", ctypes.wintypes.DWORD),
|
|
+ ("lpSecurityDescriptor", PSECURITY_DESCRIPTOR),
|
|
+ ("bInheritHandle", ctypes.wintypes.BOOL),
|
|
+ ]
|
|
+
|
|
+
|
|
+# win32 API functions, bound via ctypes.
|
|
+# NOTE: we do not use ctypes.windll.<dll_name> to avoid modifying its (global) function prototypes, which might affect
|
|
+# user's code.
|
|
+kernel32 = ctypes.WinDLL("kernel32")
|
|
+advapi32 = ctypes.WinDLL("advapi32")
|
|
+
|
|
+kernel32.CloseHandle.restype = ctypes.wintypes.BOOL
|
|
+kernel32.CloseHandle.argtypes = (ctypes.wintypes.HANDLE,)
|
|
+
|
|
+kernel32.LocalFree.restype = ctypes.wintypes.BOOL
|
|
+kernel32.LocalFree.argtypes = (ctypes.wintypes.HLOCAL,)
|
|
+
|
|
+kernel32.GetCurrentProcess.restype = ctypes.wintypes.HANDLE
|
|
+
|
|
+kernel32.OpenProcessToken.restype = ctypes.wintypes.BOOL
|
|
+kernel32.OpenProcessToken.argtypes = (
|
|
+ ctypes.wintypes.HANDLE,
|
|
+ ctypes.wintypes.DWORD,
|
|
+ ctypes.wintypes.PHANDLE,
|
|
+)
|
|
+
|
|
+advapi32.ConvertSidToStringSidW.restype = ctypes.wintypes.BOOL
|
|
+advapi32.ConvertSidToStringSidW.argtypes = (
|
|
+ PSID,
|
|
+ ctypes.POINTER(ctypes.wintypes.LPWSTR),
|
|
+)
|
|
+
|
|
+advapi32.ConvertStringSecurityDescriptorToSecurityDescriptorW.restype = ctypes.wintypes.BOOL
|
|
+advapi32.ConvertStringSecurityDescriptorToSecurityDescriptorW.argtypes = (
|
|
+ ctypes.wintypes.LPCWSTR,
|
|
+ ctypes.wintypes.DWORD,
|
|
+ ctypes.POINTER(PSECURITY_DESCRIPTOR),
|
|
+ ctypes.wintypes.PULONG,
|
|
+)
|
|
+
|
|
+
|
|
+def _win_error_to_message(error_code):
|
|
+ """
|
|
+ Convert win32 error code to message.
|
|
+ """
|
|
+ message_wstr = ctypes.wintypes.LPWSTR(None)
|
|
+ ret = kernel32.FormatMessageW(
|
|
+ FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
|
|
+ None, # lpSource
|
|
+ error_code, # dwMessageId
|
|
+ 0x400, # dwLanguageId = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT)
|
|
+ ctypes.byref(message_wstr), # pointer to LPWSTR due to FORMAT_MESSAGE_ALLOCATE_BUFFER
|
|
+ 64, # due to FORMAT_MESSAGE_ALLOCATE_BUFFER, this is minimum number of characters to allocate
|
|
+ )
|
|
+ if ret == 0:
|
|
+ return None
|
|
+
|
|
+ message = message_wstr.value
|
|
+ kernel32.LocalFree(message_wstr)
|
|
+
|
|
+ # Strip trailing CR/LF.
|
|
+ if message:
|
|
+ message = message.strip()
|
|
+ return message
|
|
+
|
|
+
|
|
+def _get_user_sid():
|
|
+ """
|
|
+ Obtain the SID for the current user.
|
|
+ """
|
|
+ process_token = ctypes.wintypes.HANDLE(INVALID_HANDLE)
|
|
+
|
|
+ try:
|
|
+ # Get access token for the current process
|
|
+ ret = kernel32.OpenProcessToken(
|
|
+ kernel32.GetCurrentProcess(),
|
|
+ TOKEN_QUERY,
|
|
+ ctypes.pointer(process_token),
|
|
+ )
|
|
+ if ret == 0:
|
|
+ error_code = kernel32.GetLastError()
|
|
+ raise RuntimeError(f"Failed to open process token! Error code: 0x{error_code:X}")
|
|
+
|
|
+ # Query buffer size for user info structure
|
|
+ user_info_size = ctypes.wintypes.DWORD(0)
|
|
+
|
|
+ ret = advapi32.GetTokenInformation(
|
|
+ process_token,
|
|
+ TokenUser,
|
|
+ None,
|
|
+ 0,
|
|
+ ctypes.byref(user_info_size),
|
|
+ )
|
|
+
|
|
+ # We expect this call to fail with ERROR_INSUFFICIENT_BUFFER
|
|
+ if ret == 0:
|
|
+ error_code = kernel32.GetLastError()
|
|
+ if error_code != ERROR_INSUFFICIENT_BUFFER:
|
|
+ raise RuntimeError(f"Failed to query token information buffer size! Error code: 0x{error_code:X}")
|
|
+ else:
|
|
+ raise RuntimeError("Unexpected return value from GetTokenInformation!")
|
|
+
|
|
+ # Allocate buffer
|
|
+ user_info = ctypes.create_string_buffer(user_info_size.value)
|
|
+ ret = advapi32.GetTokenInformation(
|
|
+ process_token,
|
|
+ TokenUser,
|
|
+ user_info,
|
|
+ user_info_size,
|
|
+ ctypes.byref(user_info_size),
|
|
+ )
|
|
+ if ret == 0:
|
|
+ error_code = kernel32.GetLastError()
|
|
+ raise RuntimeError(f"Failed to query token information! Error code: 0x{error_code:X}")
|
|
+
|
|
+ # Convert SID to string
|
|
+ # Technically, we need to pass user_info->User.Sid, but as they are at the beginning of the
|
|
+ # buffer, just pass the buffer instead...
|
|
+ sid_wstr = ctypes.wintypes.LPWSTR(None)
|
|
+ ret = advapi32.ConvertSidToStringSidW(
|
|
+ ctypes.cast(user_info, PTOKEN_USER).contents.User.Sid,
|
|
+ ctypes.pointer(sid_wstr),
|
|
+ )
|
|
+ if ret == 0:
|
|
+ error_code = kernel32.GetLastError()
|
|
+ raise RuntimeError(f"Failed to convert SID to string! Error code: 0x{error_code:X}")
|
|
+ sid = sid_wstr.value
|
|
+ kernel32.LocalFree(sid_wstr)
|
|
+ except Exception:
|
|
+ sid = None
|
|
+ finally:
|
|
+ # Close the process token
|
|
+ if process_token.value != INVALID_HANDLE:
|
|
+ kernel32.CloseHandle(process_token)
|
|
+
|
|
+ return sid
|
|
+
|
|
+
|
|
+# Get and cache current user's SID
|
|
+_user_sid = _get_user_sid()
|
|
+
|
|
+
|
|
+def secure_mkdir(dir_name):
|
|
+ """
|
|
+ Replacement for mkdir that limits the access to created directory to current user.
|
|
+ """
|
|
+
|
|
+ # Create security descriptor
|
|
+ # Prefer actual user SID over SID S-1-3-4 (current owner), because at the time of writing, Wine does not properly
|
|
+ # support the latter.
|
|
+ sid = _user_sid or "S-1-3-4"
|
|
+
|
|
+ # DACL descriptor (D):
|
|
+ # ace_type;ace_flags;rights;object_guid;inherit_object_guid;account_sid;(resource_attribute)
|
|
+ # - ace_type = SDDL_ACCESS_ALLOWED (A)
|
|
+ # - rights = SDDL_FILE_ALL (FA)
|
|
+ # - account_sid = current user (queried SID)
|
|
+ security_desc_str = f"D:(A;;FA;;;{sid})"
|
|
+ security_desc = ctypes.wintypes.LPVOID(None)
|
|
+
|
|
+ ret = advapi32.ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
|
+ security_desc_str,
|
|
+ SDDL_REVISION1,
|
|
+ ctypes.byref(security_desc),
|
|
+ None,
|
|
+ )
|
|
+ if ret == 0:
|
|
+ error_code = kernel32.GetLastError()
|
|
+ raise RuntimeError(
|
|
+ f"Failed to create security descriptor! Error code: 0x{error_code:X}, "
|
|
+ f"message: {_win_error_to_message(error_code)}"
|
|
+ )
|
|
+
|
|
+ security_attr = SECURITY_ATTRIBUTES()
|
|
+ security_attr.nLength = ctypes.sizeof(SECURITY_ATTRIBUTES)
|
|
+ security_attr.lpSecurityDescriptor = security_desc
|
|
+ security_attr.bInheritHandle = False
|
|
+
|
|
+ # Create directory
|
|
+ ret = kernel32.CreateDirectoryW(
|
|
+ dir_name,
|
|
+ security_attr,
|
|
+ )
|
|
+ if ret == 0:
|
|
+ # Call failed; store error code immediately, to avoid it being overwritten in cleanup below.
|
|
+ error_code = kernel32.GetLastError()
|
|
+
|
|
+ # Free security descriptor
|
|
+ kernel32.LocalFree(security_desc)
|
|
+
|
|
+ # Exit on succeess
|
|
+ if ret != 0:
|
|
+ return
|
|
+
|
|
+ # Construct OSError from win error code
|
|
+ error_message = _win_error_to_message(error_code)
|
|
+
|
|
+ # Strip trailing dot to match error message from os.mkdir().
|
|
+ if error_message and error_message[-1] == '.':
|
|
+ error_message = error_message[:-1]
|
|
+
|
|
+ raise OSError(
|
|
+ None, # errno
|
|
+ error_message, # strerror
|
|
+ dir_name, # filename
|
|
+ error_code, # winerror
|
|
+ None, # filename2
|
|
+ )
|
|
diff --git a/PyInstaller/hooks/pre_find_module_path/hook-_pyi_rth_utils.py b/PyInstaller/hooks/pre_find_module_path/hook-_pyi_rth_utils.py
|
|
new file mode 100644
|
|
index 0000000..d035df0
|
|
--- /dev/null
|
|
+++ b/PyInstaller/hooks/pre_find_module_path/hook-_pyi_rth_utils.py
|
|
@@ -0,0 +1,25 @@
|
|
+# -----------------------------------------------------------------------------
|
|
+# Copyright (c) 2023, 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)
|
|
+# -----------------------------------------------------------------------------
|
|
+"""
|
|
+This hook allows discovery and collection of PyInstaller's internal _pyi_rth_utils module that provides utility
|
|
+functions for run-time hooks.
|
|
+
|
|
+The module is implemented in 'PyInstaller/fake-modules/_pyi_rth_utils.py'.
|
|
+"""
|
|
+
|
|
+import os
|
|
+
|
|
+from PyInstaller import PACKAGEPATH
|
|
+
|
|
+
|
|
+def pre_find_module_path(api):
|
|
+ module_dir = os.path.join(PACKAGEPATH, 'fake-modules')
|
|
+ api.search_dirs = [module_dir]
|
|
diff --git a/PyInstaller/hooks/rthooks/pyi_rth_mplconfig.py b/PyInstaller/hooks/rthooks/pyi_rth_mplconfig.py
|
|
index 018d6fe..6ea9425 100755
|
|
--- a/PyInstaller/hooks/rthooks/pyi_rth_mplconfig.py
|
|
+++ b/PyInstaller/hooks/rthooks/pyi_rth_mplconfig.py
|
|
@@ -27,10 +27,12 @@ def _pyi_rthook():
|
|
import atexit
|
|
import os
|
|
import shutil
|
|
- import tempfile
|
|
|
|
- # Put matplot config dir to temp directory.
|
|
- configdir = tempfile.mkdtemp()
|
|
+ import _pyi_rth_utils # PyInstaller's run-time hook utilities module
|
|
+
|
|
+ # Isolate matplotlib's config dir into temporary directory.
|
|
+ # Use our replacement for `tempfile.mkdtemp` function that properly restricts access to directory on all platforms.
|
|
+ configdir = _pyi_rth_utils.secure_mkdtemp()
|
|
os.environ['MPLCONFIGDIR'] = configdir
|
|
|
|
try:
|
|
diff --git a/PyInstaller/hooks/rthooks/pyi_rth_win32comgenpy.py b/PyInstaller/hooks/rthooks/pyi_rth_win32comgenpy.py
|
|
index aed2515..a9bbbd1 100755
|
|
--- a/PyInstaller/hooks/rthooks/pyi_rth_win32comgenpy.py
|
|
+++ b/PyInstaller/hooks/rthooks/pyi_rth_win32comgenpy.py
|
|
@@ -21,13 +21,15 @@ def _pyi_rthook():
|
|
import atexit
|
|
import os
|
|
import shutil
|
|
- import tempfile
|
|
|
|
import win32com
|
|
|
|
- # Create temporary directory. The actual cache directory needs to be named `gen_py`, so create a sub-directory.
|
|
- supportdir = tempfile.mkdtemp()
|
|
+ import _pyi_rth_utils # PyInstaller's run-time hook utilities module
|
|
|
|
+ # Create temporary directory.
|
|
+ # Use our replacement for `tempfile.mkdtemp` function that properly restricts access to directory on all platforms.
|
|
+ supportdir = _pyi_rth_utils.secure_mkdtemp()
|
|
+ # The actual cache directory needs to be named `gen_py`, so create a sub-directory.
|
|
genpydir = os.path.join(supportdir, 'gen_py')
|
|
os.makedirs(genpydir, exist_ok=True)
|
|
|
|
diff --git a/news/7827.bugfix.rst b/news/7827.bugfix.rst
|
|
new file mode 100644
|
|
index 0000000..41ea37a
|
|
--- /dev/null
|
|
+++ b/news/7827.bugfix.rst
|
|
@@ -0,0 +1,5 @@
|
|
+(Windows) Ensure that the access to temporary directories created by
|
|
+the ``matplotlib`` and ``win32com`` run-time hooks is restricted to
|
|
+the user running the frozen application, even if ``TMP`` / ``TEMP``
|
|
+environment directory points to a system-wide location that can be
|
|
+accessed to all users.
|
|
diff --git a/setup.cfg b/setup.cfg
|
|
index 7cd1548..756ae34 100755
|
|
--- a/setup.cfg
|
|
+++ b/setup.cfg
|
|
@@ -70,6 +70,7 @@ include =
|
|
PyInstaller =
|
|
bootloader/*/*
|
|
fake-modules/*.py
|
|
+ fake-modules/_pyi_rth_utils/*.py
|
|
hooks/rthooks.dat
|
|
lib/README.rst
|
|
|
|
diff --git a/setup.py b/setup.py
|
|
index 51d2482..ac82bb6 100755
|
|
--- a/setup.py
|
|
+++ b/setup.py
|
|
@@ -114,6 +114,7 @@ def finalize_options(self):
|
|
*(f"bootloader/images/*.{suffix}" for suffix in self.ICON_TYPES),
|
|
# These files need to be explicitly included as well.
|
|
"fake-modules/*.py",
|
|
+ "fake-modules/_pyi_rth_utils/*.py",
|
|
"hooks/rthooks.dat",
|
|
"lib/README.rst",
|
|
],
|
|
--
|
|
2.43.0
|
|
|