mit neuen venv und exe-Files

This commit is contained in:
2024-11-03 17:26:54 +01:00
parent 07c05a338a
commit 0c373ff593
15115 changed files with 1998469 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
"""Extend msilib Dialog."""
from __future__ import annotations
from msilib import Control, Dialog
class PyDialog(Dialog):
"""Dialog class with a fixed layout: controls at the top, then a ruler,
then a list of buttons: back, next, cancel. Optionally a bitmap at the
left.
"""
def __init__(
self,
db,
name,
x,
y,
w,
h,
attr,
title,
first,
default,
cancel,
bitmap=True, # noqa: ARG002
) -> None:
Dialog.__init__(
self, db, name, x, y, w, h, attr, title, first, default, cancel
)
ruler = self.h - 36
# bmwidth = 152 * ruler / 328
# if kw.get("bitmap", True):
# self.bitmap("Bitmap", 0, 0, bmwidth, ruler, "PythonWin")
self.line("BottomLine", 0, ruler, self.w, 0)
def title(self, title) -> None:
"""Set the title text of the dialog at the top."""
# flags=0x30003=Visible|Enabled|Transparent|NoPrefix
# text, in VerdanaBold10
font = r"{\VerdanaBold10}"
self.text("Title", 15, 10, 320, 60, 0x30003, f"{font}{title}")
def backbutton(self, title, tabnext, name="Back", active=1) -> Control:
"""Add a back button with a given title, the tab-next button,
its name in the Control table, possibly initially disabled.
Return the button, so that events can be associated
"""
flags = 3 if active else 1 # Visible|Enabled or Visible
return self.pushbutton(
name, 180, self.h - 27, 56, 17, flags, title, tabnext
)
def cancelbutton(self, title, tabnext, name="Cancel", active=1) -> Control:
"""Add a cancel button with a given title, the tab-next button,
its name in the Control table, possibly initially disabled.
Return the button, so that events can be associated
"""
flags = 3 if active else 1 # Visible|Enabled or Visible
return self.pushbutton(
name, 304, self.h - 27, 56, 17, flags, title, tabnext
)
def nextbutton(self, title, tabnext, name="Next", active=1) -> Control:
"""Add a Next button with a given title, the tab-next button,
its name in the Control table, possibly initially disabled.
Return the button, so that events can be associated
"""
flags = 3 if active else 1 # Visible|Enabled or Visible
return self.pushbutton(
name, 236, self.h - 27, 56, 17, flags, title, tabnext
)
def xbutton(self, name, title, tabnext, xpos) -> Control:
"""Add a button with a given title, the tab-next button,
its name in the Control table, giving its x position; the
y-position is aligned with the other buttons.
Return the button, so that events can be associated
"""
return self.pushbutton(
name,
int(self.w * xpos - 28),
self.h - 27,
56,
17,
3,
title,
tabnext,
)

View File

@@ -0,0 +1,313 @@
"""Implements the 'bdist_appimage' command (create Linux AppImage format).
https://appimage.org/
https://docs.appimage.org/
https://docs.appimage.org/packaging-guide/manual.html#ref-manual
"""
from __future__ import annotations
import os
import platform
import shutil
import stat
from ctypes.util import find_library
from logging import INFO, WARNING
from pathlib import Path
from textwrap import dedent
from typing import ClassVar
from urllib.request import urlretrieve
from zipfile import ZipFile
from filelock import FileLock
from setuptools import Command
import cx_Freeze.icons
from cx_Freeze.exception import ExecError, PlatformError
__all__ = ["bdist_appimage"]
ARCH = platform.machine()
APPIMAGEKIT_URL = "https://github.com/AppImage/AppImageKit/releases"
APPIMAGEKIT_PATH = f"download/continuous/appimagetool-{ARCH}.AppImage"
APPIMAGEKIT_TOOL = "~/.local/bin/appimagetool"
class bdist_appimage(Command):
"""Create a Linux AppImage."""
description = "create a Linux AppImage"
user_options: ClassVar[list[tuple[str, str | None, str]]] = [
(
"appimagekit=",
None,
f'path to AppImageKit [default: "{APPIMAGEKIT_TOOL}"]',
),
(
"bdist-base=",
None,
"base directory for creating built distributions",
),
(
"build-dir=",
"b",
"directory of built executables and dependent files",
),
(
"dist-dir=",
"d",
'directory to put final built distributions in [default: "dist"]',
),
(
"skip-build",
None,
"skip rebuilding everything (for testing/debugging)",
),
("target-name=", None, "name of the file to create"),
("target-version=", None, "version of the file to create"),
("silent", "s", "suppress all output except warnings"),
]
boolean_options: ClassVar[list[str]] = [
"skip-build",
"silent",
]
def initialize_options(self) -> None:
self.appimagekit = None
self.bdist_base = None
self.build_dir = None
self.dist_dir = None
self.skip_build = None
self.target_name = None
self.target_version = None
self.fullname = None
self.silent = None
self._warnings = []
def finalize_options(self) -> None:
if os.name != "posix":
msg = (
"don't know how to create AppImage "
f"distributions on platform {os.name}"
)
raise PlatformError(msg)
# inherit options
self.set_undefined_options(
"build_exe",
("build_exe", "build_dir"),
("silent", "silent"),
)
self.set_undefined_options(
"bdist",
("bdist_base", "bdist_base"),
("dist_dir", "dist_dir"),
("skip_build", "skip_build"),
)
# for the bdist commands, there is a chance that build_exe has already
# been executed, so check skip_build if build_exe have_run
if not self.skip_build and self.distribution.have_run.get("build_exe"):
self.skip_build = 1
if self.target_name is None:
if self.distribution.metadata.name:
self.target_name = self.distribution.metadata.name
else:
executables = self.distribution.executables
executable = executables[0]
self.warn_delayed(
"using the first executable as target_name: "
f"{executable.target_name}"
)
self.target_name = executable.target_name
if self.target_version is None and self.distribution.metadata.version:
self.target_version = self.distribution.metadata.version
name = self.target_name
version = self.target_version
name, ext = os.path.splitext(name)
if ext == ".AppImage":
self.app_name = self.target_name
self.fullname = name
elif version:
self.app_name = f"{name}-{version}-{ARCH}.AppImage"
self.fullname = f"{name}-{version}"
else:
self.app_name = f"{name}-{ARCH}.AppImage"
self.fullname = name
if self.silent is not None:
self.verbose = 0 if self.silent else 2
build_exe = self.distribution.command_obj.get("build_exe")
if build_exe:
build_exe.silent = self.silent
# validate or download appimagekit
self._get_appimagekit()
def _get_appimagekit(self) -> None:
"""Fetch AppImageKit from the web if not available locally."""
appimagekit = os.path.expanduser(self.appimagekit or APPIMAGEKIT_TOOL)
appimagekit_dir = os.path.dirname(appimagekit)
self.mkpath(appimagekit_dir)
with FileLock(appimagekit + ".lock"):
if not os.path.exists(appimagekit):
self.announce(
f"download and install AppImageKit from {APPIMAGEKIT_URL}",
INFO,
)
name = os.path.basename(APPIMAGEKIT_PATH)
filename = os.path.join(appimagekit_dir, name)
if not os.path.exists(filename):
urlretrieve( # noqa: S310
os.path.join(APPIMAGEKIT_URL, APPIMAGEKIT_PATH),
filename,
)
os.chmod(filename, stat.S_IRWXU)
if not os.path.exists(appimagekit):
self.execute(
os.symlink,
(filename, appimagekit),
msg=f"linking {appimagekit} -> {filename}",
)
self.appimagekit = appimagekit
def run(self) -> None:
# Create the application bundle
if not self.skip_build:
self.run_command("build_exe")
# Make appimage (by default in dist directory)
# Set the full path of appimage to be built
self.mkpath(self.dist_dir)
output = os.path.abspath(os.path.join(self.dist_dir, self.app_name))
if os.path.exists(output):
os.unlink(output)
# Make AppDir folder
appdir = os.path.join(self.bdist_base, "AppDir")
if os.path.exists(appdir):
self.execute(shutil.rmtree, (appdir,), msg=f"removing {appdir}")
self.mkpath(appdir)
# Copy from build_exe
self.copy_tree(self.build_dir, appdir, preserve_symlinks=True)
# Remove zip file after putting all files in the file system
# (appimage is a compressed file, no need of internal zip file)
library_data = Path(appdir, "lib", "library.dat")
if library_data.exists():
target_lib_dir = library_data.parent
filename = target_lib_dir / library_data.read_bytes().decode()
with ZipFile(filename) as outfile:
outfile.extractall(target_lib_dir)
filename.unlink()
library_data.unlink()
# Add icon, desktop file, entrypoint
share_icons = os.path.join("share", "icons")
icons_dir = os.path.join(appdir, share_icons)
self.mkpath(icons_dir)
executables = self.distribution.executables
executable = executables[0]
if len(executables) > 1:
self.warn_delayed(
"using the first executable as entrypoint: "
f"{executable.target_name}"
)
if executable.icon is None:
icon_name = "logox128.png"
icon_source_dir = os.path.dirname(cx_Freeze.icons.__file__)
self.copy_file(os.path.join(icon_source_dir, icon_name), icons_dir)
else:
icon_name = executable.icon.name
self.move_file(os.path.join(appdir, icon_name), icons_dir)
relative_reference = os.path.join(share_icons, icon_name)
origin = os.path.join(appdir, ".DirIcon")
self.execute(
os.symlink,
(relative_reference, origin),
msg=f"linking {origin} -> {relative_reference}",
)
desktop_entry = f"""\
[Desktop Entry]
Type=Application
Name={self.target_name}
Exec={executable.target_name}
Comment={self.distribution.get_description()}
Icon=/{share_icons}/{os.path.splitext(icon_name)[0]}
Categories=Development;
Terminal=true
X-AppImage-Arch={ARCH}
X-AppImage-Name={self.target_name}
X-AppImage-Version={self.target_version or ''}
"""
self.save_as_file(
dedent(desktop_entry),
os.path.join(appdir, f"{self.target_name}.desktop"),
)
entrypoint = f"""\
#! /bin/bash
# If running from an extracted image, fix APPDIR
if [ -z "$APPIMAGE" ]; then
self="$(readlink -f -- $0)"
export APPDIR="${{self%/*}}"
fi
# Call the application entry point
"$APPDIR/{executable.target_name}" "$@"
"""
self.save_as_file(
dedent(entrypoint), os.path.join(appdir, "AppRun"), mode="x"
)
# Build an AppImage from an AppDir
os.environ["ARCH"] = ARCH
cmd = [self.appimagekit, "--no-appstream", appdir, output]
if find_library("fuse") is None: # libfuse.so.2 is not found
cmd.insert(1, "--appimage-extract-and-run")
with FileLock(self.appimagekit + ".lock"):
self.spawn(cmd, search_path=0)
if not os.path.exists(output):
msg = "Could not build AppImage"
raise ExecError(msg)
self.warnings()
def save_as_file(self, data, outfile, mode="r") -> tuple[str, int]:
"""Save an input data to a file respecting verbose, dry-run and force
flags.
"""
if not self.force and os.path.exists(outfile):
if self.verbose >= 1:
self.warn_delayed(f"not creating {outfile} (output exists)")
return (outfile, 0)
if self.verbose >= 1:
self.announce(f"creating {outfile}", INFO)
if self.dry_run:
return (outfile, 1)
if isinstance(data, str):
data = data.encode()
with open(outfile, "wb") as out:
out.write(data)
st_mode = stat.S_IRUSR
if "w" in mode:
st_mode = st_mode | stat.S_IWUSR
if "x" in mode:
st_mode = st_mode | stat.S_IXUSR
os.chmod(outfile, st_mode)
return (outfile, 1)
def warn_delayed(self, msg) -> None:
self._warnings.append(msg)
def warnings(self) -> None:
for msg in self._warnings:
self.announce(f"WARNING: {msg}", WARNING)

