Files
nuitka-mirror/nuitka/PostProcessing.py
Kay Hayen 3cd6d891d3 Quality: Update to latest black
* Only change is formatting of docstrings has different ideas
  about how whitespace is to be assigned.
2025-12-19 10:25:44 +01:00

606 lines
19 KiB
Python

# Copyright 2025, Kay Hayen, mailto:kay.hayen@gmail.com find license text at end of file
"""Postprocessing tasks for create binaries or modules."""
import ctypes
import os
import sys
from nuitka.build.DataComposerInterface import getConstantBlobFilename
from nuitka.ModuleRegistry import getImportedModuleNames
from nuitka.Options import (
getDebuggerName,
getFileVersionTuple,
getProductVersionTuple,
getWindowsIconExecutablePath,
getWindowsIconPaths,
getWindowsSplashScreen,
getWindowsVersionInfoStrings,
isOnefileMode,
isStandaloneMode,
shallAskForWindowsAdminRights,
shallAskForWindowsUIAccessRights,
shallCreateAppBundle,
shallCreatePyiFile,
shallCreatePyiFileContainStubs,
shallCreateScriptFileForExecution,
shallMakeModule,
shallRunInDebugger,
shallUseStaticLibPython,
)
from nuitka.OutputDirectories import (
getMainModule,
getResultBasePath,
getResultFullpath,
getResultRunFilename,
getSourceDirectoryPath,
)
from nuitka.PythonFlavors import isSelfCompiledPythonUninstalled
from nuitka.PythonVersions import getTargetPythonDLLPath, python_version
from nuitka.States import states
from nuitka.Tracing import postprocessing_logger
from nuitka.utils.Execution import wrapCommandForDebuggerForSubprocess
from nuitka.utils.FileOperations import (
addFileExecutablePermission,
getFileContents,
getFileSize,
hasFilenameExtension,
makeFilesystemEncodable,
makePath,
putTextFileContents,
removeFileExecutablePermission,
)
from nuitka.utils.Images import convertImageToIconFormat
from nuitka.utils.Importing import importFromInlineCopy
from nuitka.utils.MacOSApp import createPlistInfoFile
from nuitka.utils.SharedLibraries import (
callInstallNameTool,
cleanupHeaderForAndroid,
getOtoolDependencyOutput,
parseOtoolListingOutput,
)
from nuitka.utils.Utils import (
isAIX,
isAndroidBasedLinux,
isMacOS,
isWin32Windows,
)
from nuitka.utils.WindowsResources import (
RT_GROUP_ICON,
RT_ICON,
RT_RCDATA,
addResourceToFile,
addVersionInfoResource,
convertStructureToBytes,
copyResourcesFromFileToFile,
getDefaultWindowsExecutableManifest,
getWindowsExecutableManifest,
)
class IconDirectoryHeader(ctypes.Structure):
_fields_ = [
("reserved", ctypes.c_short),
("type", ctypes.c_short),
("count", ctypes.c_short),
]
class IconDirectoryEntry(ctypes.Structure):
_fields_ = [
("width", ctypes.c_char),
("height", ctypes.c_char),
("colors", ctypes.c_char),
("reserved", ctypes.c_char),
("planes", ctypes.c_short),
("bit_count", ctypes.c_short),
("image_size", ctypes.c_int),
("image_offset", ctypes.c_int),
]
class IconGroupDirectoryEntry(ctypes.Structure):
# Make sure the don't have padding issues.
_pack_ = 2
_fields_ = (
("width", ctypes.c_char),
("height", ctypes.c_char),
("colors", ctypes.c_char),
("reserved", ctypes.c_char),
("planes", ctypes.c_short),
("bit_count", ctypes.c_short),
("image_size", ctypes.c_int),
("id", ctypes.c_short),
)
def readFromFile(readable, c_struct):
"""Read ctypes structures from input."""
result = c_struct()
chunk = readable.read(ctypes.sizeof(result))
ctypes.memmove(ctypes.byref(result), chunk, ctypes.sizeof(result))
return result
def _addWindowsIconFromIcons(onefile):
# Relatively detailed handling, pylint: disable=too-many-locals
icon_group = 1
image_id = 1
images = []
result_filename = getResultFullpath(onefile=onefile, real=False)
for icon_spec in getWindowsIconPaths():
if "#" in icon_spec:
icon_path, icon_index = icon_spec.rsplit("#", 1)
icon_index = int(icon_index)
else:
icon_path = icon_spec
icon_index = None
if not hasFilenameExtension(icon_path, ".ico"):
postprocessing_logger.info(
"File '%s' is not in Windows icon format, converting to it." % icon_path
)
if icon_index is not None:
return postprocessing_logger.sysexit(
"Cannot specify indexes with non-ico format files in '%s'."
% icon_spec
)
icon_build_path = os.path.join(
getSourceDirectoryPath(onefile=onefile, create=False),
"icons",
)
makePath(icon_build_path)
converted_icon_path = os.path.join(
icon_build_path,
"icon-%d.ico" % image_id,
)
convertImageToIconFormat(
logger=postprocessing_logger,
image_filename=icon_spec,
converted_icon_filename=converted_icon_path,
)
icon_path = converted_icon_path
with open(icon_path, "rb") as icon_file:
# Read header and icon entries.
header = readFromFile(icon_file, IconDirectoryHeader)
icons = [
readFromFile(icon_file, IconDirectoryEntry)
for _i in range(header.count)
]
if icon_index is not None:
if icon_index > len(icons):
return postprocessing_logger.sysexit(
"Error, referenced icon index %d in file '%s' with only %d icons."
% (icon_index, icon_path, len(icons))
)
icons[:] = icons[icon_index : icon_index + 1]
postprocessing_logger.info(
"Adding %d icon(s) from icon file '%s'." % (len(icons), icon_spec)
)
# Image data are to be scanned from places specified icon entries
for icon in icons:
icon_file.seek(icon.image_offset, 0)
images.append(icon_file.read(icon.image_size))
parts = [convertStructureToBytes(header)]
for icon in icons:
parts.append(
convertStructureToBytes(
IconGroupDirectoryEntry(
width=icon.width,
height=icon.height,
colors=icon.colors,
reserved=icon.reserved,
planes=icon.planes,
bit_count=icon.bit_count,
image_size=icon.image_size,
id=image_id,
)
)
)
image_id += 1
addResourceToFile(
target_filename=result_filename,
data=b"".join(parts),
resource_kind=RT_GROUP_ICON,
lang_id=0,
res_name=icon_group,
logger=postprocessing_logger,
)
for count, image in enumerate(images, 1):
addResourceToFile(
target_filename=result_filename,
data=image,
resource_kind=RT_ICON,
lang_id=0,
res_name=count,
logger=postprocessing_logger,
)
def createScriptFileForExecution(result_filename):
script_filename = getResultRunFilename(onefile=False)
# TODO: This is probably a prefix kind that should be used more often, e.g.
# in reporting.
if isSelfCompiledPythonUninstalled():
sys_prefix = os.path.dirname(sys.executable)
else:
sys_prefix = sys.prefix
python_home = makeFilesystemEncodable(sys_prefix)
python_path = os.pathsep.join(makeFilesystemEncodable(e) for e in sys.path)
debugger_call = (
(
" ".join(
wrapCommandForDebuggerForSubprocess(
command=(), debugger=getDebuggerName()
)
)
+ " "
)
if shallRunInDebugger()
else ""
)
exe_filename = os.path.basename(makeFilesystemEncodable(result_filename))
if isWin32Windows():
script_contents = """
@echo off
rem This script was created by Nuitka to execute '%(exe_filename)s' with Python DLL being found.
set PATH=%(dll_directory)s;%%PATH%%
set PYTHONHOME=%(python_home)s
set NUITKA_PYTHONPATH=%(python_path)s
%(debugger_call)s"%%~dp0%(exe_filename)s" %%*
""" % {
"debugger_call": debugger_call,
"dll_directory": makeFilesystemEncodable(
os.path.dirname(getTargetPythonDLLPath())
),
"python_home": python_home,
"python_path": python_path,
"exe_filename": exe_filename,
}
elif isAIX() and not isOnefileMode() and isStandaloneMode():
# On AIX we need to set LIBPATH in order to force it to load the DLL from the standalone
# distribution. The onefile bootstrap does it the code already, and therefore does not
# need it at all.
# spell-checker: ignore LIBPATH
script_contents = """\
#!/bin/sh
# Absolute path to this script, e.g. /home/user/bin/foo.sh
SCRIPT=$(readlink -f "$0")
# Absolute path this script is in, thus /home/user/bin
SCRIPT_PATH=$(dirname "$SCRIPT")
LIBPATH=$SCRIPT_PATH
export LIBPATH
%(debugger_call)s"$SCRIPT_PATH/%(exe_filename)s" $@
""" % {
"debugger_call": debugger_call,
"exe_filename": exe_filename,
}
else:
# TODO: Setting PYTHONPATH should not be needed, but it fails to work
# unlike on Windows for unknown reasons.
script_contents = """\
#!/bin/sh
# Absolute path to this script, e.g. /home/user/bin/foo.sh
SCRIPT=$(readlink -f "$0")
# Absolute path this script is in, thus /home/user/bin
SCRIPT_PATH=$(dirname "$SCRIPT")
PYTHONHOME=%(python_home)s
export PYTHONHOME
NUITKA_PYTHONPATH="%(python_path)s"
export NUITKA_PYTHONPATH
PYTHONPATH="%(python_path)s"
export PYTHONPATH
%(debugger_call)s"$SCRIPT_PATH/%(exe_filename)s" $@
""" % {
"debugger_call": debugger_call,
"python_home": python_home,
"python_path": python_path,
"exe_filename": exe_filename,
}
putTextFileContents(script_filename, script_contents)
if not isWin32Windows():
addFileExecutablePermission(script_filename)
def executePostProcessingResources(result_filename, manifest, onefile):
"""Adding Windows resources to the binary.
Used for both onefile and not onefile binary, potentially two times.
"""
if manifest is None:
manifest = getDefaultWindowsExecutableManifest()
if shallAskForWindowsAdminRights():
manifest.addUacAdmin()
if shallAskForWindowsUIAccessRights():
manifest.addUacUiAccess()
manifest.addResourceToFile(result_filename, logger=postprocessing_logger)
if (
getWindowsVersionInfoStrings()
or getProductVersionTuple()
or getFileVersionTuple()
):
addVersionInfoResource(
string_values=getWindowsVersionInfoStrings(),
product_version=getProductVersionTuple(),
file_version=getFileVersionTuple(),
file_date=(0, 0),
is_exe=not shallMakeModule(),
result_filename=result_filename,
logger=postprocessing_logger,
)
# Attach icons from template file if given.
template_exe = getWindowsIconExecutablePath()
if template_exe is not None:
res_copied = copyResourcesFromFileToFile(
template_exe,
target_filename=result_filename,
resource_kinds=(RT_ICON, RT_GROUP_ICON),
)
if res_copied == 0:
postprocessing_logger.warning(
"The specified icon template executable '%s' didn't contain anything to copy."
% template_exe
)
else:
postprocessing_logger.warning(
"Copied %d icon resources from '%s'." % (res_copied, template_exe)
)
else:
_addWindowsIconFromIcons(onefile=onefile)
splash_screen_filename = getWindowsSplashScreen()
if splash_screen_filename is not None:
splash_data = getFileContents(splash_screen_filename, mode="rb")
addResourceToFile(
target_filename=result_filename,
data=splash_data,
resource_kind=RT_RCDATA,
lang_id=0,
res_name=28,
logger=postprocessing_logger,
)
def _createModulePyiFile():
if shallCreatePyiFileContainStubs():
source_code = getMainModule().getSourceCode()
if source_code is None:
stubgen = None
stubgen_reason = "Namespace packages have no code to use stubgen on"
elif python_version >= 0x300:
stubgen = importFromInlineCopy("stubgen", must_exist=False)
stubgen_reason = "Stubs included by default"
else:
stubgen = None
stubgen_reason = "Python2 is not supported for stubs"
else:
source_code = None
stubgen = None
stubgen_reason = "Stubs were disabled"
imported_module_names = getImportedModuleNames()
contents = """\
# This file was generated by Nuitka
"""
if stubgen is not None:
try:
stubs = stubgen.generate_stub_from_source(
source_code=source_code,
output_file_path=None,
text_only=True,
)
except Exception as e: # pylint: disable=broad-exception-caught
postprocessing_logger.warning(
"""\
Stub generation with internal stubgen tool failed due to: '%s'.
Please report the module code in an issue report."""
% (str(e))
)
if states.is_debug:
raise
stubs = None
stubgen_reason = (
"Stub generation failed due to error, please report the issue."
)
else:
stubs = None
stubs = "# %s\n%s" % (stubgen_reason, "" if stubs is None else stubs)
contents += "\n" + stubs + "\n"
contents += "__name__ = ..."
imported_module_names = getImportedModuleNames()
# Meaningless, TODO: Potentially others could be listed here too.
if "__future__" in imported_module_names:
imported_module_names.remove("__future__")
contents += "\n" * 4
if imported_module_names:
contents += (
"# Modules used internally, to allow implicit dependencies to be seen:\n"
)
contents += "\n".join(
"import %s" % module_name for module_name in getImportedModuleNames()
)
else:
contents += "# No other modules used internally, no implicit dependencies.\n"
pyi_filename = getResultBasePath() + ".pyi"
putTextFileContents(pyi_filename, contents, encoding="utf-8")
def executePostProcessing(result_filename):
"""Postprocessing of the resulting binary.
These are in part required steps, not usable after failure.
"""
# Lots of cases to deal with,
# pylint: disable=too-many-branches
if isWin32Windows():
if not shallMakeModule():
if python_version < 0x300:
# Copy the Windows manifest from the CPython binary to the created
# executable, so it finds "MSCRT.DLL". This is needed for Python2
# only, for Python3 newer MSVC doesn't hide the C runtime.
manifest = getWindowsExecutableManifest(sys.executable)
else:
manifest = None
executePostProcessingResources(
result_filename=result_filename, manifest=manifest, onefile=False
)
source_dir = getSourceDirectoryPath(onefile=False, create=True)
# Attach the binary blob as a Windows resource.
addResourceToFile(
target_filename=result_filename,
data=getFileContents(getConstantBlobFilename(source_dir), mode="rb"),
resource_kind=RT_RCDATA,
res_name=3,
lang_id=0,
logger=postprocessing_logger,
)
# On macOS, we update the executable path for searching the "libpython"
# library.
if isMacOS() and not shallMakeModule() and not shallUseStaticLibPython():
for dependency in parseOtoolListingOutput(
getOtoolDependencyOutput(result_filename)
):
if os.path.basename(dependency).lower().startswith(("python", "libpython")):
python_dll_filename = dependency
break
else:
return postprocessing_logger.sysexit(
"""
Error, expected 'libpython dependency not found. Please report the bug."""
)
python_lib_path = os.path.dirname(python_dll_filename)
python_dll_path = python_dll_filename
if not os.path.exists(python_lib_path):
python_lib_path = os.path.join(sys.prefix, "lib")
python_dll_path = os.path.join(
python_lib_path, os.path.basename(python_dll_filename)
)
# Note: For CPython, and potentially others, the rpath for the Python
# library needs to be set, so it will be detected as a dependency
# without tricks.
callInstallNameTool(
filename=result_filename,
mapping=(
(
python_dll_filename,
python_dll_path,
),
),
id_path=None,
rpath=python_lib_path,
)
if shallCreateAppBundle():
createPlistInfoFile(logger=postprocessing_logger, onefile=False)
# Modules should not be executable, but Scons creates them like it, fix
# it up here.
if not isWin32Windows() and shallMakeModule():
removeFileExecutablePermission(result_filename)
if isWin32Windows() and shallMakeModule():
candidate = os.path.join(
os.path.dirname(result_filename),
"lib" + os.path.basename(result_filename)[:-4] + ".a",
)
if os.path.exists(candidate):
os.unlink(candidate)
if isAndroidBasedLinux():
cleanupHeaderForAndroid(result_filename)
# Might have to create a CMD file, potentially with debugger run.
if shallCreateScriptFileForExecution():
createScriptFileForExecution(result_filename=result_filename)
# Create a ".pyi" file for created modules
if shallMakeModule() and shallCreatePyiFile():
_createModulePyiFile()
if isWin32Windows() and getFileSize(result_filename) > 2**30 * 1.8:
postprocessing_logger.warning(
"""\
The created compiled binary is larger than 1.8GB and therefore may not be
executable by Windows due to its limitations."""
)
# Part of "Nuitka", an optimizing Python compiler that is compatible and
# integrates with CPython, but also works on its own.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.