"""Implements the 'build_exe' command.""" from __future__ import annotations import logging import os import sys from sysconfig import get_platform, get_python_version from typing import ClassVar from setuptools import Command from cx_Freeze._compat import IS_WINDOWS from cx_Freeze.common import normalize_to_list from cx_Freeze.exception import OptionError, SetupError from cx_Freeze.freezer import Freezer from cx_Freeze.module import ConstantsModule __all__ = ["build_exe"] class build_exe(Command): """Build executables from Python scripts.""" description = "build executables from Python scripts" user_options: ClassVar[list[tuple[str, str | None, str]]] = [ ( "build-exe=", "b", "directory for built executables and dependent files", ), ("includes=", "i", "comma-separated list of modules to include"), ("excludes=", "e", "comma-separated list of modules to exclude"), ( "packages=", "p", "comma-separated list of packages to include, " "which includes all submodules in the package", ), ( "replace-paths=", None, "comma-separated list of paths to replace in included modules, " "using the form =", ), ( "path=", None, "comma-separated list of paths to search for modules; the default " "value is sys.path (use only if you know what you are doing)", ), ( "include-path=", None, "comma-separated list of paths to modify the search for modules", ), ("constants=", None, "comma-separated list of constants to include"), ( "bin-includes=", None, "list of files to include when determining " "dependencies of binary files that would normally be excluded", ), ( "bin-excludes=", None, "list of files to exclude when determining " "dependencies of binary files that would normally be included", ), ( "bin-path-includes=", None, "list of paths from which to include files when determining " "dependencies of binary files", ), ( "bin-path-excludes=", None, "list of paths from which to exclude files when determining " "dependencies of binary files", ), ( "include-files=", "f", "list of tuples of additional files to include in distribution", ), ( "zip-includes=", None, "list of tuples of additional files to include in zip file", ), ( "zip-include-packages=", None, "comma-separated list of packages to include in the zip file " "(or * for all) [default: none]", ), ( "zip-exclude-packages=", None, "comma-separated list of packages to exclude from the zip file " "and place in the file system instead (or * for all) " "[default: *]", ), ( "zip-filename=", None, "filename for the shared zipfile (.zip) " '[default: "library.zip" or None if --no-compress is used]', ), ( "no-compress", None, "create a zip file with no compression (See also --zip-filename)", ), ( "optimize=", "O", 'optimization level: -O1 for "python -O", ' '-O2 for "python -OO" and -O0 to disable [default: -O0]', ), ( "silent", "s", "suppress all output except warnings " "(equivalent to --silent-level=1)", ), ( "silent-level=", None, "suppress output from build_exe command." " level 0: get all messages; [default]" " level 1: suppress information messages, but still get warnings;" " (equivalent to --silent)" " level 2: suppress missing missing-module warnings" " level 3: suppress all warning messages", ), ( "include-msvcr", None, "include the Microsoft Visual C runtime files", ), ] boolean_options: ClassVar[list[str]] = [ "no-compress", "include-msvcr", "silent", ] def add_to_path(self, name) -> None: source_dir = getattr(self, name.lower()) if source_dir is not None: sys.path.insert(0, source_dir) def build_extension(self, name, module_name=None) -> str | None: # XXX: This method, add_to_path and set_source_location can be deleted? if module_name is None: module_name = name source_dir = getattr(self, name.lower()) if source_dir is None: return None orig_dir = os.getcwd() script_args = ["build"] command = self.distribution.get_command_obj("build") if command.compiler is not None: script_args.append(f"--compiler={command.compiler}") os.chdir(source_dir) logging.info("building '%s' extension in '%s'", name, source_dir) distutils_core = __import__("distutils.core", fromlist=["run_setup"]) distribution = distutils_core.run_setup("setup.py", script_args) ext_modules = distribution.ext_modules modules = [m for m in ext_modules if m.name == module_name] if not modules: msg = f"no module named '{module_name}' in '{source_dir}'" raise SetupError(msg) command = distribution.get_command_obj("build_ext") command.ensure_finalized() if command.compiler is None: command.run() else: command.build_extensions() dir_name = os.path.join(source_dir, command.build_lib) os.chdir(orig_dir) if dir_name not in sys.path: sys.path.insert(0, dir_name) return os.path.join( source_dir, command.build_lib, command.get_ext_filename(module_name), ) def initialize_options(self) -> None: self.list_options = [ "excludes", "includes", "packages", "replace_paths", "constants", "include_files", "include_path", "bin_excludes", "bin_includes", "bin_path_excludes", "bin_path_includes", "zip_includes", "zip_exclude_packages", "zip_include_packages", ] for option in self.list_options: setattr(self, option, []) self.zip_exclude_packages = ["*"] self.build_exe = None self.include_msvcr = None self.no_compress = False self.optimize = 0 self.path = None self.silent = None self.silent_level = None self.zip_filename = None def finalize_options(self) -> None: build = self.get_finalized_command("build") # check use of deprecated option options = build.distribution.get_option_dict("build") if options.get("build_exe", (None, None)) != (None, None): msg = ( "[REMOVED] The use of build command with 'build-exe' " "option is deprecated.\n\t\t" "Use build_exe command with 'build-exe' option instead." ) raise OptionError(msg) # check values of build_base and build_exe self.build_base = build.build_base if self.build_exe == self.build_base: msg = "build_exe option cannot be the same as build_base directory" raise SetupError(msg) if not self.build_exe: # empty or None dir_name = f"exe.{get_platform()}-{get_python_version()}" self.build_exe = os.path.join(self.build_base, dir_name) # make sure all options of multiple values are lists for option in self.list_options: setattr(self, option, normalize_to_list(getattr(self, option))) # path - accepts os.pathsep to be backwards compatible with CLI if self.path and isinstance(self.path, str): self.path = self.path.replace(os.pathsep, ",") include_path = self.include_path if include_path: self.path = include_path + normalize_to_list(self.path or sys.path) # the degree of silencing, set from either the silent or silent-level # option, as appropriate self.silent = int(self.silent or self.silent_level or 0) # compression options self.no_compress = bool(self.no_compress) if self.zip_filename: self.zip_filename = os.path.basename( os.path.splitext(self.zip_filename)[0] + ".zip" ) elif self.no_compress is False: self.zip_filename = "library.zip" # include-msvcr is used on Windows, but not in MingW self.include_msvcr = IS_WINDOWS and bool(self.include_msvcr) # optimization level: 0,1,2 self.optimize = int(self.optimize or 0) def run(self) -> None: metadata = self.distribution.metadata constants_module = ConstantsModule( metadata.version, constants=self.constants ) freezer: Freezer = Freezer( self.distribution.executables, constants_module, self.includes, self.excludes, self.packages, self.replace_paths, (not self.no_compress), self.optimize, self.path, self.build_exe, bin_includes=self.bin_includes, bin_excludes=self.bin_excludes, bin_path_includes=self.bin_path_includes, bin_path_excludes=self.bin_path_excludes, include_files=self.include_files, zip_includes=self.zip_includes, zip_include_packages=self.zip_include_packages, zip_exclude_packages=self.zip_exclude_packages, silent=self.silent, metadata=metadata, include_msvcr=self.include_msvcr, zip_filename=self.zip_filename, ) freezer.freeze() freezer.print_report() def set_source_location(self, name, *pathParts) -> None: env_name = f"{name.upper()}_BASE" attr_name = name.lower() source_dir = getattr(self, attr_name) if source_dir is None: base_dir = os.environ.get(env_name) if base_dir is None: return source_dir = os.path.join(base_dir, *pathParts) if os.path.isdir(source_dir): setattr(self, attr_name, source_dir) # -- Predicates for the sub-command list --------------------------- def has_executables(self) -> bool: return getattr(self.distribution, "executables", None) is not None