View File

@@ -0,0 +1,122 @@
"""Implements the 'bdist_deb' command (create DEB binary distributions).
This is a simple wrapper around 'alien' that converts a rpm to deb.
"""
from __future__ import annotations
import logging
import os
import shutil
import subprocess
from typing import ClassVar
from setuptools import Command
from cx_Freeze.command.bdist_rpm import bdist_rpm
from cx_Freeze.exception import ExecError, PlatformError
__all__ = ["bdist_deb"]
class bdist_deb(Command):
"""Create an DEB distribution."""
description = "create an DEB distribution"
user_options: ClassVar[list[tuple[str, str | None, str]]] = [
(
"bdist-base=",
None,
"base directory for creating built distributions",
),
(
"build-dir=",
"b",
"directory of built executables and dependent files",
),
("dist-dir=", "d", "directory to put final built distributions in"),
]
def initialize_options(self) -> None:
self.bdist_base = None
self.build_dir = None
self.dist_dir = None
def finalize_options(self) -> None:
if os.name != "posix":
msg = (
"don't know how to create DEB "
f"distributions on platform {os.name}"
)
raise PlatformError(msg)
if not shutil.which("alien"):
msg = "failed to find 'alien' for this platform."
raise PlatformError(msg)
if os.getuid() != 0 and not shutil.which("fakeroot"):
msg = "failed to find 'fakeroot' for this platform."
raise PlatformError(msg)
self.set_undefined_options("bdist", ("bdist_base", "bdist_base"))
self.set_undefined_options(
"bdist",
("bdist_base", "bdist_base"),
("dist_dir", "dist_dir"),
)
def run(self) -> None:
# make a binary RPM to convert
cmd_rpm = bdist_rpm(
self.distribution,
bdist_base=self.bdist_base,
dist_dir=self.dist_dir,
)
cmd_rpm.ensure_finalized()
if not self.dry_run:
cmd_rpm.run()
rpm_filename = None
for command, _, filename in self.distribution.dist_files:
if command == "bdist_rpm":
rpm_filename = os.path.basename(filename)
break
if rpm_filename is None:
msg = "could not build rpm"
raise ExecError(msg)
else:
rpm_filename = "filename.rpm"
# convert rpm to deb (by default in dist directory)
logging.info("building DEB")
cmd = ["alien", "--to-deb", rpm_filename]
if os.getuid() != 0:
cmd.insert(0, "fakeroot")
if self.dry_run:
self.spawn(cmd)
else:
logging.info(subprocess.list2cmdline(cmd))
process = subprocess.run(
cmd,
text=True,
capture_output=True,
check=False,
cwd=self.dist_dir,
)
if process.returncode != 0:
msg = process.stderr.splitlines()[0]
if msg.startswith(f"Unpacking of '{rpm_filename}' failed at"):
info = [
"\n\t\x08Please check if you have `cpio 2.13` on "
"Ubuntu 22.04.",
"\t\x08You can try to install a previous version:",
"\t\x08$ sudo apt-get install cpio=2.13+dfsg-7",
]
msg += "\n".join(info)
raise ExecError(msg)
output = process.stdout
logging.info(output)
filename = output.splitlines()[0].split()[0]
filename = os.path.join(self.dist_dir, filename)
if not os.path.exists(filename):
msg = "could not build deb"
raise ExecError(msg)
self.distribution.dist_files.append(("bdist_deb", "any", filename))

