Repository: marcelotduarte/cx_Freeze (python)

Issue:
api-ms-*.dll are copied regardless of `include_msvcr` value
Might be related to #367. Asked bout this behavior in discussion #2171.

I run a build workflow on github, as I do not own a microsoft machine. During the build, various api-ms-*.dll are copied from the PATH. I feel that this behavior is unwarranted.

Specific to the build:

* My [setup.py](https://github.com/BannerlordCE/pyCEStoriesEditor/blob/2f416a502c17de36822befa98a079b2a8952f98c/setup.py).
* The [build workflow](https://github.com/BannerlordCE/pyCEStoriesEditor/blob/2f416a502c17de36822befa98a079b2a8952f98c/.github/workflows/build.yml)
* Last [job](https://github.com/BannerlordCE/pyCEStoriesEditor/actions/runs/7430262647/job/20219746239) (please look at the *build* section)

Version of cx_Freeze: 6.15.12

It may yet be user error, in which case I apologize.



Discussion:
I can confirm this issue. With VS 2022 and PowerShell, there are new API-ms DLLs to exclude. I'll make a patch.

Patch:
diff --git a/cx_Freeze/freezer.py b/cx_Freeze/freezer.py
--- a/cx_Freeze/freezer.py
+++ b/cx_Freeze/freezer.py
@@ -88,8 +88,8 @@ def __init__(
         self.path: list[str] | None = self._validate_path(path)
         self.include_msvcr: bool = include_msvcr
         self.target_dir = target_dir
-        self.bin_includes: list[str] | None = bin_includes
-        self.bin_excludes: list[str] | None = bin_excludes
+        self.bin_includes: list[str] = self._validate_bin_file(bin_includes)
+        self.bin_excludes: list[str] = self._validate_bin_file(bin_excludes)
         self.bin_path_includes: list[str] = self._validate_bin_path(
             bin_path_includes
         )
@@ -289,7 +289,7 @@ def _freeze_executable(self, exe: Executable) -> None:
             self._copy_top_dependency(source)
             # Once copied, it should be deleted from the list to ensure
             # it will not be copied again.
-            name = Path(source.name)
+            name = os.path.normcase(source.name)
             if name in self.bin_includes:
                 self.bin_includes.remove(name)
                 self.bin_excludes.append(name)
@@ -426,35 +426,36 @@ def _remove_version_numbers(filename: str) -> str:
             return ".".join(parts)
         return filename
 
-    def _should_copy_file(self, path: Path) -> bool:  # noqa: PLR0911
+    def _should_copy_file(self, path: Path) -> bool:
         """Return true if the file should be copied to the target machine.
-        This is done by checking the bin_path_includes, bin_path_excludes,
-        bin_includes and bin_excludes configuration variables using first
-        the full file name, then just the base file name, then the file name
-        without any version numbers.
+
+        This is done by checking the bin_includes and bin_excludes
+        configuration variables using first the full file name, then just the
+        base file name, then the file name without any version numbers.
+        Then, bin_path_includes and bin_path_excludes are checked.
 
         Files are included unless specifically excluded but inclusions take
         precedence over exclusions.
         """
         # check the full path
-        if path in self.bin_includes:
-            return True
-        if path in self.bin_excludes:
-            return False
-
         # check the file name by itself (with any included version numbers)
-        filename = Path(path.name)
-        if filename in self.bin_includes:
-            return True
-        if filename in self.bin_excludes:
-            return False
-
         # check the file name by itself (version numbers removed)
-        filename = Path(self._remove_version_numbers(path.name))
-        if filename in self.bin_includes:
-            return True
-        if filename in self.bin_excludes:
-            return False
+        filename = Path(os.path.normcase(path.name))
+        filename_noversion = Path(self._remove_version_numbers(filename.name))
+        for binfile in self.bin_includes:
+            if (
+                path.match(binfile)
+                or filename.match(binfile)
+                or filename_noversion.match(binfile)
+            ):
+                return True
+        for binfile in self.bin_excludes:
+            if (
+                path.match(binfile)
+                or filename.match(binfile)
+                or filename_noversion.match(binfile)
+            ):
+                return False
 
         # check the path for inclusion/exclusion
         dirname = path.parent
@@ -508,6 +509,15 @@ def _validate_path(path: list[str | Path] | None = None) -> list[str]:
                 path.insert(index, os.fspath(dynload))
         return path
 
+    @staticmethod
+    def _validate_bin_file(
+        filenames: Sequence[str | Path] | None,
+    ) -> list[str]:
+        """Returns valid filenames for bin_includes and bin_excludes."""
+        if filenames is None:
+            return []
+        return [os.path.normcase(filename) for filename in filenames]
+
     @staticmethod
     def _validate_bin_path(bin_path: Sequence[str | Path] | None) -> list[str]:
         """Returns valid search path for bin_path_includes and
@@ -522,15 +532,8 @@ def _validate_bin_path(bin_path: Sequence[str | Path] | None) -> list[str]:
         return valid
 
     def _verify_configuration(self) -> None:
-        """Verify and normalize names and paths."""
-        filenames = list(self.bin_includes or [])
-        filenames += self._default_bin_includes()
-        self.bin_includes = [Path(name) for name in filenames]
-
-        filenames = list(self.bin_excludes or [])
-        filenames += self._default_bin_excludes()
-        self.bin_excludes = [Path(name) for name in filenames]
-
+        self.bin_includes += self._default_bin_includes()
+        self.bin_excludes += self._default_bin_excludes()
         self.bin_path_includes += self._default_bin_path_includes()
         self.bin_path_excludes += self._default_bin_path_excludes()
 
@@ -882,9 +885,8 @@ def _pre_copy_hook(self, source: Path, target: Path) -> tuple[Path, Path]:
         C runtime libraries.
         """
         # fix the target path for C runtime files
-        norm_target_name = target.name.lower()
-        if norm_target_name in self.runtime_files:
-            target = self.target_dir / norm_target_name
+        if any(filter(target.match, self.runtime_files)):
+            target = self.target_dir / target.name
         return source, target
 
     def _post_copy_hook(
@@ -1000,11 +1002,12 @@ def _platform_add_extra_dependencies(
         search_dirs: set[Path] = set()
         for filename in dependent_files:
             search_dirs.add(filename.parent)
-        for filename in self.runtime_files:
-            for search_dir in search_dirs:
-                filepath = search_dir / filename
-                if filepath.exists():
-                    dependent_files.add(filepath)
+        for search_dir in search_dirs:
+            for pattern in self.runtime_files:
+                for filename in search_dir.glob(pattern):
+                    filepath = search_dir / filename
+                    if filepath.exists():
+                        dependent_files.add(filepath)
 
     def _post_freeze_hook(self) -> None:
         target_lib = self.target_dir / "lib"
@@ -1025,9 +1028,9 @@ def runtime_files(self) -> set[str]:
         winmsvcr = import_module("cx_Freeze.winmsvcr")
         if not self.include_msvcr:
             # just put on the exclusion list
-            self.bin_excludes.extend(list(map(Path, winmsvcr.FILES)))
+            self.bin_excludes.extend(winmsvcr.FILES)
             return set()
-        return winmsvcr.FILES
+        return set(winmsvcr.FILES)
 
 
 class DarwinFreezer(Freezer, Parser):
diff --git a/cx_Freeze/winmsvcr.py b/cx_Freeze/winmsvcr.py
--- a/cx_Freeze/winmsvcr.py
+++ b/cx_Freeze/winmsvcr.py
@@ -7,47 +7,8 @@
 from __future__ import annotations
 
 FILES = (
+    "api-ms-win-*.dll",
     # VC 2015 and 2017
-    "api-ms-win-core-console-l1-1-0.dll",
-    "api-ms-win-core-datetime-l1-1-0.dll",
-    "api-ms-win-core-debug-l1-1-0.dll",
-    "api-ms-win-core-errorhandling-l1-1-0.dll",
-    "api-ms-win-core-file-l1-1-0.dll",
-    "api-ms-win-core-file-l1-2-0.dll",
-    "api-ms-win-core-file-l2-1-0.dll",
-    "api-ms-win-core-handle-l1-1-0.dll",
-    "api-ms-win-core-heap-l1-1-0.dll",
-    "api-ms-win-core-interlocked-l1-1-0.dll",
-    "api-ms-win-core-libraryloader-l1-1-0.dll",
-    "api-ms-win-core-localization-l1-2-0.dll",
-    "api-ms-win-core-memory-l1-1-0.dll",
-    "api-ms-win-core-namedpipe-l1-1-0.dll",
-    "api-ms-win-core-processenvironment-l1-1-0.dll",
-    "api-ms-win-core-processthreads-l1-1-0.dll",
-    "api-ms-win-core-processthreads-l1-1-1.dll",
-    "api-ms-win-core-profile-l1-1-0.dll",
-    "api-ms-win-core-rtlsupport-l1-1-0.dll",
-    "api-ms-win-core-string-l1-1-0.dll",
-    "api-ms-win-core-synch-l1-1-0.dll",
-    "api-ms-win-core-synch-l1-2-0.dll",
-    "api-ms-win-core-sysinfo-l1-1-0.dll",
-    "api-ms-win-core-timezone-l1-1-0.dll",
-    "api-ms-win-core-util-l1-1-0.dll",
-    "api-ms-win-crt-conio-l1-1-0.dll",
-    "api-ms-win-crt-convert-l1-1-0.dll",
-    "api-ms-win-crt-environment-l1-1-0.dll",
-    "api-ms-win-crt-filesystem-l1-1-0.dll",
-    "api-ms-win-crt-heap-l1-1-0.dll",
-    "api-ms-win-crt-locale-l1-1-0.dll",
-    "api-ms-win-crt-math-l1-1-0.dll",
-    "api-ms-win-crt-multibyte-l1-1-0.dll",
-    "api-ms-win-crt-private-l1-1-0.dll",
-    "api-ms-win-crt-process-l1-1-0.dll",
-    "api-ms-win-crt-runtime-l1-1-0.dll",
-    "api-ms-win-crt-stdio-l1-1-0.dll",
-    "api-ms-win-crt-string-l1-1-0.dll",
-    "api-ms-win-crt-time-l1-1-0.dll",
-    "api-ms-win-crt-utility-l1-1-0.dll",
     "concrt140.dll",
     "msvcp140_1.dll",
     "msvcp140_2.dll",
@@ -58,7 +19,9 @@
     "vcomp140.dll",
     "vcruntime140.dll",
     # VS 2019
-    "vcruntime140_1.dll",
     "msvcp140_atomic_wait.dll",
     "msvcp140_codecvt_ids.dll",
+    "vcruntime140_1.dll",
+    # VS 2022
+    "vcruntime140_threads.dll",
 )


Test patch:
diff --git a/tests/test_command_build_exe.py b/tests/test_command_build_exe.py
--- a/tests/test_command_build_exe.py
+++ b/tests/test_command_build_exe.py
@@ -97,20 +97,6 @@ def test_build_exe_asmodule(datafiles: Path):
     assert output.startswith("Hello from cx_Freeze")
 
 
-@pytest.mark.skipif(sys.platform != "win32", reason="Windows tests")
-@pytest.mark.datafiles(SAMPLES_DIR / "simple")
-def test_build_exe_simple_include_msvcr(datafiles: Path):
-    """Test the simple sample with include_msvcr option."""
-    command = BUILD_EXE_CMD + " --include-msvcr"
-    output = run_command(datafiles, command)
-
-    build_exe_dir = datafiles / BUILD_EXE_DIR
-    executable = build_exe_dir / f"hello{SUFFIX}"
-    assert executable.is_file()
-    output = run_command(datafiles, executable, timeout=10)
-    assert output.startswith("Hello from cx_Freeze")
-
-
 @pytest.mark.datafiles(SAMPLES_DIR / "sqlite")
 def test_build_exe_sqlite(datafiles: Path):
     """Test the sqlite sample."""
diff --git a/tests/test_winmsvcr.py b/tests/test_winmsvcr.py
--- a/tests/test_winmsvcr.py
+++ b/tests/test_winmsvcr.py
@@ -2,53 +2,17 @@
 from __future__ import annotations
 
 import sys
+from pathlib import Path
+from sysconfig import get_platform, get_python_version
 
 import pytest
+from generate_samples import run_command
 
 from cx_Freeze.winmsvcr import FILES
 
 EXPECTED = (
+    "api-ms-win-*.dll",
     # VC 2015 and 2017
-    "api-ms-win-core-console-l1-1-0.dll",
-    "api-ms-win-core-datetime-l1-1-0.dll",
-    "api-ms-win-core-debug-l1-1-0.dll",
-    "api-ms-win-core-errorhandling-l1-1-0.dll",
-    "api-ms-win-core-file-l1-1-0.dll",
-    "api-ms-win-core-file-l1-2-0.dll",
-    "api-ms-win-core-file-l2-1-0.dll",
-    "api-ms-win-core-handle-l1-1-0.dll",
-    "api-ms-win-core-heap-l1-1-0.dll",
-    "api-ms-win-core-interlocked-l1-1-0.dll",
-    "api-ms-win-core-libraryloader-l1-1-0.dll",
-    "api-ms-win-core-localization-l1-2-0.dll",
-    "api-ms-win-core-memory-l1-1-0.dll",
-    "api-ms-win-core-namedpipe-l1-1-0.dll",
-    "api-ms-win-core-processenvironment-l1-1-0.dll",
-    "api-ms-win-core-processthreads-l1-1-0.dll",
-    "api-ms-win-core-processthreads-l1-1-1.dll",
-    "api-ms-win-core-profile-l1-1-0.dll",
-    "api-ms-win-core-rtlsupport-l1-1-0.dll",
-    "api-ms-win-core-string-l1-1-0.dll",
-    "api-ms-win-core-synch-l1-1-0.dll",
-    "api-ms-win-core-synch-l1-2-0.dll",
-    "api-ms-win-core-sysinfo-l1-1-0.dll",
-    "api-ms-win-core-timezone-l1-1-0.dll",
-    "api-ms-win-core-util-l1-1-0.dll",
-    "api-ms-win-crt-conio-l1-1-0.dll",
-    "api-ms-win-crt-convert-l1-1-0.dll",
-    "api-ms-win-crt-environment-l1-1-0.dll",
-    "api-ms-win-crt-filesystem-l1-1-0.dll",
-    "api-ms-win-crt-heap-l1-1-0.dll",
-    "api-ms-win-crt-locale-l1-1-0.dll",
-    "api-ms-win-crt-math-l1-1-0.dll",
-    "api-ms-win-crt-multibyte-l1-1-0.dll",
-    "api-ms-win-crt-private-l1-1-0.dll",
-    "api-ms-win-crt-process-l1-1-0.dll",
-    "api-ms-win-crt-runtime-l1-1-0.dll",
-    "api-ms-win-crt-stdio-l1-1-0.dll",
-    "api-ms-win-crt-string-l1-1-0.dll",
-    "api-ms-win-crt-time-l1-1-0.dll",
-    "api-ms-win-crt-utility-l1-1-0.dll",
     "concrt140.dll",
     "msvcp140_1.dll",
     "msvcp140_2.dll",
@@ -59,13 +23,52 @@
     "vcomp140.dll",
     "vcruntime140.dll",
     # VS 2019
-    "vcruntime140_1.dll",
     "msvcp140_atomic_wait.dll",
     "msvcp140_codecvt_ids.dll",
+    "vcruntime140_1.dll",
+    # VS 2022
+    "vcruntime140_threads.dll",
 )
 
+PLATFORM = get_platform()
+PYTHON_VERSION = get_python_version()
+BUILD_EXE_DIR = f"build/exe.{PLATFORM}-{PYTHON_VERSION}"
+
+SAMPLES_DIR = Path(__file__).resolve().parent.parent / "samples"
+BUILD_EXE_CMD = "python setup.py build_exe --silent --excludes=tkinter"
+IS_WINDOWS = sys.platform == "win32"
+SUFFIX = ".exe" if IS_WINDOWS else ""
+
 
 @pytest.mark.skipif(sys.platform != "win32", reason="Windows tests")
 def test_files():
     """Test winmsvcr.FILES."""
     assert EXPECTED == FILES
+
+
+@pytest.mark.skipif(sys.platform != "win32", reason="Windows tests")
+@pytest.mark.parametrize("include_msvcr", [False, True], ids=["no", "yes"])
+@pytest.mark.datafiles(SAMPLES_DIR / "sqlite")
+def test_build_exe_with_include_msvcr(datafiles: Path, include_msvcr: bool):
+    """Test the simple sample with include_msvcr option."""
+    command = BUILD_EXE_CMD
+    if include_msvcr:
+        command += " --include-msvcr"
+    output = run_command(datafiles, command)
+
+    build_exe_dir = datafiles / BUILD_EXE_DIR
+
+    executable = build_exe_dir / f"test_sqlite3{SUFFIX}"
+    assert executable.is_file()
+    output = run_command(datafiles, executable, timeout=10)
+    assert output.startswith("dump.sql created")
+
+    names = [
+        file.name.lower()
+        for file in build_exe_dir.glob("*.dll")
+        if any(filter(file.match, EXPECTED))
+    ]
+    if include_msvcr:
+        assert names != []
+    else:
+        assert names == []