266 lines
8.6 KiB
Python
266 lines
8.6 KiB
Python
"""Module for the Executable base class."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import string
|
|
import sys
|
|
from collections.abc import Mapping
|
|
from pathlib import Path
|
|
from sysconfig import get_config_var, get_platform
|
|
from typing import TYPE_CHECKING
|
|
|
|
from cx_Freeze._compat import EXE_SUFFIX, IS_MACOS, IS_MINGW, IS_WINDOWS
|
|
from cx_Freeze.common import get_resource_file_path
|
|
from cx_Freeze.exception import OptionError, SetupError
|
|
|
|
if TYPE_CHECKING:
|
|
from setuptools import Distribution
|
|
|
|
STRINGREPLACE = list(
|
|
string.whitespace + string.punctuation.replace(".", "").replace("_", "")
|
|
)
|
|
|
|
__all__ = ["Executable", "validate_executables"]
|
|
|
|
|
|
class Executable:
|
|
"""Base Executable class."""
|
|
|
|
def __init__(
|
|
self,
|
|
script: str | Path,
|
|
init_script: str | Path | None = None,
|
|
base: str | Path | None = None,
|
|
target_name: str | None = None,
|
|
icon: str | Path | None = None,
|
|
shortcut_name: str | None = None,
|
|
shortcut_dir: str | Path | None = None,
|
|
copyright: str | None = None, # noqa: A002
|
|
trademarks: str | None = None,
|
|
manifest: str | Path | None = None,
|
|
uac_admin: bool = False,
|
|
uac_uiaccess: bool = False,
|
|
) -> None:
|
|
self.main_script = script
|
|
self.init_script = init_script
|
|
self.base = base
|
|
self.target_name = target_name
|
|
self.icon = icon
|
|
self.shortcut_name = shortcut_name
|
|
self.shortcut_dir = shortcut_dir
|
|
self.copyright = copyright
|
|
self.trademarks = trademarks
|
|
self.manifest = manifest
|
|
self.uac_admin = uac_admin
|
|
self.uac_uiaccess = uac_uiaccess
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<Executable script={self.main_script}>"
|
|
|
|
@property
|
|
def base(self) -> Path:
|
|
""":return: the name of the base executable
|
|
:rtype: Path
|
|
|
|
"""
|
|
return self._base
|
|
|
|
@base.setter
|
|
def base(self, name: str | Path | None) -> None:
|
|
name = name or "console"
|
|
if name == "gui":
|
|
name = "Win32GUI" if IS_WINDOWS or IS_MINGW else "console"
|
|
elif name == "service":
|
|
name = "Win32Service" if IS_WINDOWS or IS_MINGW else "console"
|
|
if IS_WINDOWS or IS_MINGW:
|
|
platform_nodot = get_platform().replace(".", "").replace("-", "_")
|
|
soabi = f"{sys.implementation.cache_tag}-{platform_nodot}"
|
|
else:
|
|
soabi = get_config_var("SOABI")
|
|
suffix = EXE_SUFFIX
|
|
name_base = f"{name}-{soabi}"
|
|
self._base: Path = get_resource_file_path("bases", name_base, suffix)
|
|
if self._base is None:
|
|
msg = f"no base named {name!r} ({name_base!r})"
|
|
raise OptionError(msg)
|
|
self._ext: str = suffix
|
|
|
|
@property
|
|
def icon(self) -> Path | None:
|
|
""":return: the path of the icon
|
|
:rtype: Path
|
|
|
|
"""
|
|
return self._icon
|
|
|
|
@icon.setter
|
|
def icon(self, name: str | Path | None) -> None:
|
|
iconfile: Path = Path(name) if name else None
|
|
if iconfile and not iconfile.suffix:
|
|
# add an extension
|
|
valid_extensions = [".png", ".svg"]
|
|
if IS_WINDOWS or IS_MINGW:
|
|
valid_extensions.insert(0, ".ico")
|
|
elif IS_MACOS:
|
|
valid_extensions.insert(0, ".icns")
|
|
for ext in valid_extensions:
|
|
iconfile = iconfile.with_suffix(ext)
|
|
if iconfile.exists():
|
|
break
|
|
self._icon: Path | None = iconfile
|
|
|
|
@property
|
|
def init_module_name(self) -> str:
|
|
""":return: the name of the init module in zip file
|
|
:rtype: str
|
|
|
|
"""
|
|
return f"__init__{self._internal_name}"
|
|
|
|
@property
|
|
def init_script(self) -> Path:
|
|
""":return: the name of the initialization script that will be executed
|
|
before the main script is executed
|
|
:rtype: Path
|
|
|
|
"""
|
|
return self._init_script
|
|
|
|
@init_script.setter
|
|
def init_script(self, name: str | Path | None) -> None:
|
|
name = name or "console"
|
|
self._init_script: Path = get_resource_file_path(
|
|
"initscripts", name, ".py"
|
|
)
|
|
if self._init_script is None:
|
|
msg = f"no init_script named {name}"
|
|
raise OptionError(msg)
|
|
|
|
@property
|
|
def main_module_name(self) -> str:
|
|
""":return: the name of the main module in zip file
|
|
:rtype: str
|
|
|
|
"""
|
|
return f"__main__{self._internal_name}"
|
|
|
|
@property
|
|
def main_script(self) -> Path:
|
|
""":return: the path of the file containing the script which is to be
|
|
frozen
|
|
:rtype: Path
|
|
|
|
"""
|
|
return self._main_script
|
|
|
|
@main_script.setter
|
|
def main_script(self, name: str | Path) -> None:
|
|
self._main_script: Path = Path(name)
|
|
|
|
@property
|
|
def manifest(self) -> str | None:
|
|
""":return: the XML schema of the manifest which is to be included in
|
|
the frozen executable
|
|
:rtype: str
|
|
|
|
"""
|
|
return self._manifest
|
|
|
|
@manifest.setter
|
|
def manifest(self, name: str | Path | None) -> None:
|
|
self._manifest: str | None = None
|
|
if name is None:
|
|
return
|
|
if isinstance(name, str):
|
|
name = Path(name)
|
|
self._manifest = name.read_text(encoding="utf-8")
|
|
|
|
@property
|
|
def shortcut_name(self) -> str:
|
|
""":return: the name to give a shortcut for the executable when
|
|
included in an MSI package (Windows only).
|
|
:rtype: str
|
|
|
|
"""
|
|
return self._shortcut_name
|
|
|
|
@shortcut_name.setter
|
|
def shortcut_name(self, name: str) -> None:
|
|
self._shortcut_name: str = name
|
|
|
|
@property
|
|
def shortcut_dir(self) -> Path:
|
|
""":return: tthe directory in which to place the shortcut when being
|
|
installed by an MSI package; see the MSI Shortcut table documentation
|
|
for more information on what values can be placed here (Windows only).
|
|
:rtype: Path
|
|
|
|
"""
|
|
return self._shortcut_dir
|
|
|
|
@shortcut_dir.setter
|
|
def shortcut_dir(self, name: str | Path) -> None:
|
|
self._shortcut_dir: Path = Path(name) if name else None
|
|
|
|
@property
|
|
def target_name(self) -> str:
|
|
""":return: the name of the target executable
|
|
:rtype: str
|
|
|
|
"""
|
|
return self._name + self._ext
|
|
|
|
@target_name.setter
|
|
def target_name(self, name: str | None) -> None:
|
|
if name is None:
|
|
name = self.main_script.stem
|
|
else:
|
|
pathname = Path(name)
|
|
if name != pathname.name:
|
|
msg = (
|
|
"target_name cannot contain the path, only the filename: "
|
|
f"{pathname.name}"
|
|
)
|
|
raise OptionError(msg)
|
|
if sys.platform == "win32" and pathname.suffix.lower() == ".exe":
|
|
name = pathname.stem
|
|
self._name: str = name
|
|
name = name.partition(".")[0]
|
|
if not name.isidentifier():
|
|
for invalid in STRINGREPLACE:
|
|
name = name.replace(invalid, "_")
|
|
name = os.path.normcase(name)
|
|
self._internal_name: str = name
|
|
if not self.init_module_name.isidentifier():
|
|
msg = f"target_name is invalid: {self._name!r}"
|
|
raise OptionError(msg)
|
|
|
|
|
|
def validate_executables(dist: Distribution, attr: str, value) -> None:
|
|
"""Verify that value is a valid executables attribute, which could be an
|
|
Executable list, a mapping list or a string list.
|
|
"""
|
|
try:
|
|
# verify that value is a list or tuple to exclude unordered
|
|
# or single-use iterables
|
|
assert isinstance(value, (list, tuple)) # noqa: S101
|
|
assert value # noqa: S101
|
|
# verify that elements of value are Executable, Dict or string
|
|
for executable in value:
|
|
assert isinstance(executable, (Executable, Mapping, str)) # noqa: S101
|
|
except (TypeError, ValueError, AttributeError, AssertionError) as exc:
|
|
msg = f"{attr!r} must be a list of Executable (got {value!r})"
|
|
raise SetupError(msg) from exc
|
|
|
|
# Returns valid Executable list
|
|
if dist.executables == value:
|
|
dist.executables = []
|
|
executables = list(value)
|
|
for i, executable in enumerate(executables):
|
|
if isinstance(executable, str):
|
|
executables[i] = Executable(executable)
|
|
elif isinstance(executable, Mapping):
|
|
executables[i] = Executable(**executable)
|
|
dist.executables.extend(executables)
|