View File

@@ -0,0 +1,385 @@
"""Implements the 'bdist_dmg' command (create macOS dmg and/or app bundle)."""
from __future__ import annotations
import os
import shutil
from typing import ClassVar
from dmgbuild import build_dmg
from setuptools import Command
import cx_Freeze.icons
from cx_Freeze import Executable
from cx_Freeze.exception import OptionError
__all__ = ["bdist_dmg"]
class bdist_dmg(Command):
"""Create a Mac DMG disk image containing the Mac application bundle."""
description = (
"create a Mac DMG disk image containing the Mac application bundle"
)
user_options: ClassVar[list[tuple[str, str | None, str]]] = [
("volume-label=", None, "Volume label of the DMG disk image"),
(
"applications-shortcut=",
None,
"Boolean for whether to include "
"shortcut to Applications in the DMG disk image",
),
("silent", "s", "suppress all output except warnings"),
("format=", None, 'format of the disk image [default: "UDZO"]'),
(
"filesystem=",
None,
'filesystem of the disk image [default: "HFS+"]',
),
(
"size=",
None,
"If defined, specifies the size of the filesystem within the "
"image. If this is not defined, cx_Freeze (and then dmgbuild) "
"will attempt to determine a reasonable size for the image. "
"If you set this, you should set it large enough to hold the "
"files you intend to copy into the image. The syntax is the "
"same as for the -size argument to hdiutil, i.e. you can use "
"the suffixes `b`, `k`, `m`, `g`, `t`, `p` and `e` for bytes, "
"kilobytes, megabytes, gigabytes, terabytes, exabytes and "
"petabytes respectively.",
),
(
"background",
"b",
"A rgb color in the form #3344ff, svg named color like goldenrod, "
"a path to an image, or the words 'builtin-arrow' [default: None]",
),
(
"show-status-bar",
None,
"Show the status bar in the Finder window. Default is False.",
),
(
"show-tab-view",
None,
"Show the tab view in the Finder window. Default is False.",
),
(
"show-path-bar",
None,
"Show the path bar in the Finder window. Default is False.",
),
(
"show-sidebar",
None,
"Show the sidebar in the Finder window. Default is False.",
),
(
"sidebar-width",
None,
"Width of the sidebar in the Finder window. Default is None.",
),
(
"window-rect",
None,
"Window rectangle in the form x, y, width, height. The position "
"of the window in ((x, y), (w, h)) format, with y co-ordinates "
"running from bottom to top. The Finder makes sure that the "
"window will be on the user's display, so if you want your window "
"at the top left of the display you could use (0, 100000) as the "
"x, y co-ordinates. Unfortunately it doesn't appear to be "
"possible to position the window relative to the top left or "
"relative to the centre of the user's screen.",
),
(
"icon-locations",
None,
"A dictionary specifying the co-ordinates of items in the root "
"directory of the disk image, where the keys are filenames and "
"the values are (x, y) tuples. e.g.: "
'icon-locations = { "Applications": (100, 100), '
'"README.txt": (200, 100) }',
),
(
"default-view",
None,
"The default view of the Finder window. Possible values are "
'"icon-view", "list-view", "column-view", "coverflow".',
),
(
"show-icon-preview",
None,
"Show icon preview in the Finder window. Default is False.",
),
(
"license",
None,
"Dictionary specifying license details with 'default-language', "
"'licenses', and 'buttons'."
"default-language: Language code (e.g., 'en_US') if no matching "
"system language."
"licenses: Map of language codes to license file paths "
"(e.g., {'en_US': 'path/to/license_en.txt'})."
"buttons: Map of language codes to UI strings "
"([language, agree, disagree, print, save, instruction])."
"Example: {'default-language': 'en_US', "
"'licenses': {'en_US': 'path/to/license_en.txt'}, "
"'buttons': {'en_US': ['English', 'Agree', 'Disagree', 'Print', "
"'Save', 'Instruction text']}}",
),
]
def initialize_options(self) -> None:
self.silent = None
self.volume_label = self.distribution.get_fullname()
self.applications_shortcut = False
self._symlinks = {}
self._files = []
self.format = "UDZO"
self.filesystem = "HFS+"
self.size = None
self.background = None
self.show_status_bar = False
self.show_tab_view = False
self.show_path_bar = False
self.show_sidebar = False
self.sidebar_width = None
self.window_rect = None
self.hide = None
self.hide_extensions = None
self.icon_locations = None
self.default_view = None
self.show_icon_preview = False
self.license = None
# Non-exposed options
self.include_icon_view_settings = "auto"
self.include_list_view_settings = "auto"
self.arrange_by = None
self.grid_offset = None
self.grid_spacing = None
self.scroll_position = None
self.label_pos = None
self.text_size = None
self.icon_size = None
self.list_icon_size = None
self.list_text_size = None
self.list_scroll_position = None
self.list_sort_by = None
self.list_use_relative_dates = None
self.list_calculate_all_sizes = None
self.list_columns = None
self.list_column_widths = None
self.list_column_sort_directions = None
def finalize_options(self) -> None:
if not self.volume_label:
msg = "volume-label must be set"
raise OptionError(msg)
if self.applications_shortcut:
self._symlinks["Applications"] = "/Applications"
if self.silent is None:
self.silent = False
self.finalize_dmgbuild_options()
def finalize_dmgbuild_options(self) -> None:
if self.background:
self.background = self.background.strip()
if self.background == "builtin-arrow" and (
self.icon_locations or self.window_rect
):
msg = (
"background='builtin-arrow' cannot be used with "
"icon_locations or window_rect"
)
raise OptionError(msg)
if not self.arrange_by:
self.arrange_by = None
if not self.grid_offset:
self.grid_offset = (0, 0)
if not self.grid_spacing:
self.grid_spacing = 100
if not self.scroll_position:
self.scroll_position = (0, 0)
if not self.label_pos:
self.label_pos = "bottom"
if not self.text_size:
self.text_size = 16
if not self.icon_size:
self.icon_size = 128
def build_dmg(self) -> None:
# Remove DMG if it already exists
if os.path.exists(self.dmg_name):
os.unlink(self.dmg_name)
# Make dist folder
self.dist_dir = os.path.join(self.build_dir, "dist")
if os.path.exists(self.dist_dir):
shutil.rmtree(self.dist_dir)
self.mkpath(self.dist_dir)
# Copy App Bundle
dest_dir = os.path.join(
self.dist_dir, os.path.basename(self.bundle_dir)
)
if self.silent:
shutil.copytree(self.bundle_dir, dest_dir, symlinks=True)
else:
self.copy_tree(self.bundle_dir, dest_dir, preserve_symlinks=True)
# Add the App Bundle to the list of files
self._files.append(self.bundle_dir)
# set the app_name for the application bundle
app_name = os.path.basename(self.bundle_dir)
# Set the defaults
if (
self.background == "builtin-arrow"
and not self.icon_locations
and not self.window_rect
):
self.icon_locations = {
"Applications": (500, 120),
app_name: (140, 120),
}
self.window_rect = ((100, 100), (640, 380))
executables = self.distribution.executables # type: list[Executable]
executable: Executable = executables[0]
if len(executables) > 1:
self.warn(
"using the first executable as entrypoint: "
f"{executable.target_name}"
)
if executable.icon is None:
icon_name = "setup.icns"
icon_source_dir = os.path.dirname(cx_Freeze.icons.__file__)
self.icon = os.path.join(icon_source_dir, icon_name)
else:
self.icon = os.path.abspath(executable.icon)
with open("settings.py", "w") as f:
def add_param(name, value) -> None:
# if value is a string, add quotes
if isinstance(value, (str)):
f.write(f"{name} = '{value}'\n")
else:
f.write(f"{name} = {value}\n")
# Some fields expect and allow None, others don't
# so we need to check for None and not add them for
# the fields that don't allow it
# Disk Image Settings
add_param("filename", self.dmg_name)
add_param("volume_label", self.volume_label)
add_param("format", self.format)
add_param("filesystem", self.filesystem)
add_param("size", self.size)
# Content Settings
add_param("files", self._files)
add_param("symlinks", self._symlinks)
if self.hide:
add_param("hide", self.hide)
if self.hide_extensions:
add_param("hide_extensions", self.hide_extensions)
# Only one of these can be set
if self.icon_locations:
add_param("icon_locations", self.icon_locations)
if self.icon:
add_param("icon", self.icon)
# We don't need to set this, as we only support icns
# add param ( "badge_icon", self.badge_icon)
# Window Settings
add_param("background", self.background)
add_param("show_status_bar", self.show_status_bar)
add_param("show_tab_view", self.show_tab_view)
add_param("show_pathbar", self.show_path_bar)
add_param("show_sidebar", self.show_sidebar)
add_param("sidebar_width", self.sidebar_width)
if self.window_rect:
add_param("window_rect", self.window_rect)
if self.default_view:
add_param("default_view", self.default_view)
add_param("show_icon_preview", self.show_icon_preview)
add_param(
"include_icon_view_settings", self.include_icon_view_settings
)
add_param(
"include_list_view_settings", self.include_list_view_settings
)
# Icon View Settings\
add_param("arrange_by", self.arrange_by)
add_param("grid_offset", self.grid_offset)
add_param("grid_spacing", self.grid_spacing)
add_param("scroll_position", self.scroll_position)
add_param("label_pos", self.label_pos)
if self.text_size:
add_param("text_size", self.text_size)
if self.icon_size:
add_param("icon_size", self.icon_size)
if self.icon_locations:
add_param("icon_locations", self.icon_locations)
# List View Settings
if self.list_icon_size:
add_param("list_icon_size", self.list_icon_size)
if self.list_text_size:
add_param("list_text_size", self.list_text_size)
if self.list_scroll_position:
add_param("list_scroll_position", self.list_scroll_position)
add_param("list_sort_by", self.list_sort_by)
add_param("list_use_relative_dates", self.list_use_relative_dates)
add_param(
"list_calculate_all_sizes", self.list_calculate_all_sizes
)
if self.list_columns:
add_param("list_columns", self.list_columns)
if self.list_column_widths:
add_param("list_column_widths", self.list_column_widths)
if self.list_column_sort_directions:
add_param(
"list_column_sort_directions",
self.list_column_sort_directions,
)
# License Settings
add_param("license", self.license)
def log_handler(msg: dict[str, str]) -> None:
if not self.silent:
loggable = ",".join(
f"{key}: {value}" for key, value in msg.items()
)
self.announce(loggable)
build_dmg(
self.dmg_name,
self.volume_label,
"settings.py",
callback=log_handler,
)
def run(self) -> None:
# Create the application bundle
self.run_command("bdist_mac")
# Find the location of the application bundle and the build dir
self.bundle_dir = self.get_finalized_command("bdist_mac").bundle_dir
self.build_dir = self.get_finalized_command("build_exe").build_base
# Set the file name of the DMG to be built
self.dmg_name = os.path.join(
self.build_dir, self.volume_label + ".dmg"
)
self.execute(self.build_dmg, ())

