OpenMiInk/third_party/krita-ci-utilities/build-windows-zip-package.py
Hellings b8c8ff98e2 Hi
2026-10-16 09:05:56 -04:00

684 lines
28 KiB
Python

#!/usr/bin/env python
# SPDX-FileCopyrightText: 2021 L. E. Segovia <amy@amyspark.me>
# SPDX-License-Identifier: GPL-2.0-or-later
# This Python script is meant to prepare a Krita package folder to be zipped or
# to be a base for the installer.
import argparse
import glob
import itertools
import os
import re
import pathlib
import shutil
import subprocess
import sys
from components import MergeFolders, CommonUtils
# Subroutines
def choice(prompt="Is this ok?"):
c = input(f"{prompt} [y/n] ")
if c == "y":
return True
else:
return False
def find_on_path(variable, executable):
exeString = shutil.which(executable)
if exeString:
os.environ[variable] = exeString
def prompt_for_file(prompt):
user_input = input(f"{prompt} ")
if not len(user_input):
return None
result = os.path.isfile(user_input)
if result is None:
print("Input does not point to valid dir!")
return prompt_for_file(prompt)
else:
return os.path.realpath(user_input)
def prompt_for_dir(prompt):
user_input = input(f"{prompt} ")
if not len(user_input):
return None
result = os.path.isdir(user_input)
if result is None:
print("Input does not point to valid dir!")
return prompt_for_dir(prompt)
else:
return os.path.realpath(user_input)
# command-line args parsing
parser = argparse.ArgumentParser()
basic_options = parser.add_argument_group("Basic options")
basic_options.add_argument("--no-interactive", action='store_true',
help="Run without interactive prompts. When not specified, the script will prompt for some of the parameters.")
basic_options.add_argument(
"--package-name", action='store', help="Specify the package name", required=True)
path_options = parser.add_argument_group("Path options")
path_options.add_argument("--src-dir", action='store',
help="Specify Krita source dir. If unspecified, this will be determined from the script location")
path_options.add_argument("--deps-install-dir", action='store',
help="Specify deps install dir")
path_options.add_argument("--krita-install-dir", action='store',
help="Specify Krita install dir")
special_options = parser.add_argument_group("Special options")
special_options.add_argument("--pre-zip-hook", action='store',
help="Specify a script to be called before packaging the zip archive, can be used to sign the binaries")
args = parser.parse_args()
# Check environment config
useVerbosePackagingLog = CommonUtils.useVerbosePackagingLog()
logger = CommonUtils.createLogger(f"{os.path.basename(__file__)}.log")
def run_subprocess_checked(argList):
logger.info(f"Run: {' '.join(argList)}")
subprocess.run(argList, check=True)
if os.environ.get("SEVENZIP_EXE") is None:
find_on_path("SEVENZIP_EXE", "7z.exe")
if os.environ.get("SEVENZIP_EXE") is None:
find_on_path("SEVENZIP_EXE", "7za.exe")
if os.environ.get("SEVENZIP_EXE") is None:
os.environ["SEVENZIP_EXE"] = f"{os.environ['ProgramFiles']}\\7-Zip\\7z.exe"
if not os.path.isfile(os.environ["SEVENZIP_EXE"]):
os.environ["SEVENZIP_EXE"] = "{}\\7-Zip\\7z.exe".format(
os.environ["ProgramFiles(x86)"])
if not os.path.isfile(os.environ["SEVENZIP_EXE"]):
logger.error("7-Zip not found!")
sys.exit(102)
logger.info(f"7-Zip: {os.environ['SEVENZIP_EXE']}")
if not os.environ.get("MINGW_BIN_DIR"):
find_on_path("MINGW_BIN_DIR_MAKE_EXE", "mingw32-make.exe")
if not os.environ.get('MINGW_BIN_DIR_MAKE_EXE'):
if not args.no_interactive:
os.environ['MINGW_BIN_DIR_MAKE_EXE'] = prompt_for_file("Provide path to mingw32-make.exe of mingw-w64")
if not os.environ.get('MINGW_BIN_DIR_MAKE_EXE'):
logger.error("mingw-w64 not found!")
sys.exit(102)
os.environ['MINGW_BIN_DIR'] = os.path.dirname(os.environ['MINGW_BIN_DIR_MAKE_EXE'])
else:
os.environ['MINGW_BIN_DIR'] = os.path.dirname(os.environ['MINGW_BIN_DIR_MAKE_EXE'])
logger.info(f"Found mingw on PATH: {os.environ['MINGW_BIN_DIR_MAKE_EXE']}")
if not args.no_interactive:
status = choice("Is this correct?")
if not status:
os.environ['MINGW_BIN_DIR_MAKE_EXE'] = prompt_for_dir("Provide path to mingw32-make.exe of mingw-w64")
if not os.environ['MINGW_BIN_DIR_MAKE_EXE']:
logger.error("mingw-w64 not found!")
sys.exit(102)
os.environ['MINGW_BIN_DIR'] = os.path.dirname(os.environ['MINGW_BIN_DIR_MAKE_EXE'])
logger.info(f"mingw-w64: {os.environ['MINGW_BIN_DIR']}")
# Windows SDK is needed for windeployqt to get d3dcompiler_xx.dll
# If we don't define this variable, windeployqt will fetch the library
# from %WINDIR%\system32, which is not supposed to be redistributable
if os.environ.get("WindowsSdkDir") is None and os.environ.get("ProgramFiles(x86)") is not None:
os.environ["WindowsSdkDir"] = "{}\\Windows Kits\\10".format(
os.environ["ProgramFiles(x86)"])
KRITA_SRC_DIR = None
if args.src_dir is not None:
KRITA_SRC_DIR = args.src_dir
if KRITA_SRC_DIR is None:
_temp = sys.argv[0]
if os.path.dirname(_temp).endswith("\\packaging\\windows"):
_base = pathlib.PurePath(_temp)
if os.path.isfile(f"{_base.parents[2]}\\CMakeLists.txt"):
if os.path.isfile(f"{_base.parents[2]}\\libs\\version\\kritaversion.h.cmake"):
KRITA_SRC_DIR = os.path.realpath(_base.parents[2])
logger.info("Script is running inside Krita source dir")
if KRITA_SRC_DIR is None:
if args.no_interactive:
KRITA_SRC_DIR = prompt_for_dir("Provide path of Krita src dir")
if KRITA_SRC_DIR is None:
logger.error("Krita src dir not found!")
sys.exit(102)
logger.info(f"Krita src: {KRITA_SRC_DIR}")
DEPS_INSTALL_DIR = None
if args.deps_install_dir is not None:
DEPS_INSTALL_DIR = args.deps_install_dir
if DEPS_INSTALL_DIR is None:
DEPS_INSTALL_DIR = f"{os.getcwd()}\\_install"
logger.info(f"Using default deps install dir: {DEPS_INSTALL_DIR}")
if not args.no_interactive:
status = choice()
if not status:
DEPS_INSTALL_DIR = prompt_for_dir(
"Provide path of deps install dir")
if DEPS_INSTALL_DIR is None:
logger.error("Deps install dir not set!")
sys.exit(102)
logger.info(f"Deps install dir: {DEPS_INSTALL_DIR}")
KRITA_INSTALL_DIR = None
if args.krita_install_dir is not None:
KRITA_INSTALL_DIR = args.krita_install_dir
if KRITA_INSTALL_DIR is None:
KRITA_INSTALL_DIR = f"{os.getcwd()}\\_install"
logger.info(f"Using default Krita install dir: {KRITA_INSTALL_DIR}")
if not args.no_interactive:
status = choice()
if not status:
KRITA_INSTALL_DIR = prompt_for_dir(
"Provide path of Krita install dir")
if KRITA_INSTALL_DIR is None:
logger.error("Krita install dir not set!")
sys.exit(102)
logger.info(f"Krita install dir: {KRITA_INSTALL_DIR}")
# Simple checking
if not os.path.isdir(DEPS_INSTALL_DIR):
logger.error("Cannot find the deps install folder!")
sys.exit(1)
if not os.path.isdir(KRITA_INSTALL_DIR):
logger.error("Cannot find the krita install folder!")
sys.exit(1)
# Amyspark: paths with spaces are automagically handled by Python!
pkg_name = args.package_name
logger.info(f"Package name is {pkg_name}")
pkg_root = f"{os.getcwd()}\\{pkg_name}"
logger.info(f"Packaging dir is {pkg_root}\n")
if os.path.isdir(pkg_root):
logger.error(
"Packaging dir already exists! Please remove or rename it first.")
sys.exit(1)
if os.path.isfile(f"{pkg_root}.zip"):
logger.error(
"Packaging zip already exists! Please remove or rename it first.")
sys.exit(1)
if os.path.isfile(f"{pkg_root}-dbg.zip"):
logger.error(
"Packaging debug zip already exists! Please remove or rename it first.")
sys.exit(1)
if not args.no_interactive:
status = choice()
if not status:
sys.exit(255)
print("")
# Initialize clean PATH
os.environ["PATH"] = fr"{os.environ['SystemRoot']}\system32;{os.environ['SystemRoot']};{os.environ['SystemRoot']}\System32\Wbem;{os.environ['SystemRoot']}\System32\WindowsPowerShell\v1.0" + "\\"
os.environ["PATH"] = fr"{os.environ['MINGW_BIN_DIR']};{DEPS_INSTALL_DIR}\bin;{os.environ['PATH']}"
logger.info("")
logger.info("Trying to guess compiler version...")
for arg in ("g++", "clang++"):
commandToRun = f"{arg} --version"
ret = subprocess.call(commandToRun, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, shell=True)
if ret != 1:
os.environ['CC'] = arg
if not os.environ.get('CC'):
logger.error("No working compiler.")
sys.exit(1)
output = subprocess.check_output(f"{os.environ['CC']} --version", shell=True, text=True)
GCCversionLines = output.splitlines()
GCC_VERSION_LINE = ""
for versionLine in GCCversionLines:
if re.search("g\\+\\+", versionLine):
GCC_VERSION_LINE = versionLine
# The space in "clang " is essential to detect
# the version line and not the InstalledDir
if not GCC_VERSION_LINE:
for versionLine in GCCversionLines:
if re.search("clang ", versionLine):
GCC_VERSION_LINE = versionLine
if not GCC_VERSION_LINE:
logger.error("Failed to get version of g++.")
sys.exit(1)
logger.info(f"-- {GCC_VERSION_LINE}")
IS_TDM = False
IS_MSYS = False
IS_CLANG = False
if re.search("tdm64", GCC_VERSION_LINE):
logger.info("Compiler looks like TDM64-GCC")
IS_TDM = True
if re.search("MSYS", GCC_VERSION_LINE):
logger.info("Compiler looks like MSYS GCC")
IS_MSYS = True
if re.search("clang", GCC_VERSION_LINE):
logger.info("Compiler looks like Clang")
IS_CLANG = True
else:
logger.info("Compiler looks like plain old MinGW64")
IS_LLVM_MINGW = False
IS_MSYS_CLANG = False
if IS_CLANG:
# Look for the multiarch target binary dirs. Unfortunately there is
# no surefire way to detect a llvm-mingw toolchain.
if os.path.isdir(f"{os.environ['MINGW_BIN_DIR']}\\..\\i686-w64-mingw32\\bin\\") and \
os.path.isdir(f"{os.environ['MINGW_BIN_DIR']}\\..\\x86_64-w64-mingw32\\bin\\"):
logger.info("Toolchain looks like llvm-mingw")
IS_LLVM_MINGW = True
else:
logger.info("Toolchain does not look like llvm-mingw, assuming MSYS Clang")
IS_MSYS_CLANG = True
logger.info("")
logger.info("Trying to guess target architecture...")
OBJDUMP = CommonUtils.detectObjdump()
if not OBJDUMP:
logger.error("objdump is not working.")
sys.exit(1)
output = subprocess.check_output(fr"{OBJDUMP} -f {KRITA_INSTALL_DIR}\bin\krita.exe", text=True)
targetArchLines = output.splitlines()
TARGET_ARCH_LINE = ""
for archLine in targetArchLines:
if re.search("i386", archLine):
TARGET_ARCH_LINE = archLine
if not TARGET_ARCH_LINE:
logger.info("Possible LLVM objdump, trying to detect architecture...")
if re.search("coff", archLine):
TARGET_ARCH_LINE = archLine
logger.info(f"-- {TARGET_ARCH_LINE}")
IS_x64 = False
if not re.search("x86-64", TARGET_ARCH_LINE):
logger.info("Target looks like x86")
else:
logger.info("Target looks like x86_64")
IS_x64 = True
logger.info("")
logger.info("Testing for objcopy...")
commandToRun = "objcopy --version"
try:
subprocess.check_call(commandToRun, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, shell=True)
except subprocess.CalledProcessError:
logger.error("objcopy is not working.")
sys.exit(1)
logger.info("")
logger.info("Testing for strip...")
commandToRun = "strip --version"
try:
subprocess.check_call(commandToRun, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, shell=True)
except subprocess.CalledProcessError:
logger.error("strip is not working.")
sys.exit(1)
logger.info("")
logger.info("Trying to guess Qt version...")
useQt6Build = False
if os.path.exists(f"{DEPS_INSTALL_DIR}\\bin\\Qt6Core.dll"):
logger.info(" Found Qt6")
useQt6Build = True
else:
logger.info(" Found Qt5")
logger.info("")
logger.info("Creating base directories...")
try:
os.makedirs(f"{pkg_root}", exist_ok=True)
os.makedirs(f"{pkg_root}\\bin", exist_ok=True)
os.makedirs(f"{pkg_root}\\etc", exist_ok=True)
os.makedirs(f"{pkg_root}\\lib", exist_ok=True)
os.makedirs(f"{pkg_root}\\share", exist_ok=True)
except:
logger.error("Cannot create packaging dir tree!")
sys.exit(1)
logger.info("")
logger.info("Copying GCC libraries...")
os.environ['STDLIBS_DIR'] = os.environ['MINGW_BIN_DIR']
if IS_TDM:
if not IS_x64:
# TDM-GCC x86
os.environ['STDLIBS'] = "libgomp-1"
else:
# TDM-GCC x64
os.environ['STDLIBS'] = "libgomp_64-1"
elif IS_MSYS:
if not IS_x64:
# msys-mingw-w64 x86
os.environ['STDLIBS'] = "libgcc_s_dw2-1 libgomp-1 libstdc++-6 libwinpthread-1 libatomic-1 libiconv-2 zlib1 libexpat-1 libintl-8 libssl-1_1 libcrypto-1_1"
else:
# msys-mingw-w64 x64
os.environ['STDLIBS'] = "libgcc_s_seh-1 libgomp-1 libstdc++-6 libwinpthread-1 libatomic-1 libiconv-2 zlib1 libexpat-1 libintl-8 libssl-1_1-x64 libcrypto-1_1-x64"
elif IS_MSYS_CLANG:
if not IS_x64:
# msys-mingw-w64-clang64 x86
os.environ['STDLIBS'] = "libomp libssp-0 libunwind libc++ libwinpthread-1 libiconv-2 zlib1 libexpat-1 libintl-8 libssl-1_1 libcrypto-1_1"
else:
# msys-mingw-w64-clang64 x64
os.environ['STDLIBS'] = "libomp libssp-0 libunwind libc++ libwinpthread-1 libiconv-2 zlib1 libexpat-1 libintl-8 libssl-1_1-x64 libcrypto-1_1-x64"
elif IS_LLVM_MINGW:
# llvm-mingw
os.environ['STDLIBS'] = "libc++ libomp libssp-0 libunwind libwinpthread-1"
# The toolchain does not include all of the DLLs in the compiler bin dir,
# so we need to copy them from the cross target bin dir.
if not IS_x64:
asanLibName ='libclang_rt.asan_dynamic-i386'
os.environ['STDLIBS_DIR'] = fr"{os.environ['MINGW_BIN_DIR']}\..\i686-w64-mingw32\bin"
else:
asanLibName = 'libclang_rt.asan_dynamic-x86_64'
os.environ['STDLIBS_DIR'] = fr"{os.environ['MINGW_BIN_DIR']}\..\x86_64-w64-mingw32\bin"
output = subprocess.check_output(fr"{OBJDUMP} -p {KRITA_INSTALL_DIR}\bin\krita.exe", text=True)
if re.search(asanLibName, output):
logger.info('The package contains ASAN, packaging the ASAN runtime as well!')
os.environ['STDLIBS'] = f'{os.environ["STDLIBS"]} {asanLibName}'
symbolizerBinaries = ['libLLVM-18.dll', 'llvm-symbolizer.exe']
for file in symbolizerBinaries:
filePath = os.path.join(os.environ['MINGW_BIN_DIR'], file)
logger.info(f'Copying symbolizer lib: {filePath}')
shutil.copy(filePath, fr"{pkg_root}\bin")
else:
logger.info('No ASAN was found in the Krita binary, skipping its packaging...')
else:
if not IS_x64:
# mingw-w64 x86
os.environ['STDLIBS'] = "libgcc_s_dw2-1 libgomp-1 libstdc++-6 libwinpthread-1"
else:
# mingw-w64 x64
os.environ['STDLIBS'] = "libgcc_s_seh-1 libgomp-1 libstdc++-6 libwinpthread-1"
for lib in os.environ['STDLIBS'].split(" "):
libPath = fr"{os.environ['STDLIBS_DIR']}\{lib}.dll"
# libssp-0 is not present in clang18
if lib != 'libssp-0' or os.path.isfile(libPath):
shutil.copy(libPath, fr"{pkg_root}\bin")
logger.info("")
logger.info("Copying files...")
# krita.exe
shutil.copy(f"{KRITA_INSTALL_DIR}\\bin\\krita.exe", f"{pkg_root}\\bin\\")
shutil.copy(f"{KRITA_INSTALL_DIR}\\bin\\krita.com", f"{pkg_root}\\bin\\")
if os.path.isfile(f"{KRITA_INSTALL_DIR}\\bin\\krita.pdb"):
shutil.copy(f"{KRITA_INSTALL_DIR}\\bin\\krita.pdb", f"{pkg_root}\\bin\\")
# kritarunner.exe
if os.path.isfile(f"{KRITA_INSTALL_DIR}\\bin\\kritarunner.exe"):
shutil.copy(f"{KRITA_INSTALL_DIR}\\bin\\kritarunner.exe", f"{pkg_root}\\bin\\")
if os.path.isfile(f"{KRITA_INSTALL_DIR}\\bin\\kritarunner.pdb"):
shutil.copy(f"{KRITA_INSTALL_DIR}\\bin\\kritarunner.pdb",
f"{pkg_root}\\bin\\")
shutil.copy(f"{KRITA_INSTALL_DIR}\\bin\\kritarunner.com",
f"{pkg_root}\\bin\\")
if os.path.isfile(f"{KRITA_INSTALL_DIR}\\bin\\FreehandStrokeBenchmark.exe"):
shutil.copy(f"{KRITA_INSTALL_DIR}\\bin\\FreehandStrokeBenchmark.exe", f"{pkg_root}\\bin\\")
MergeFolders.merge_folders(f"{DEPS_INSTALL_DIR}\\bin\\data\\", f"{pkg_root}\\bin\\data\\")
# qt.conf -- to specify the location to Qt translations
shutil.copy(fr"{KRITA_SRC_DIR}\packaging\windows\qt.conf", fr"{pkg_root}\bin")
# DLLs from bin/
logger.info("Copying all DLLs except Qt5/Qt6 * from bin/")
files = glob.glob(f"{KRITA_INSTALL_DIR}\\bin\\*.dll")
pdbs = glob.glob(f"{KRITA_INSTALL_DIR}\\bin\\*.pdb")
for f in itertools.chain(files, pdbs):
if not os.path.basename(f).startswith("Qt5") and not os.path.basename(f).startswith("Qt6"):
shutil.copy(f, f"{pkg_root}\\bin")
files = glob.glob(f"{DEPS_INSTALL_DIR}\\bin\\*.dll")
for f in files:
pdb = f"{os.path.dirname(f)}\\{os.path.splitext(os.path.basename(f))[0]}.pdb"
if not os.path.basename(f).startswith("Qt5") and not os.path.basename(f).startswith("Qt6"):
shutil.copy(f, f"{pkg_root}\\bin")
if os.path.isfile(pdb):
shutil.copy(pdb, f"{pkg_root}\\bin")
# symsrv.yes for Dr. Mingw
shutil.copy(f"{DEPS_INSTALL_DIR}\\bin\\symsrv.yes", f"{pkg_root}\\bin")
# DLLs from lib/
logger.info("Copying all DLLs from lib/ (deps)")
files = glob.glob(fr"{DEPS_INSTALL_DIR}\lib\*.dll")
for f in files:
shutil.copy(f, fr"{pkg_root}\bin")
for f in files:
shutil.copy(f, fr"{pkg_root}\bin")
# KF5/KF6 plugins may be placed at different locations depending on how Qt is built
MergeFolders.merge_folders(f"{DEPS_INSTALL_DIR}\\lib\\plugins\\imageformats\\", f"{pkg_root}\\bin\\imageformats\\")
MergeFolders.merge_folders(f"{DEPS_INSTALL_DIR}\\plugins\\imageformats\\", f"{pkg_root}\\bin\\imageformats\\")
if useQt6Build:
MergeFolders.merge_folders(f"{DEPS_INSTALL_DIR}\\plugins\\kf6\\", f"{pkg_root}\\bin\\kf6\\")
else:
MergeFolders.merge_folders(f"{DEPS_INSTALL_DIR}\\plugins\\kf5\\", f"{pkg_root}\\bin\\kf5\\")
# Copy the sql drivers explicitly
MergeFolders.merge_folders(f"{DEPS_INSTALL_DIR}\\plugins\\sqldrivers\\", f"{pkg_root}\\bin\\sqldrivers")
# Qt Translations
# it seems that windeployqt does these, but only * some * of these???
os.makedirs(f"{pkg_root}\\bin\\translations", exist_ok=True)
files = glob.glob(f"{DEPS_INSTALL_DIR}\\translations\\qt_*.qm")
for f in files:
# Exclude qt_help_*.qm
if not os.path.basename(f).startswith("qt_help"):
shutil.copy(f, f"{pkg_root}\\bin\\translations")
# Krita plugins
MergeFolders.merge_folders(f"{KRITA_INSTALL_DIR}\\lib\\kritaplugins\\", f"{pkg_root}\\lib\\kritaplugins\\")
if os.path.isdir(f"{DEPS_INSTALL_DIR}\\lib\\krita-python-libs"):
MergeFolders.merge_folders(f"{DEPS_INSTALL_DIR}\\lib\\krita-python-libs", f"{pkg_root}\\lib\\krita-python-libs")
if os.path.isdir(f"{KRITA_INSTALL_DIR}\\lib\\krita-python-libs"):
MergeFolders.merge_folders(f"{KRITA_INSTALL_DIR}\\lib\\krita-python-libs", f"{pkg_root}\\lib\\krita-python-libs")
if os.path.isdir(f"{DEPS_INSTALL_DIR}\\lib\\site-packages"):
MergeFolders.merge_folders(f"{DEPS_INSTALL_DIR}\\lib\\site-packages", f"{pkg_root}\\lib\\site-packages")
# MLT plugins and their data
MergeFolders.merge_folders(f"{DEPS_INSTALL_DIR}\\lib\\mlt", f"{pkg_root}\\lib\\mlt")
MergeFolders.merge_folders(f"{DEPS_INSTALL_DIR}\\share\\mlt", f"{pkg_root}\\share\\mlt")
# Fontconfig
MergeFolders.merge_folders(f"{DEPS_INSTALL_DIR}\\etc\\fonts", f"{pkg_root}\\etc\\fonts")
# Share
MergeFolders.merge_folders(f"{KRITA_INSTALL_DIR}\\share\\color", f"{pkg_root}\\share\\color")
MergeFolders.merge_folders(f"{KRITA_INSTALL_DIR}\\share\\color-schemes", f"{pkg_root}\\share\\color-schemes")
MergeFolders.merge_folders(f"{KRITA_INSTALL_DIR}\\share\\icons", f"{pkg_root}\\share\\icons")
MergeFolders.merge_folders(f"{KRITA_INSTALL_DIR}\\share\\krita", f"{pkg_root}\\share\\krita")
MergeFolders.merge_folders(f"{KRITA_INSTALL_DIR}\\share\\kritaplugins", f"{pkg_root}\\share\\kritaplugins")
if useQt6Build:
MergeFolders.merge_folders(f"{DEPS_INSTALL_DIR}\\share\\kf6", f"{pkg_root}\\share\\kf6")
else:
MergeFolders.merge_folders(f"{DEPS_INSTALL_DIR}\\share\\kf5", f"{pkg_root}\\share\\kf5")
MergeFolders.merge_folders(f"{DEPS_INSTALL_DIR}\\share\\mime", f"{pkg_root}\\share\\mime")
# Python libs are copied by share\krita above
# Copy locale to bin
MergeFolders.merge_folders(f"{KRITA_INSTALL_DIR}\\share\\locale", f"{pkg_root}\\bin\\locale")
MergeFolders.merge_folders(f"{DEPS_INSTALL_DIR}\\share\\locale", f"{pkg_root}\\bin\\locale")
# Copy shortcut link from source (can't create it dynamically)
shutil.copy(f"{KRITA_SRC_DIR}\\packaging\\windows\\krita.lnk", pkg_root)
shutil.copy(
f"{KRITA_SRC_DIR}\\packaging\\windows\\krita-minimal.lnk", pkg_root)
shutil.copy(
f"{KRITA_SRC_DIR}\\packaging\\windows\\krita-animation.lnk", pkg_root)
# QML deployment:
#
# When deploying QML modules we should pass windeployqt all the folders
# **in the source tree** where our resource-embedded .qml files are situated.
# Every such folder should be declared with --qmldir option. These files will
# **not** be deployed (because they are expected to be stored as binary resources),
# but all their dependencies will be deployed to the package folder.
#
# Multiple QML dependencies search paths can be provided by --qmlimport switch,
# we don't pass it explicitly and let it be deduced by windeployqt using qtpath
# executable.
# Here we should list all the folders/plugins in Krita that have
# .qml files inside. Theoretically, we can just pass the entire Krita's
# source tree, but I'm not sure it is a good idea.
QMLDIR_ARGS = ["--qmldir", fr"{KRITA_SRC_DIR}\plugins\dockers\textproperties"]
# A safeguard for the case when KDE_INSTALL_USE_QT_SYS_PATHS is not properly
# activated on Windows and the QML modules are installed into a default KDE's
# location instead of the one returned by qtpath. If you see this error you
# should either recreate your build tree, or pass -DKDE_INSTALL_USE_QT_SYS_PATHS=ON
# to the build or check if you have non-standard qt.conf in your installation
# root.
if os.path.isdir(fr"{KRITA_INSTALL_DIR}\lib\qml"):
logger.error("Some of Krita's QML modules were installed in an incorrect location")
logger.error(fr" actual path: {KRITA_INSTALL_DIR}\lib\qml")
logger.error(fr" expected path: {KRITA_INSTALL_DIR}\qml")
exit(103)
if os.path.isdir(fr"{DEPS_INSTALL_DIR}\lib\qml"):
logger.error("Some of Deps' QML modules were installed in an incorrect location")
logger.error(fr" actual path: {DEPS_INSTALL_DIR}\lib\qml")
logger.error(fr" expected path: {DEPS_INSTALL_DIR}\qml")
exit(103)
# windeployqt
if useQt6Build:
# NOTE: we don't pass `--release` option to activate autodetection of the build type
# (which will effectively accept any kind of the binaries unless on MSVC)
verboseOption = ["-verbose", "2"] if useVerbosePackagingLog else []
run_subprocess_checked(
["windeployqt.exe", *QMLDIR_ARGS,
"-gui", "-core", "-core5compat", "-openglwidgets", "-svgwidgets", "-opengl",
"-concurrent", "-network", "-printsupport", "-svg",
# don't ship Windows Vista and Windows 11 styles if they happen to be present
"--skip-plugin-types", "styles",
"-xml", "-sql", "-qml", "-quick", "-quickwidgets", *verboseOption,
f"{pkg_root}\\bin\\krita.exe", f"{pkg_root}\\bin\\krita.dll"])
else:
verboseOption = ["-verbose", "2"] if useVerbosePackagingLog else []
run_subprocess_checked(
["windeployqt.exe", *QMLDIR_ARGS, "--release", "-gui", "-core", "-concurrent", "-network", "-printsupport", "-svg",
"-xml", "-sql", "-qml", "-quick", "-quickwidgets", *verboseOption,
f"{pkg_root}\\bin\\krita.exe", f"{pkg_root}\\bin\\krita.dll"])
# Qt5 version of windeployqt.exe cannot skip plugin types, so we
# should remove this style manually
if os.path.exists(fr"{pkg_root}\bin\styles\qwindowsvistastyle.dll"):
os.remove(fr"{pkg_root}\bin\styles\qwindowsvistastyle.dll")
# verify that broken Windows Vista and Windows11 styles are **not** deployed!
deployedStyles = glob.glob(fr"{pkg_root}\bin\styles\*")
if deployedStyles:
logger.error("Some custom style plugins have been deployed! Not supported by Krita!")
for style in deployedStyles:
logger.error(fr" {style}")
exit(104)
# ffmpeg
if os.path.exists(f"{DEPS_INSTALL_DIR}\\bin\\ffmpeg.exe"):
shutil.copy(f"{DEPS_INSTALL_DIR}\\bin\\ffmpeg.exe", f"{pkg_root}\\bin")
shutil.copy(f"{DEPS_INSTALL_DIR}\\bin\\ffprobe.exe", f"{pkg_root}\\bin")
if os.path.exists(f"{DEPS_INSTALL_DIR}\\bin\\ffmpeg_LICENSE.txt"):
shutil.copy(f"{DEPS_INSTALL_DIR}\\bin\\ffmpeg_LICENSE.txt",
f"{pkg_root}\\bin")
if os.path.exists(f"{DEPS_INSTALL_DIR}\\bin\\ffmpeg_README.txt"):
shutil.copy(f"{DEPS_INSTALL_DIR}\\bin\\ffmpeg_README.txt",
f"{pkg_root}\\bin")
# Copy embedded Python
MergeFolders.merge_folders(f"{DEPS_INSTALL_DIR}\\python", f"{pkg_root}\\python")
if os.path.exists(f"{pkg_root}\\python\\python.exe"):
os.remove(f"{pkg_root}\\python\\python.exe")
if os.path.exists(f"{pkg_root}\\python\\pythonw.exe"):
os.remove(f"{pkg_root}\\python\\pythonw.exe")
# Remove Python cache files
logger.info("")
logger.info("Deleting python cache...")
for d in os.walk(pkg_root):
pycache = f"{d[0]}\\__pycache__"
if os.path.isdir(pycache):
logger.debug(f"Deleting Python cache {pycache}")
shutil.rmtree(pycache)
if os.path.exists(f"{pkg_root}\\lib\\site-packages"):
logger.info(f"Deleting unnecessary Python packages")
for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\packaging*"):
shutil.rmtree(f)
for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\pip*"):
shutil.rmtree(f)
for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\ply*"):
shutil.rmtree(f)
for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\pyparsing*"):
shutil.rmtree(f)
for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\PyQt_builder*"):
shutil.rmtree(f)
for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\setuptools.pth"):
os.remove(f)
for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\setuptools*"):
shutil.rmtree(f)
for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\sip*"):
shutil.rmtree(f)
for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\toml*"):
shutil.rmtree(f)
for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\meson*"):
shutil.rmtree(f)
for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\easy-install.pth"):
os.remove(f)
if not CommonUtils.boolFromEnv(os.environ.get('KRITACI_SKIP_SPLIT_DEBUG', '0')):
run_subprocess_checked(
[sys.executable,
"-u",
os.path.join(os.path.dirname(__file__), "split-debug-info.py"),
"-p", "Windows",
pkg_root
])
if args.pre_zip_hook:
logger.info("Running pre-zip hook...")
if args.pre_zip_hook.endswith('.cmd'):
run_subprocess_checked(
["cmd", "/c", args.pre_zip_hook, f"{pkg_root}\\"])
elif args.pre_zip_hook.endswith('.py'):
run_subprocess_checked(
[sys.executable, "-u", args.pre_zip_hook, f"{pkg_root}\\"])
else:
logger.error("pre-zip hook has unknown format!")
sys.exit(102)
logger.info("")
logger.info("Packaging stripped binaries...")
run_subprocess_checked(
[os.environ["SEVENZIP_EXE"], "a", "-tzip",
f"{pkg_name}.zip", f"{pkg_root}\\", "-xr!*.debug"])
if not CommonUtils.boolFromEnv(os.environ.get('KRITACI_SKIP_DEBUG_PACKAGE', '0')):
logger.info("")
logger.info("Packaging debug info...")
# (note that the top-level package dir is not included)
run_subprocess_checked(
[os.environ["SEVENZIP_EXE"], "a", "-tzip",
f"{pkg_name}-dbg.zip", "-r", f"{pkg_root}\\*.debug"])
logger.info("")
logger.info(f"Krita packaged as {pkg_name}.zip")
if os.path.isfile(f"{pkg_name}-dbg.zip"):
logger.info(f"Debug info packaged as {pkg_name}-dbg.zip")
logger.info(f"Packaging dir is {pkg_root}")
logger.info("NOTE: Do not create installer with packaging dir. Extract from")
logger.info(f" {pkg_name}.zip instead")
logger.info("and do _not_ run krita inside the extracted directory because it will")
logger.info(" create extra unnecessary files.\n")
logger.info("Please remember to actually test the package before releasing it.\n")