diff --git a/cx_Freeze/parser.py b/cx_Freeze/parser.py index 5973d4f52..5396824f7 100644 --- a/cx_Freeze/parser.py +++ b/cx_Freeze/parser.py @@ -14,24 +14,14 @@ from pathlib import Path from tempfile import TemporaryDirectory -from cx_Freeze._compat import IS_MINGW, IS_WINDOWS, PLATFORM +from cx_Freeze._compat import PLATFORM from cx_Freeze.exception import PlatformError # In Windows, to get dependencies, the default is to use lief package, # but LIEF can be disabled with: # set CX_FREEZE_BIND=imagehlp -if IS_WINDOWS or IS_MINGW: - import lief - - with suppress(ImportError): - from .util import BindError, GetDependentFiles - try: - # LIEF 0.15+ - lief.logging.set_level(lief.logging.LEVEL.ERROR) - except AttributeError: - lief.logging.set_level(lief.logging.LOGGING_LEVEL.ERROR) - -LIEF_DISABLED = os.environ.get("CX_FREEZE_BIND", "") == "imagehlp" + +# In Linux, to get dependencies, the default is to use ldd in x64 platforms LDD_DISABLED = ( os.environ.get( "CX_FREEZE_BIND", "" if PLATFORM.endswith("x86_64") else "patchelf" @@ -119,8 +109,21 @@ class PEParser(Parser): def __init__( self, path: list[str], bin_path_includes: list[str], silent: int = 0 ) -> None: + if os.environ.get("CX_FREEZE_BIND", "") == "imagehlp": + lief = None + else: + try: + import lief + except ImportError: + lief = None + else: + try: + # LIEF 0.15+ + lief.logging.set_level(lief.logging.LEVEL.ERROR) + except AttributeError: + lief.logging.set_level(lief.logging.LOGGING_LEVEL.ERROR) super().__init__(path, bin_path_includes, silent) - if hasattr(lief.PE, "ParserConfig"): + if lief and hasattr(lief.PE, "ParserConfig"): # LIEF 0.14+ imports_only = lief.PE.ParserConfig() imports_only.parse_exports = False @@ -139,6 +142,15 @@ def __init__( else: self.imports_only = None self.resource_only = None + if lief: + self._pe = lief.PE + else: + from cx_Freeze.util import BindError, GetDependentFiles + + self.GetDependentFiles = GetDependentFiles + self.BindError = BindError + self._get_dependent_files = self._get_dependent_files_imagehlp + self._pe = None @staticmethod def is_pe(filename: str | Path) -> bool: @@ -149,9 +161,9 @@ def is_pe(filename: str | Path) -> bool: _is_binary = is_pe - def _get_dependent_files_lief(self, filename: Path) -> set[Path]: + def _get_dependent_files(self, filename: Path) -> set[Path]: with filename.open("rb", buffering=0) as raw: - binary = lief.PE.parse(raw, self.imports_only or filename.name) + binary = self._pe.parse(raw, self.imports_only or filename.name) if not binary: return set() @@ -179,8 +191,8 @@ def _get_dependent_files_imagehlp(self, filename: Path) -> set[Path]: [os.path.normpath(path) for path in self.search_path] ) try: - return {Path(dep) for dep in GetDependentFiles(filename)} - except BindError as exc: + return {Path(dep) for dep in self.GetDependentFiles(filename)} + except self.BindError as exc: # Sometimes this gets called when filename is not actually # a library (See issue 88). if self._silent < 3: @@ -190,20 +202,18 @@ def _get_dependent_files_imagehlp(self, filename: Path) -> set[Path]: os.environ["PATH"] = env_path return set() - if LIEF_DISABLED: - _get_dependent_files = _get_dependent_files_imagehlp - else: - _get_dependent_files = _get_dependent_files_lief - def read_manifest(self, filename: str | Path) -> str: """:return: the XML schema of the manifest included in the executable :rtype: str """ - if isinstance(filename, str): - filename = Path(filename) + if self._pe is None: + if self._silent < 3: + print("WARNING: ignoring read manifest for {filename}") + return "" + filename = Path(filename) with filename.open("rb", buffering=0) as raw: - binary = lief.PE.parse(raw, self.resource_only or filename.name) + binary = self._pe.parse(raw, self.resource_only or filename.name) resources_manager = binary.resources_manager return ( resources_manager.manifest @@ -216,13 +226,16 @@ def write_manifest(self, filename: str | Path, manifest: str) -> None: :rtype: str """ - if isinstance(filename, str): - filename = Path(filename) + if self._pe is None: + if self._silent < 3: + print("WARNING: ignoring write manifest for {filename}") + return + filename = Path(filename) with filename.open("rb", buffering=0) as raw: - binary = lief.PE.parse(raw, self.resource_only or filename.name) + binary = self._pe.parse(raw, self.resource_only or filename.name) resources_manager = binary.resources_manager resources_manager.manifest = manifest - builder = lief.PE.Builder(binary) + builder = self._pe.Builder(binary) builder.build_resources(True) builder.build() with TemporaryDirectory(prefix="cxfreeze-") as tmp_dir: diff --git a/tests/test_executables.py b/tests/test_executables.py index bc93d8188..cdba2c9de 100644 --- a/tests/test_executables.py +++ b/tests/test_executables.py @@ -393,7 +393,7 @@ def test_not_found_icon(tmp_path: Path) -> None: """ -@pytest.mark.skipif(sys.platform != "win32", reason="Windows tests") +@pytest.mark.skipif(not (IS_MINGW or IS_WINDOWS), reason="Windows tests") def test_invalid_icon(tmp_path: Path) -> None: """Test with invalid icon in Windows.""" create_package(tmp_path, SOURCE_INVALID_ICON) diff --git a/tests/test_windows_manifest.py b/tests/test_windows_manifest.py index dd09c2672..5251212eb 100644 --- a/tests/test_windows_manifest.py +++ b/tests/test_windows_manifest.py @@ -3,13 +3,14 @@ from __future__ import annotations import ctypes +import os import sys from typing import TYPE_CHECKING import pytest from generate_samples import create_package, run_command -from cx_Freeze._compat import PLATFORM, PYTHON_VERSION +from cx_Freeze._compat import BUILD_EXE_DIR from cx_Freeze.parser import PEParser if TYPE_CHECKING: @@ -72,7 +73,7 @@ def tmp_manifest(tmp_path_factory) -> Path: tmp_path = tmp_path_factory.mktemp("manifest") create_package(tmp_path, SOURCE) run_command(tmp_path) - return tmp_path / f"build/exe.{PLATFORM}-{PYTHON_VERSION}" + return tmp_path / BUILD_EXE_DIR def test_manifest(tmp_manifest: Path) -> None: @@ -89,6 +90,10 @@ def test_simple_manifest(tmp_manifest: Path) -> None: """With simple manifest, without "supportedOS Id", windows version returned is the compatible version for Windows 8.1, ie, 6.2. """ + if os.environ.get("CX_FREEZE_BIND", "") == "imagehlp": + pytest.skip(reason="Use of lief is disabled") + pytest.importorskip("lief", reason="Depends on extra package: lief") + executable = tmp_manifest / "test_simple_manifest.exe" assert executable.is_file() output = run_command(tmp_manifest, executable, timeout=10) @@ -103,6 +108,10 @@ def test_simple_manifest(tmp_manifest: Path) -> None: def test_uac_admin(tmp_manifest: Path) -> None: """With the uac_admin, should return WinError 740 - requires elevation.""" + if os.environ.get("CX_FREEZE_BIND", "") == "imagehlp": + pytest.skip(reason="Use of lief is disabled") + pytest.importorskip("lief", reason="Depends on extra package: lief") + executable = tmp_manifest / "test_uac_admin.exe" assert executable.is_file() if ctypes.windll.shell32.IsUserAnAdmin(): @@ -113,6 +122,10 @@ def test_uac_admin(tmp_manifest: Path) -> None: def test_uac_uiaccess(tmp_manifest: Path) -> None: """With the uac_uiaccess, should return WinError 740.""" + if os.environ.get("CX_FREEZE_BIND", "") == "imagehlp": + pytest.skip(reason="Use of lief is disabled") + pytest.importorskip("lief", reason="Depends on extra package: lief") + executable = tmp_manifest / "test_uac_uiaccess.exe" assert executable.is_file() if ctypes.windll.shell32.IsUserAnAdmin(): diff --git a/tests/test_winversioninfo.py b/tests/test_winversioninfo.py index e81d8cb76..b9f4acd17 100644 --- a/tests/test_winversioninfo.py +++ b/tests/test_winversioninfo.py @@ -2,7 +2,6 @@ from __future__ import annotations -import sys from pathlib import Path from subprocess import CalledProcessError @@ -10,7 +9,7 @@ from generate_samples import create_package, run_command from packaging.version import Version -from cx_Freeze._compat import BUILD_EXE_DIR +from cx_Freeze._compat import BUILD_EXE_DIR, EXE_SUFFIX, IS_MINGW, IS_WINDOWS from cx_Freeze.winversioninfo import COMMENTS_MAX_LEN, VersionInfo, main_test SOURCE_SIMPLE_TEST = """ @@ -28,7 +27,7 @@ """ -@pytest.mark.skipif(sys.platform != "win32", reason="Windows tests") +@pytest.mark.skipif(not (IS_MINGW or IS_WINDOWS), reason="Windows tests") class TestVersionInfo: """Test VersionInfo class.""" @@ -123,13 +122,13 @@ def test_windows_versions(self, input_version, version) -> None: """ default_version = VersionInfo(input_version) assert default_version.version == version - assert default_version.version_info(Path("test.exe")) + assert default_version.version_info(Path(f"test{EXE_SUFFIX}")) def test_file_not_found(self) -> None: """Test for FileNotFoundError exception.""" version = VersionInfo("0.1") with pytest.raises(FileNotFoundError): - version.stamp("test.exe") + version.stamp(f"test{EXE_SUFFIX}") @pytest.fixture def tmp_test(self, tmp_path) -> Path: @@ -137,7 +136,7 @@ def tmp_test(self, tmp_path) -> Path: create_package(tmp_path, SOURCE_SIMPLE_TEST) run_command(tmp_path) - file_created = tmp_path / BUILD_EXE_DIR / "test.exe" + file_created = tmp_path / BUILD_EXE_DIR / f"test{EXE_SUFFIX}" assert file_created.is_file(), f"file not found: {file_created}" output = run_command(tmp_path, file_created, timeout=10)