View File

@@ -0,0 +1,529 @@
"""Implements the 'bdist_mac' commands (create macOS
app blundle).
"""
from __future__ import annotations
import os
import plistlib
import shutil
import subprocess
from pathlib import Path
from typing import ClassVar
from setuptools import Command
from cx_Freeze.common import normalize_to_list
from cx_Freeze.darwintools import (
apply_adhoc_signature,
change_load_reference,
isMachOFile,
)
from cx_Freeze.exception import OptionError
__all__ = ["bdist_mac"]
class bdist_mac(Command):
"""Create a Mac application bundle."""
description = "create a Mac application bundle"
plist_items: list[tuple[str, str]]
include_frameworks: list[str]
include_resources: list[str]
user_options: ClassVar[list[tuple[str, str | None, str]]] = [
("iconfile=", None, "Path to an icns icon file for the application."),
(
"qt-menu-nib=",
None,
"Location of qt_menu.nib folder for Qt "
"applications. Will be auto-detected by default.",
),
(
"bundle-name=",
None,
"File name for the bundle application "
"without the .app extension.",
),
(
"plist-items=",
None,
"A list of key-value pairs (type: list[tuple[str, str]]) to "
"be added to the app bundle Info.plist file.",
),
(
"custom-info-plist=",
None,
"File to be used as the Info.plist in "
"the app bundle. A basic one will be generated by default.",
),
(
"include-frameworks=",
None,
"A comma separated list of Framework "
"directories to include in the app bundle.",
),
(
"include-resources=",
None,
"A list of tuples of additional "
"files to include in the app bundle's resources directory, with "
"the first element being the source, and second the destination "
"file or directory name.",
),
(
"codesign-identity=",
None,
"The identity of the key to be used to sign the app bundle.",
),
(
"codesign-entitlements=",
None,
"The path to an entitlements file "
"to use for your application's code signature.",
),
(
"codesign-deep=",
None,
"Boolean for whether to codesign using the --deep option.",
),
(
"codesign-timestamp",
None,
"Boolean for whether to codesign using the --timestamp option.",
),
(
"codesign-resource-rules",
None,
"Plist file to be passed to "
"codesign's --resource-rules option.",
),
(
"absolute-reference-path=",
None,
"Path to use for all referenced "
"libraries instead of @executable_path.",
),
(
"codesign-verify",
None,
"Boolean to verify codesign of the .app bundle using the codesign "
"command",
),
(
"spctl-assess",
None,
"Boolean to verify codesign of the .app bundle using the spctl "
"command",
),
(
"codesign-strict=",
None,
"Boolean for whether to codesign using the --strict option.",
),
(
"codesign-options=",
None,
"Option flags to be embedded in the code signature",
),
]
def initialize_options(self) -> None:
self.list_options = [
"plist_items",
"include_frameworks",
"include_resources",
]
for option in self.list_options:
setattr(self, option, [])
self.absolute_reference_path = None
self.bundle_name = self.distribution.get_fullname()
self.codesign_deep = None
self.codesign_entitlements = None
self.codesign_identity = None
self.codesign_timestamp = None
self.codesign_strict = None
self.codesign_options = None
self.codesign_resource_rules = None
self.codesign_verify = None
self.spctl_assess = None
self.custom_info_plist = None
self.iconfile = None
self.qt_menu_nib = False
self.build_base = None
self.build_dir = None
def finalize_options(self) -> None:
# Make sure all options of multiple values are lists
for option in self.list_options:
setattr(self, option, normalize_to_list(getattr(self, option)))
for item in self.plist_items:
if not isinstance(item, tuple) or len(item) != 2:
msg = (
"Error, plist_items must be a list of key, value pairs "
"(list[tuple[str, str]]) (bad list item)."
)
raise OptionError(msg)
# Define the paths within the application bundle
self.set_undefined_options(
"build_exe",
("build_base", "build_base"),
("build_exe", "build_dir"),
)
self.bundle_dir = os.path.join(
self.build_base, f"{self.bundle_name}.app"
)
self.contents_dir = os.path.join(self.bundle_dir, "Contents")
self.bin_dir = os.path.join(self.contents_dir, "MacOS")
self.frameworks_dir = os.path.join(self.contents_dir, "Frameworks")
self.resources_dir = os.path.join(self.contents_dir, "Resources")
self.helpers_dir = os.path.join(self.contents_dir, "Helpers")
def create_plist(self) -> None:
"""Create the Contents/Info.plist file."""
# Use custom plist if supplied, otherwise create a simple default.
if self.custom_info_plist:
with open(self.custom_info_plist, "rb") as file:
contents = plistlib.load(file)
else:
contents = {
"CFBundleIconFile": "icon.icns",
"CFBundleDevelopmentRegion": "English",
"CFBundleIdentifier": self.bundle_name,
# Specify that bundle is an application bundle
"CFBundlePackageType": "APPL",
# Cause application to run in high-resolution mode by default
# (Without this, applications run from application bundle may
# be pixelated)
"NSHighResolutionCapable": "True",
}
# Ensure CFBundleExecutable is set correctly
contents["CFBundleExecutable"] = self.bundle_executable
# add custom items to the plist file
for key, value in self.plist_items:
contents[key] = value
with open(os.path.join(self.contents_dir, "Info.plist"), "wb") as file:
plistlib.dump(contents, file)
def set_absolute_reference_paths(self, path=None) -> None:
"""For all files in Contents/MacOS, set their linked library paths to
be absolute paths using the given path instead of @executable_path.
"""
if not path:
path = self.absolute_reference_path
files = os.listdir(self.bin_dir)
for filename in files:
filepath = os.path.join(self.bin_dir, filename)
# Skip some file types
if filepath[-1:] in ("txt", "zip") or os.path.isdir(filepath):
continue
out = subprocess.check_output(
("otool", "-L", filepath), encoding="utf_8"
)
for line in out.splitlines()[1:]:
lib = line.lstrip("\t").split(" (compat")[0]
if lib.startswith("@executable_path"):
replacement = lib.replace("@executable_path", path)
path, name = os.path.split(replacement)
# see if we provide the referenced file;
# if so, change the reference
if name in files:
change_load_reference(filepath, lib, replacement)
apply_adhoc_signature(filepath)
def find_qt_menu_nib(self) -> str | None:
"""Returns a location of a qt_menu.nib folder, or None if this is not
a Qt application.
"""
if self.qt_menu_nib:
return self.qt_menu_nib
if any(n.startswith("PyQt4.QtCore") for n in os.listdir(self.bin_dir)):
name = "PyQt4"
elif any(
n.startswith("PySide.QtCore") for n in os.listdir(self.bin_dir)
):
name = "PySide"
else:
return None
qtcore = __import__(name, fromlist=["QtCore"]).QtCore
libpath = str(
qtcore.QLibraryInfo.location(qtcore.QLibraryInfo.LibrariesPath)
)
for subpath in [
"QtGui.framework/Resources/qt_menu.nib",
"Resources/qt_menu.nib",
]:
path = os.path.join(libpath, subpath)
if os.path.exists(path):
return path
# Last resort: fixed paths (macports)
for path in [
"/opt/local/Library/Frameworks/QtGui.framework/Versions/"
"4/Resources/qt_menu.nib"
]:
if os.path.exists(path):
return path
print("Could not find qt_menu.nib")
msg = "Could not find qt_menu.nib"
raise OSError(msg)
def prepare_qt_app(self) -> None:
"""Add resource files for a Qt application. Should do nothing if the
application does not use QtCore.
"""
qt_conf = os.path.join(self.resources_dir, "qt.conf")
qt_conf_2 = os.path.join(self.resources_dir, "qt_bdist_mac.conf")
if os.path.exists(qt_conf_2):
self.execute(
shutil.move,
(qt_conf_2, qt_conf),
msg=f"moving {qt_conf_2} -> {qt_conf}",
)
nib_locn = self.find_qt_menu_nib()
if nib_locn is None:
return
# Copy qt_menu.nib
self.copy_tree(
nib_locn, os.path.join(self.resources_dir, "qt_menu.nib")
)
# qt.conf needs to exist, but needn't have any content
if not os.path.exists(qt_conf):
with open(qt_conf, "wb"):
pass
def run(self) -> None:
self.run_command("build_exe")
# Remove App if it already exists
# ( avoids confusing issues where prior builds persist! )
if os.path.exists(self.bundle_dir):
self.execute(
shutil.rmtree,
(self.bundle_dir,),
msg=f"staging - removed existing '{self.bundle_dir}'",
)
# Find the executable name
executable = self.distribution.executables[0].target_name
_, self.bundle_executable = os.path.split(executable)
print(f"Executable name: {self.build_dir}/{executable}")
# Build the app directory structure
self.mkpath(self.bin_dir) # /MacOS
self.mkpath(self.frameworks_dir) # /Frameworks
self.mkpath(self.resources_dir) # /Resources
# Copy the full build_exe to Contents/Resources
self.copy_tree(self.build_dir, self.resources_dir)
# Move only executables in Contents/Resources to Contents/MacOS
for executable in self.distribution.executables:
source = os.path.join(self.resources_dir, executable.target_name)
target = os.path.join(self.bin_dir, executable.target_name)
self.move_file(source, target)
# Make symlink between folders under Resources such as lib and others
# specified by the user in include_files and Contents/MacOS so we can
# use non-relative reference paths to pass codesign...
for filename in os.listdir(self.resources_dir):
target = os.path.join(self.resources_dir, filename)
if os.path.isdir(target):
origin = os.path.join(self.bin_dir, filename)
relative_reference = os.path.relpath(target, self.bin_dir)
self.execute(
os.symlink,
(relative_reference, origin, True),
msg=f"linking {origin} -> {relative_reference}",
)
# Copy the icon
if self.iconfile:
self.copy_file(
self.iconfile, os.path.join(self.resources_dir, "icon.icns")
)
# Copy in Frameworks
for framework in self.include_frameworks:
self.copy_tree(
framework,
os.path.join(self.frameworks_dir, os.path.basename(framework)),
)
# Copy in Resources
for resource, destination in self.include_resources:
if os.path.isdir(resource):
self.copy_tree(
resource, os.path.join(self.resources_dir, destination)
)
else:
parent_dirs = os.path.dirname(
os.path.join(self.resources_dir, destination)
)
os.makedirs(parent_dirs, exist_ok=True)
self.copy_file(
resource, os.path.join(self.resources_dir, destination)
)
# Create the Info.plist file
self.execute(self.create_plist, (), msg="creating Contents/Info.plist")
# Make library references absolute if enabled
if self.absolute_reference_path:
self.execute(
self.set_absolute_reference_paths,
(),
msg="set absolute reference path "
f"'{self.absolute_reference_path}",
)
# For a Qt application, run some tweaks
self.execute(self.prepare_qt_app, ())
# Move Contents/Resources/share/*.app to Contents/Helpers
share_dir = os.path.join(self.resources_dir, "share")
if os.path.isdir(share_dir):
for filename in os.listdir(share_dir):
if not filename.endswith(".app"):
continue
# create /Helpers only if required
self.mkpath(self.helpers_dir)
source = os.path.join(share_dir, filename)
target = os.path.join(self.helpers_dir, filename)
self.execute(
shutil.move,
(source, target),
msg=f"moving {source} -> {target}",
)
if os.path.isdir(target):
origin = os.path.join(target, "Contents", "MacOS", "lib")
relative_reference = os.path.relpath(
os.path.join(self.resources_dir, "lib"),
os.path.join(target, "Contents", "MacOS"),
)
self.execute(
os.symlink,
(relative_reference, origin, True),
msg=f"linking {origin} -> {relative_reference}",
)
# Sign the app bundle if a key is specified
self.execute(
self._codesign,
(self.bundle_dir,),
msg=f"sign: '{self.bundle_dir}'",
)
def _codesign(self, root_path) -> None:
"""Run codesign on all .so, .dylib and binary files in reverse order.
Signing from inside-out.
"""
if not self.codesign_identity:
return
binaries_to_sign = []
# Identify all binary files
for dirpath, _, filenames in os.walk(root_path):
for filename in filenames:
full_path = Path(os.path.join(dirpath, filename))
if isMachOFile(full_path):
binaries_to_sign.append(full_path)
# Sort files by depth, so we sign the deepest files first
binaries_to_sign.sort(key=lambda x: str(x).count(os.sep), reverse=True)
for binary_path in binaries_to_sign:
self._codesign_file(binary_path, self._get_sign_args())
self._verify_signature()
print("Finished .app signing")
def _get_sign_args(self) -> list[str]:
signargs = ["codesign", "--sign", self.codesign_identity, "--force"]
if self.codesign_timestamp:
signargs.append("--timestamp")
if self.codesign_strict:
signargs.append(f"--strict={self.codesign_strict}")
if self.codesign_deep:
signargs.append("--deep")
if self.codesign_options:
signargs.append("--options")
signargs.append(self.codesign_options)
if self.codesign_entitlements:
signargs.append("--entitlements")
signargs.append(self.codesign_entitlements)
return signargs
def _codesign_file(self, file_path, sign_args) -> None:
print(f"Signing file: {file_path}")
sign_args.append(file_path)
subprocess.run(sign_args, check=False)
def _verify_signature(self) -> None:
if self.codesign_verify:
verify_args = [
"codesign",
"-vvv",
"--deep",
"--strict",
self.bundle_dir,
]
print("Running codesign verification")
result = subprocess.run(
verify_args, capture_output=True, text=True, check=False
)
print("ExitCode:", result.returncode)
print(" stdout:", result.stdout)
print(" stderr:", result.stderr)
if self.spctl_assess:
spctl_args = [
"spctl",
"--assess",
"--raw",
"--verbose=10",
"--type",
"exec",
self.bundle_dir,
]
try:
completed_process = subprocess.run(
spctl_args,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
print(
"spctl command's output: "
f"{completed_process.stdout.decode()}"
)
except subprocess.CalledProcessError as error:
print(f"spctl check got an error: {error.stdout.decode()}")
raise

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,562 @@
"""Implements the 'bdist_rpm' command (create RPM binary distributions).
Borrowed from distutils.command.bdist_rpm of Python 3.10 and merged with
bdist_rpm subclass of cx_Freeze 6.10.
https://rpm.org/documentation.html
https://rpm-packaging-guide.github.io/
"""
from __future__ import annotations
import logging
import os
import platform
import shutil
import sys
import tarfile
from subprocess import CalledProcessError, check_output
from sysconfig import get_python_version
from typing import ClassVar
from setuptools import Command
from cx_Freeze._compat import IS_CONDA
from cx_Freeze.exception import ExecError, FileError, PlatformError
__all__ = ["bdist_rpm"]
class bdist_rpm(Command):
"""Create an RPM distribution."""
description = "create an RPM distribution"
user_options: ClassVar[list[tuple[str, str | None, str]]] = [
(
"bdist-base=",
None,
"base directory for creating built distributions",
),
(
"rpm-base=",
None,
"base directory for creating RPMs "
'[defaults to "rpm" under "--bdist-base"]',
),
(
"dist-dir=",
"d",
"directory to put final RPM files in "
"(and .spec files if --spec-only)",
),
("spec-only", None, "only regenerate spec file"),
# More meta-data: too RPM-specific to put in the setup script,
# but needs to go in the .spec file -- so we make these options
# to "bdist_rpm". The idea is that packagers would put this
# info in pyproject.toml or setup.cfg, although they are of course free
# to supply it on the command line.
(
"distribution-name=",
None,
"name of the (Linux) distribution to which this "
"RPM applies (*not* the name of the module distribution!)",
),
(
"group=",
None,
'package classification [default: "Development/Libraries"]',
),
("release=", None, "RPM release number"),
("serial=", None, "RPM serial number"),
(
"vendor=",
None,
'RPM "vendor" (eg. "Joe Blow <joe@example.com>") '
"[default: maintainer or author from setup script]",
),
(
"packager=",
None,
'RPM packager (eg. "Jane Doe <jane@example.net>") '
"[default: same as vendor]",
),
(
"doc-files=",
None,
"list of documentation files (space or comma-separated)",
),
("changelog=", None, "RPM changelog"),
("icon=", None, "name of icon file"),
("provides=", None, "capabilities provided by this package"),
("requires=", None, "capabilities required by this package"),
("conflicts=", None, "capabilities which conflict with this package"),
(
"build-requires=",
None,
"capabilities required to build this package",
),
("obsoletes=", None, "capabilities made obsolete by this package"),
("no-autoreq", None, "do not automatically calculate dependencies"),
# Actions to take when building RPM
("keep-temp", "k", "don't clean up RPM build directory"),
("no-keep-temp", None, "clean up RPM build directory [default]"),
# Add the hooks necessary for specifying custom scripts
(
"prep-script=",
None,
"Specify a script for the PREP phase of RPM building",
),
(
"build-script=",
None,
"Specify a script for the BUILD phase of RPM building",
),
(
"pre-install=",
None,
"Specify a script for the pre-INSTALL phase of RPM building",
),
(
"install-script=",
None,
"Specify a script for the INSTALL phase of RPM building",
),
(
"post-install=",
None,
"Specify a script for the post-INSTALL phase of RPM building",
),
(
"pre-uninstall=",
None,
"Specify a script for the pre-UNINSTALL phase of RPM building",
),
(
"post-uninstall=",
None,
"Specify a script for the post-UNINSTALL phase of RPM building",
),
(
"clean-script=",
None,
"Specify a script for the CLEAN phase of RPM building",
),
(
"verify-script=",
None,
"Specify a script for the VERIFY phase of the RPM build",
),
("quiet", "q", "Run the INSTALL phase of RPM building in quiet mode"),
("debug", "g", "Run in debug mode"),
]
boolean_options: ClassVar[list[str]] = [
"keep-temp",
"no-autoreq",
"quiet",
"debug",
]
negative_opt: ClassVar[dict[str, str]] = {
"no-keep-temp": "keep-temp",
}
def initialize_options(self) -> None:
self.bdist_base = None
self.dist_dir = None
self.rpm_base = None
self.spec_only = None
self.distribution_name = None
self.group = None
self.release = None
self.serial = None
self.vendor = None
self.packager = None
self.doc_files = None
self.changelog = None
self.icon = None
self.prep_script = None
self.build_script = None
self.install_script = None
self.clean_script = None
self.verify_script = None
self.pre_install = None
self.post_install = None
self.pre_uninstall = None
self.post_uninstall = None
self.prep = None
self.provides = None
self.requires = None
self.conflicts = None
self.build_requires = None
self.obsoletes = None
self.keep_temp = 0
self.no_autoreq = 0
self.quiet = 0
self.debug = 0
def finalize_options(self) -> None:
if os.name != "posix":
msg = (
"don't know how to create RPM "
f"distributions on platform {os.name}"
)
raise PlatformError(msg)
self._rpm = shutil.which("rpm")
self._rpmbuild = shutil.which("rpmbuild")
if not self._rpmbuild:
msg = "failed to find rpmbuild for this platform."
raise PlatformError(msg)
self.set_undefined_options(
"bdist",
("bdist_base", "bdist_base"),
("dist_dir", "dist_dir"),
)
if self.rpm_base is None:
self.rpm_base = os.path.join(self.bdist_base, "rpm")
self.finalize_package_data()
def finalize_package_data(self) -> None:
self.ensure_string("group", "Development/Libraries")
contact = self.distribution.get_contact() or "UNKNOWN"
contact_email = self.distribution.get_contact_email() or "UNKNOWN"
self.ensure_string("vendor", f"{contact} <{contact_email}>")
self.ensure_string("packager")
self.ensure_string_list("doc_files")
if isinstance(self.doc_files, list):
doc_files = set(self.doc_files)
for readme in ("README", "README.txt"):
if os.path.exists(readme) and readme not in doc_files:
self.doc_files.append(readme)
self.ensure_string("release", "1")
self.ensure_string("serial") # should it be an int?
self.ensure_string("distribution_name")
self.ensure_string("changelog")
# Format changelog correctly
self.changelog = self._format_changelog(self.changelog)
self.ensure_filename("icon")
self.ensure_filename("prep_script")
self.ensure_filename("build_script")
self.ensure_filename("install_script")
self.ensure_filename("clean_script")
self.ensure_filename("verify_script")
self.ensure_filename("pre_install")
self.ensure_filename("post_install")
self.ensure_filename("pre_uninstall")
self.ensure_filename("post_uninstall")
# Now *this* is some meta-data that belongs in the setup script...
self.ensure_string_list("provides")
self.ensure_string_list("requires")
self.ensure_string_list("conflicts")
self.ensure_string_list("build_requires")
self.ensure_string_list("obsoletes")
def run(self) -> None:
if self.debug:
print("before _get_package_data():")
print("vendor =", self.vendor)
print("packager =", self.packager)
print("doc_files =", self.doc_files)
print("changelog =", self.changelog)
# make directories
if self.spec_only:
spec_dir = self.dist_dir
else:
rpm_dir = {}
for data in ("SOURCES", "SPECS", "BUILD", "RPMS", "SRPMS"):
rpm_dir[data] = os.path.join(self.rpm_base, data)
self.mkpath(rpm_dir[data])
spec_dir = rpm_dir["SPECS"]
self.mkpath(self.dist_dir)
# Spec file goes into 'dist_dir' if '--spec-only specified',
# build/rpm.<plat> otherwise.
distribution_name = self.distribution.get_name()
spec_path = os.path.join(spec_dir, f"{distribution_name}.spec")
self.execute(
write_file,
(spec_path, self._make_spec_file()),
f"writing '{spec_path}'",
)
if self.spec_only: # stop if requested
return
# Make a source distribution and copy to SOURCES directory with
# optional icon.
def exclude_filter(info: tarfile.TarInfo) -> tarfile.TarInfo | None:
if (
os.path.basename(info.name) in ("build", "dist")
and info.isdir()
):
return None
return info
name = self.distribution.get_name()
version = self.distribution.get_version()
source = f"{name}-{version}"
source_dir = rpm_dir["SOURCES"]
source_fullname = os.path.join(source_dir, source + ".tar.gz")
with tarfile.open(source_fullname, "w:gz") as tar:
tar.add(".", source, filter=exclude_filter)
if self.icon:
if os.path.exists(self.icon):
self.copy_file(self.icon, source_dir)
else:
msg = f"icon file {self.icon!r} does not exist"
raise FileError(msg)
# build package, binary only (-bb)
logging.info("building RPMs")
rpm_cmd = [self._rpmbuild, "-bb"]
if not self.keep_temp:
rpm_cmd.append("--clean")
if self.quiet:
rpm_cmd.append("--quiet")
rpm_cmd.append(spec_path)
# Determine the binary rpm names that should be built out of this spec
# file
# Note that some of these may not be really built (if the file
# list is empty)
nvr_string = "%{name}-%{version}-%{release}"
src_rpm = nvr_string + ".src.rpm"
non_src_rpm = "%{arch}/" + nvr_string + ".%{arch}.rpm"
q_cmd = [
self._rpm,
"-q",
"--qf",
rf"{src_rpm} {non_src_rpm}\n",
"--specfile",
spec_path,
]
try:
out = check_output(q_cmd, text=True)
except CalledProcessError as exc:
msg = f"Failed to execute: {' '.join(q_cmd)!r}"
raise ExecError(msg) from exc
binary_rpms = []
for line in out.splitlines():
rows = line.split()
assert len(rows) == 2 # noqa: S101
binary_rpms.append(rows[1])
self.spawn(rpm_cmd)
if not self.dry_run:
pyversion = get_python_version()
for binary_rpm in binary_rpms:
rpm = os.path.join(rpm_dir["RPMS"], binary_rpm)
if os.path.exists(rpm):
self.move_file(rpm, self.dist_dir)
filename = os.path.join(
self.dist_dir, os.path.basename(rpm)
)
self.distribution.dist_files.append(
("bdist_rpm", pyversion, filename)
)
def _make_spec_file(self) -> list[str]:
"""Generate the text of an RPM spec file and return it as a
list of strings (one per line).
"""
# definitions and headers
dist = self.distribution
spec_file = [
f"%define _topdir {os.path.abspath(self.rpm_base)}",
# cx_Freeze specific
"%define __prelink_undo_cmd %{nil}",
"%define __strip /bin/true",
"",
f"%define name {dist.get_name()}",
f"%define version {dist.get_version().replace('-', '_')}",
f"%define unmangled_version {dist.get_version()}",
f"%define release {self.release.replace('-', '_')}",
"",
f"Summary: {dist.get_description() or 'UNKNOWN'}",
"Name: %{name}",
"Version: %{version}",
"Release: %{release}",
f"License: {dist.get_license() or 'UNKNOWN'}",
f"Group: {self.group}",
"BuildRoot: %{buildroot}",
"Prefix: %{_prefix}",
f"BuildArch: {platform.machine()}",
]
# Fix for conda
if IS_CONDA:
spec_file.append("%define debug_package %{nil}")
# Workaround for #14443 which affects some RPM based systems such as
# RHEL6 (and probably derivatives)
vendor_hook = check_output(
[self._rpm, "--eval", "%{__os_install_post}"], text=True
)
# Generate a potential replacement value for __os_install_post (whilst
# normalizing the whitespace to simplify the test for whether the
# invocation of brp-python-bytecompile passes in __python):
vendor_hook = "\n".join(
[f" {line.strip()} \\" for line in vendor_hook.splitlines()]
)
problem = "brp-python-bytecompile \\\n"
fixed = "brp-python-bytecompile %{__python} \\\n"
fixed_hook = vendor_hook.replace(problem, fixed)
if fixed_hook != vendor_hook:
spec_file += [
"# Workaround for http://bugs.python.org/issue14443",
f"%define __python {sys.executable}",
f"%define __os_install_post {fixed_hook}",
"",
]
# we create the spec file before running 'tar' in case of --spec-only.
spec_file.append("Source0: %{name}-%{unmangled_version}.tar.gz")
for field in (
"Vendor",
"Packager",
"Provides",
"Requires",
"Conflicts",
"Obsoletes",
):
val = getattr(self, field.lower())
if isinstance(val, list):
join_val = " ".join(val)
spec_file.append(f"{field}: {join_val}")
elif val is not None:
spec_file.append(f"{field}: {val}")
if dist.get_url() not in (None, "UNKNOWN"):
spec_file.append(f"Url: {dist.get_url()}")
if self.distribution_name:
spec_file.append(f"Distribution: {self.distribution_name}")
if self.build_requires:
spec_file.append("BuildRequires: " + " ".join(self.build_requires))
if self.icon:
spec_file.append("Icon: " + os.path.basename(self.icon))
if self.no_autoreq:
spec_file.append("AutoReq: 0")
spec_file += [
"",
"%description",
dist.get_long_description() or dist.get_description() or "UNKNOWN",
]
# rpm scripts - figure out default build script
if dist.script_name == "cxfreeze":
def_setup_call = shutil.which(dist.script_name)
else:
def_setup_call = f"{sys.executable} {dist.script_name}"
def_build = f"{def_setup_call} build_exe --optimize=1 --silent"
def_build = 'env CFLAGS="$RPM_OPT_FLAGS" ' + def_build
# insert contents of files
# this is kind of misleading: user-supplied options are files
# that we open and interpolate into the spec file, but the defaults
# are just text that we drop in as-is.
install_cmd = (
f"{def_setup_call} install --skip-build"
" --prefix=%{_prefix} --root=%{buildroot}"
)
script_options = [
("prep", "prep_script", "%setup -n %{name}-%{unmangled_version}"),
("build", "build_script", def_build),
("install", "install_script", install_cmd),
("clean", "clean_script", "rm -rf %{buildroot}"),
("verifyscript", "verify_script", None),
("pre", "pre_install", None),
("post", "post_install", None),
("preun", "pre_uninstall", None),
("postun", "post_uninstall", None),
]
for rpm_opt, attr, default in script_options:
# Insert contents of file referred to, if no file is referred to
# use 'default' as contents of script
val = getattr(self, attr)
if val or default:
spec_file.extend(["", "%" + rpm_opt])
if val:
with open(val, encoding="utf_8") as file:
spec_file.extend(file.read().split("\n"))
else:
spec_file.append(default)
# files section
spec_file += [
"",
"%files",
"%dir %{_prefix}/lib/%{name}-%{unmangled_version}",
"%{_prefix}/lib/%{name}-%{unmangled_version}/*",
"%{_bindir}/%{name}",
"%defattr(-,root,root)",
]
if self.doc_files:
spec_file.append("%doc " + " ".join(self.doc_files))
if self.changelog:
spec_file.extend(["", "%changelog"])
spec_file.extend(self.changelog)
return spec_file
@staticmethod
def _format_changelog(changelog) -> list[str]:
"""Format the changelog correctly and convert it to a string list."""
if not changelog:
return changelog
new_changelog = []
for raw_line in changelog.strip().split("\n"):
line = raw_line.strip()
if line[0] == "*":
new_changelog.extend(["", line])
elif line[0] == "-":
new_changelog.append(line)
else:
new_changelog.append(" " + line)
# strip trailing newline inserted by first changelog entry
if not new_changelog[0]:
del new_changelog[0]
return new_changelog
def write_file(filename, contents) -> None:
"""Create a file with the specified name and write 'contents'
(a sequence of strings without line terminators) to it.
"""
with open(filename, "w", encoding="utf_8") as file:
for line in contents:
file.write(line + "\n")

View File

@@ -0,0 +1,321 @@
"""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 <search>=<replace>",
),
(
"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

View File

@@ -0,0 +1,78 @@
"""Extends setuptools 'install' command."""
from __future__ import annotations
import contextlib
import os
import sys
import warnings
from typing import ClassVar, ContextManager
from setuptools.command.install import install as _install
__all__ = ["Install"]
@contextlib.contextmanager
def suppress_known_deprecation() -> ContextManager:
with warnings.catch_warnings():
warnings.filterwarnings("ignore", "setup.py install is deprecated")
yield
class Install(_install):
"""Install everything from build directory."""
command_name = "install"
user_options: ClassVar[list[tuple[str, str | None, str]]] = [
*_install.user_options,
("install-exe=", None, "installation directory for executables"),
]
def expand_dirs(self) -> None:
super().expand_dirs()
self._expand_attrs(["install_exe"])
def get_sub_commands(self) -> list[str]:
sub_commands = super().get_sub_commands()[:]
if self.distribution.executables:
sub_commands.remove("install_egg_info")
sub_commands.remove("install_scripts")
sub_commands.append("install_exe")
return sub_commands
def initialize_options(self) -> None:
with suppress_known_deprecation():
super().initialize_options()
self.install_exe = None
def finalize_options(self) -> None:
if self.prefix is None and sys.platform == "win32":
winreg = __import__("winreg")
key = winreg.OpenKey(
winreg.HKEY_LOCAL_MACHINE,
r"Software\Microsoft\Windows\CurrentVersion",
)
base = winreg.QueryValueEx(key, "ProgramFilesDir")[0]
metadata = self.distribution.metadata
self.prefix = os.path.join(
os.path.normpath(base), metadata.get_name()
)
super().finalize_options()
self.convert_paths("exe")
if self.root is not None:
self.change_roots("exe")
def select_scheme(self, name) -> None:
super().select_scheme(name)
if self.install_exe is None:
if sys.platform == "win32":
self.install_exe = "$base"
else:
metadata = self.distribution.metadata
dir_name = f"{metadata.get_name()}-{metadata.get_version()}"
self.install_exe = f"$base/lib/{dir_name}"
def run(self) -> None:
# setuptools used inspect.currentframe(), this method needs to exist.
super().run()

View File

@@ -0,0 +1,76 @@
"""Implements the 'install_exe' command."""
from __future__ import annotations
import os
import shutil
import sys
from typing import ClassVar
from setuptools import Command
__all__ = ["install_exe"]
class install_exe(Command):
"""Install executables built from Python scripts."""
command_name = "install_exe"
description = "install executables built from Python scripts"
user_options: ClassVar[list[tuple[str, str | None, str]]] = [
("install-dir=", "d", "directory to install executables to"),
("build-dir=", "b", "build directory (where to install from)"),
("force", "f", "force installation (overwrite existing files)"),
("skip-build", None, "skip the build steps"),
]
def initialize_options(self) -> None:
self.install_dir: str | None = None
self.force = 0
self.build_dir = None
self.skip_build = None
self.outfiles = None
def finalize_options(self) -> None:
self.set_undefined_options("build_exe", ("build_exe", "build_dir"))
self.set_undefined_options(
"install",
("install_exe", "install_dir"),
("force", "force"),
("skip_build", "skip_build"),
)
def run(self) -> None:
if not self.skip_build:
self.run_command("build_exe")
self.mkpath(self.install_dir)
self.outfiles = self.copy_tree(self.build_dir, self.install_dir)
if sys.platform == "win32":
return
# in posix, make symlinks to the executables
install_dir = self.install_dir
bin_dir = os.path.join(
os.path.dirname(os.path.dirname(install_dir)), "bin"
)
self.execute(shutil.rmtree, (bin_dir, True), msg=f"removing {bin_dir}")
self.mkpath(bin_dir)
for executable in self.get_inputs():
name = executable.target_name
target = os.path.join(install_dir, name)
origin = os.path.join(bin_dir, name)
relative_reference = os.path.relpath(target, bin_dir)
self.execute(
os.symlink,
(relative_reference, origin, True),
msg=f"linking {origin} -> {relative_reference}",
)
self.outfiles.append(origin)
def get_inputs(self) -> list[str]:
return self.distribution.executables or []
def get_outputs(self) -> list[str]:
return self.outfiles or []