#!/usr/bin/env python # SPDX-FileCopyrightText: 2021 L. E. Segovia # 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")