File indexing completed on 2024-05-12 04:28:29

0001 #!/usr/bin/env python
0002 
0003 # SPDX-FileCopyrightText: 2021 L. E. Segovia <amy@amyspark.me>
0004 # SPDX-License-Identifier: GPL-2.0-or-later
0005 
0006 # This Python script is meant to prepare a Krita package folder to be zipped or
0007 # to be a base for the installer.
0008 
0009 import argparse
0010 import glob
0011 import itertools
0012 import os
0013 import re
0014 import pathlib
0015 import shutil
0016 import subprocess
0017 import sys
0018 import warnings
0019 
0020 
0021 # Subroutines
0022 
0023 def choice(prompt="Is this ok?"):
0024     c = input(f"{prompt} [y/n] ")
0025     if c == "y":
0026         return True
0027     else:
0028         return False
0029 
0030 
0031 def find_on_path(variable, executable):
0032     os.environ[variable] = shutil.which(executable)
0033 
0034 
0035 def prompt_for_dir(prompt):
0036     user_input = input(f"{prompt} ")
0037     if not len(user_input):
0038         return None
0039     result = os.path.exists(user_input)
0040     if result is None:
0041         print("Input does not point to valid dir!")
0042         return prompt_for_dir(prompt)
0043     else:
0044         return os.path.realpath(user_input)
0045 
0046 
0047 print("Krita Windows packaging script (MSVC version)")
0048 
0049 
0050 # command-line args parsing
0051 parser = argparse.ArgumentParser()
0052 
0053 basic_options = parser.add_argument_group("Basic options")
0054 basic_options.add_argument("--no-interactive", action='store_true',
0055                            help="Run without interactive prompts. When not specified, the script will prompt for some of the parameters.")
0056 basic_options.add_argument(
0057     "--package-name", action='store', help="Specify the package name", required=True)
0058 
0059 path_options = parser.add_argument_group("Path options")
0060 path_options.add_argument("--src-dir", action='store',
0061                           help="Specify Krita source dir. If unspecified, this will be determined from the script location")
0062 path_options.add_argument("--deps-install-dir", action='store',
0063                           help="Specify deps install dir")
0064 path_options.add_argument("--krita-install-dir", action='store',
0065                           help="Specify Krita install dir")
0066 
0067 special_options = parser.add_argument_group("Special options")
0068 special_options.add_argument("--pre-zip-hook", action='store',
0069                              help="Specify a script to be called before packaging the zip archive, can be used to sign the binaries")
0070 
0071 args = parser.parse_args()
0072 
0073 # Check environment config
0074 
0075 if os.environ.get("SEVENZIP_EXE") is None:
0076     find_on_path("SEVENZIP_EXE", "7z.exe")
0077 if os.environ.get("SEVENZIP_EXE") is None:
0078     find_on_path("SEVENZIP_EXE", "7za.exe")
0079 if os.environ.get("SEVENZIP_EXE") is None:
0080     os.environ["SEVENZIP_EXE"] = f"{os.environ['ProgramFiles']}\\7-Zip\\7-Z.exe"
0081     if not os.path.isfile(os.environ["SEVENZIP_EXE"]):
0082         os.environ["SEVENZIP_EXE"] = "{}\\7-Zip\\7-Z.exe".format(
0083             os.environ["ProgramFiles(x86)"])
0084     if not os.path.isfile(os.environ["SEVENZIP_EXE"]):
0085         warnings.warn("7-Zip not found!")
0086         exit(102)
0087 print(f"7-Zip: {os.environ['SEVENZIP_EXE']}")
0088 
0089 HAVE_FXC_EXE = None
0090 
0091 # Windows SDK is needed for windeployqt to get d3dcompiler_xx.dll
0092 if os.environ.get("WindowsSdkDir") is None and os.environ.get("ProgramFiles(x86)") is not None:
0093     os.environ["WindowsSdkDir"] = "{}\\Windows Kits\\10".format(
0094         os.environ["ProgramFiles(x86)"])
0095 if os.path.isdir(os.environ["WindowsSdkDir"]):
0096     f = os.environ["WindowsSdkDir"]
0097     if os.path.isfile(f"{f}\\bin\\d\\fxc.exe"):
0098         os.environ["HAVE_FXC_EXE"] = True
0099     else:
0100         delims = glob.glob(f"{f}\\bin\\10.*")
0101         for f in delims:
0102             if os.path.isfile(f"{f}\\x64\\fxc.exe"):
0103                 HAVE_FXC_EXE = True
0104 if HAVE_FXC_EXE is None:
0105     os.environ["WindowsSdkDir"] = None
0106     warnings.warn("Windows SDK 10 with fxc.exe not found")
0107     warnings.warn(
0108         "If Qt was built with ANGLE (dynamic OpenGL) support, the package might not work properly on some systems!")
0109 else:
0110     print(
0111         f"Windows SDK 10 with fxc.exe found on {os.environ['WindowsSdkDir']}")
0112 
0113 KRITA_SRC_DIR = None
0114 
0115 if args.src_dir is not None:
0116     KRITA_SRC_DIR = args.src_dir
0117 
0118 if KRITA_SRC_DIR is None:
0119     _temp = sys.argv[0]
0120     if os.path.dirname(_temp).endswith("\\packaging\\windows"):
0121         _base = pathlib.PurePath(_temp)
0122         if os.path.isfile(f"{_base.parents[2]}\\CMakeLists.txt"):
0123             if os.path.isfile(f"{_base.parents[2]}\\3rdparty\\CMakeLists.txt"):
0124                 KRITA_SRC_DIR = os.path.realpath(_base.parents[2])
0125                 print("Script is running inside Krita source dir")
0126 
0127 if KRITA_SRC_DIR is None:
0128     if args.no_interactive:
0129         KRITA_SRC_DIR = prompt_for_dir("Provide path of Krita src dir")
0130     if KRITA_SRC_DIR is None:
0131         warnings.warn("ERROR: Krita src dir not found!")
0132         exit(102)
0133 print(f"Krita src: {KRITA_SRC_DIR}")
0134 
0135 DEPS_INSTALL_DIR = None
0136 
0137 if args.deps_install_dir is not None:
0138     DEPS_INSTALL_DIR = args.deps_install_dir
0139 if DEPS_INSTALL_DIR is None:
0140     DEPS_INSTALL_DIR = f"{os.getcwd()}\\i_deps"
0141     print(f"Using default deps install dir: {DEPS_INSTALL_DIR}")
0142     if not args.no_interactive:
0143         status = choice()
0144         if not status:
0145             DEPS_INSTALL_DIR = prompt_for_dir(
0146                 "Provide path of deps install dir")
0147     if DEPS_INSTALL_DIR is None:
0148         warnings.warn("ERROR: Deps install dir not set!")
0149         exit(102)
0150 print(f"Deps install dir: {DEPS_INSTALL_DIR}")
0151 
0152 KRITA_INSTALL_DIR = None
0153 
0154 if args.krita_install_dir is not None:
0155     KRITA_INSTALL_DIR = args.krita_install_dir
0156 if KRITA_INSTALL_DIR is None:
0157     KRITA_INSTALL_DIR = f"{os.getcwd()}\\i"
0158     print(f"Using default Krita install dir: {KRITA_INSTALL_DIR}")
0159     if not args.no_interactive:
0160         status = choice()
0161         if not status:
0162             KRITA_INSTALL_DIR = prompt_for_dir(
0163                 "Provide path of Krita install dir")
0164     if KRITA_INSTALL_DIR is None:
0165         warnings.warn("ERROR: Krita install dir not set!")
0166         exit(102)
0167 print(f"Krita install dir: {KRITA_INSTALL_DIR}")
0168 
0169 # Simple checking
0170 if not os.path.isdir(DEPS_INSTALL_DIR):
0171     warnings.warn("ERROR: Cannot find the deps install folder!")
0172     exit(1)
0173 if not os.path.isdir(KRITA_INSTALL_DIR):
0174     warnings.warn("ERROR: Cannot find the krita install folder!")
0175     exit(1)
0176 # Amyspark: paths with spaces are automagically handled by Python!
0177 
0178 pkg_name = args.package_name
0179 print(f"Package name is {pkg_name}")
0180 
0181 pkg_root = f"{os.getcwd()}\\{pkg_name}"
0182 print(f"Packaging dir is {pkg_root}\n")
0183 if os.path.isdir(pkg_root):
0184     warnings.warn(
0185         "ERROR: Packaging dir already exists! Please remove or rename it first.")
0186     exit(1)
0187 if os.path.isfile(f"{pkg_root}.zip"):
0188     warnings.warn(
0189         "ERROR: Packaging zip already exists! Please remove or rename it first.")
0190     exit(1)
0191 if os.path.isfile(f"{pkg_root}-dbg.zip"):
0192     warnings.warn(
0193         "ERROR: Packaging debug zip already exists! Please remove or rename it first.")
0194     exit(1)
0195 
0196 if not args.no_interactive:
0197     status = choice()
0198     if not status:
0199         exit(255)
0200     print("")
0201 
0202 # Initialize PATH
0203 os.environ["PATH"] = f"{DEPS_INSTALL_DIR}\\bin;{os.environ['PATH']}"
0204 
0205 print("\nThis is the packaging script for MSVC.\n")
0206 
0207 print("\nCreating base directories...")
0208 
0209 try:
0210     os.makedirs(f"{pkg_root}", exist_ok=True)
0211     os.makedirs(f"{pkg_root}\\bin", exist_ok=True)
0212     os.makedirs(f"{pkg_root}\\etc", exist_ok=True)
0213     os.makedirs(f"{pkg_root}\\lib", exist_ok=True)
0214     os.makedirs(f"{pkg_root}\\share", exist_ok=True)
0215 except:
0216     warnings.warn("ERROR: Cannot create packaging dir tree!")
0217     exit(1)
0218 
0219 print("\nCopying files...")
0220 # krita.exe
0221 shutil.copy(f"{KRITA_INSTALL_DIR}\\bin\\krita.exe", f"{pkg_root}\\bin\\")
0222 shutil.copy(f"{KRITA_INSTALL_DIR}\\bin\\krita.com", f"{pkg_root}\\bin\\")
0223 if os.path.isfile(f"{KRITA_INSTALL_DIR}\\bin\\krita.pdb"):
0224     shutil.copy(f"{KRITA_INSTALL_DIR}\\bin\\krita.pdb", f"{pkg_root}\\bin\\")
0225 # kritarunner.exe
0226 shutil.copy(f"{KRITA_INSTALL_DIR}\\bin\\kritarunner.exe", f"{pkg_root}\\bin\\")
0227 if os.path.isfile(f"{KRITA_INSTALL_DIR}\\bin\\kritarunner.pdb"):
0228     shutil.copy(f"{KRITA_INSTALL_DIR}\\bin\\kritarunner.pdb",
0229                 f"{pkg_root}\\bin\\")
0230 shutil.copy(f"{KRITA_INSTALL_DIR}\\bin\\kritarunner_com.com",
0231             f"{pkg_root}\\bin\\")
0232 
0233 if os.path.isfile(f"{KRITA_INSTALL_DIR}\\bin\\FreehandStrokeBenchmark.exe"):
0234     shutil.copy(f"{KRITA_INSTALL_DIR}\\bin\\FreehandStrokeBenchmark.exe", f"{pkg_root}\\bin\\")
0235     subprocess.run(["xcopy", "/S", "/Y", "/I",
0236                    f"{DEPS_INSTALL_DIR}\\bin\\data\\", f"{pkg_root}\\bin\\data\\"])
0237 
0238 # DLLs from bin/
0239 print("INFO: Copying all DLLs except Qt5 * from bin/")
0240 files = glob.glob(f"{KRITA_INSTALL_DIR}\\bin\\*.dll")
0241 pdbs = glob.glob(f"{KRITA_INSTALL_DIR}\\bin\\*.pdb")
0242 for f in itertools.chain(files, pdbs):
0243     if not os.path.basename(f).startswith("Qt5"):
0244         shutil.copy(f, f"{pkg_root}\\bin")
0245 files = glob.glob(f"{DEPS_INSTALL_DIR}\\bin\\*.dll")
0246 for f in files:
0247     pdb = f"{os.path.dirname(f)}\\{os.path.splitext(os.path.basename(f))[0]}.pdb"
0248     if not os.path.basename(f).startswith("Qt5"):
0249         shutil.copy(f, f"{pkg_root}\\bin")
0250         if os.path.isfile(pdb):
0251             shutil.copy(pdb, f"{pkg_root}\\bin")
0252 # symsrv.yes for Dr. Mingw
0253 shutil.copy(f"{DEPS_INSTALL_DIR}\\bin\\symsrv.yes", f"{pkg_root}\\bin")
0254 # KF5 plugins may be placed at different locations depending on how Qt is built
0255 subprocess.run(["xcopy", "/S", "/Y", "/I",
0256                f"{DEPS_INSTALL_DIR}\\lib\\plugins\\imageformats\\", f"{pkg_root}\\bin\\imageformats\\"])
0257 subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\plugins\\imageformats\\".format(
0258     DEPS_INSTALL_DIR), f"{pkg_root}\\bin\\imageformats\\"])
0259 subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\plugins\\kf5\\".format(
0260     DEPS_INSTALL_DIR), f"{pkg_root}\\bin\\kf5\\"])
0261 
0262 # Copy the sql drivers explicitly
0263 subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\plugins\\sqldrivers\\".format(
0264     DEPS_INSTALL_DIR), f"{pkg_root}\\bin\\sqldrivers"], check=True)
0265 
0266 # Qt Translations
0267 # it seems that windeployqt does these, but only * some * of these???
0268 os.makedirs(f"{pkg_root}\\bin\\translations", exist_ok=True)
0269 files = glob.glob(f"{DEPS_INSTALL_DIR}\\translations\\qt_*.qm")
0270 for f in files:
0271     # Exclude qt_help_*.qm
0272     if not os.path.basename(f).startswith("qt_help"):
0273         shutil.copy(f, f"{pkg_root}\\bin\\translations")
0274 
0275 # Krita plugins
0276 subprocess.run(["xcopy", "/Y", "{}\\lib\\kritaplugins\\*.dll".format(
0277     KRITA_INSTALL_DIR), f"{pkg_root}\\lib\\kritaplugins\\"], check=True)
0278 subprocess.run(["xcopy", "/Y", "{}\\lib\\kritaplugins\\*.pdb".format(
0279     KRITA_INSTALL_DIR), f"{pkg_root}\\lib\\kritaplugins\\"], check=True)
0280 if os.path.isdir(f"{DEPS_INSTALL_DIR}\\lib\\krita-python-libs"):
0281     subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\lib\\krita-python-libs".format(DEPS_INSTALL_DIR), f"{pkg_root}\\lib\\krita-python-libs"], check=True)
0282 if os.path.isdir(f"{KRITA_INSTALL_DIR}\\lib\\krita-python-libs"):
0283     subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\lib\\krita-python-libs".format(
0284         KRITA_INSTALL_DIR), f"{pkg_root}\\lib\\krita-python-libs"], check=True)
0285 if os.path.isdir(f"{DEPS_INSTALL_DIR}\\lib\\site-packages"):
0286     subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\lib\\site-packages".format(
0287         DEPS_INSTALL_DIR), f"{pkg_root}\\lib\\site-packages"], check=True)
0288 
0289 # MLT plugins and their data
0290 subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\lib\\mlt".format(
0291     DEPS_INSTALL_DIR), f"{pkg_root}\\lib\\mlt"], check=True)
0292 subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\share\\mlt".format(
0293     DEPS_INSTALL_DIR), f"{pkg_root}\\share\\mlt"], check=True)
0294 
0295 # Fontconfig
0296 subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\etc\\fonts".format(
0297     DEPS_INSTALL_DIR), f"{pkg_root}\\etc\\fonts"], check=True)
0298 
0299 # Share
0300 subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\share\\color".format(
0301     KRITA_INSTALL_DIR), f"{pkg_root}\\share\\color"], check=True)
0302 subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\share\\color-schemes".format(
0303     KRITA_INSTALL_DIR), f"{pkg_root}\\share\\color-schemes"], check=True)
0304 subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\share\\icons".format(
0305     KRITA_INSTALL_DIR), f"{pkg_root}\\share\\icons"], check=True)
0306 subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\share\\krita".format(
0307     KRITA_INSTALL_DIR), f"{pkg_root}\\share\\krita"])
0308 subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\share\\kritaplugins".format(
0309     KRITA_INSTALL_DIR), f"{pkg_root}\\share\\kritaplugins"], check=True)
0310 subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\share\\kf5".format(
0311     DEPS_INSTALL_DIR), f"{pkg_root}\\share\\kf5"], check=True)
0312 subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\share\\mime".format(
0313     DEPS_INSTALL_DIR), f"{pkg_root}\\share\\mime"], check=True)
0314 # Python libs are copied by share\krita above
0315 # Copy locale to bin
0316 subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\share\\locale".format(
0317     KRITA_INSTALL_DIR), f"{pkg_root}\\bin\\locale"])
0318 subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\share\\locale".format(
0319     DEPS_INSTALL_DIR), f"{pkg_root}\\bin\\locale"], check=True)
0320 
0321 # Copy shortcut link from source (can't create it dynamically)
0322 shutil.copy(f"{KRITA_SRC_DIR}\\packaging\\windows\\krita.lnk", pkg_root)
0323 shutil.copy(
0324     f"{KRITA_SRC_DIR}\\packaging\\windows\\krita-minimal.lnk", pkg_root)
0325 shutil.copy(
0326     f"{KRITA_SRC_DIR}\\packaging\\windows\\krita-animation.lnk", pkg_root)
0327 
0328 QMLDIR_ARGS = ["--qmldir", f"{DEPS_INSTALL_DIR}\\qml"]
0329 if os.path.isdir(f"{KRITA_INSTALL_DIR}\\lib\\qml"):
0330     subprocess.run(["xcopy", "/S", "/Y", "/I",
0331                    f"{KRITA_INSTALL_DIR}\\lib\\qml", f"{pkg_root}\\bin\\"], check=True)
0332     # This doesn't really seem to do anything
0333     QMLDIR_ARGS.extend(["--qmldir", f"{KRITA_INSTALL_DIR}\\lib\\qml"])
0334 
0335 # windeployqt
0336 subprocess.run(["windeployqt.exe", *QMLDIR_ARGS, "--release", "-gui", "-core", "-concurrent", "-network", "-printsupport", "-svg",
0337                "-xml", "-sql", "-qml", "-quick", "-quickwidgets", f"{pkg_root}\\bin\\krita.exe", f"{pkg_root}\\bin\\krita.dll"], check=True)
0338 
0339 # ffmpeg
0340 if os.path.exists(f"{DEPS_INSTALL_DIR}\\bin\\ffmpeg.exe"):
0341     shutil.copy(f"{DEPS_INSTALL_DIR}\\bin\\ffmpeg.exe", f"{pkg_root}\\bin")
0342     shutil.copy(f"{DEPS_INSTALL_DIR}\\bin\\ffprobe.exe", f"{pkg_root}\\bin")
0343     shutil.copy(f"{DEPS_INSTALL_DIR}\\bin\\ffmpeg_LICENSE.txt",
0344                 f"{pkg_root}\\bin")
0345     shutil.copy(f"{DEPS_INSTALL_DIR}\\bin\\ffmpeg_README.txt",
0346                 f"{pkg_root}\\bin")
0347 
0348 # Copy embedded Python
0349 subprocess.run(["xcopy", "/S", "/Y", "/I",
0350                f"{DEPS_INSTALL_DIR}\\python", f"{pkg_root}\\python"], check=True)
0351 if os.path.exists(f"{pkg_root}\\python\\python.exe"):
0352     os.remove(f"{pkg_root}\\python\\python.exe")
0353 if os.path.exists(f"{pkg_root}\\python\\pythonw.exe"):
0354     os.remove(f"{pkg_root}\\python\\pythonw.exe")
0355 
0356 # Remove Python cache files
0357 for d in os.walk(pkg_root):
0358     pycache = f"{d[0]}\\__pycache__"
0359     if os.path.isdir(pycache):
0360         print(f"Deleting Python cache {pycache}")
0361         shutil.rmtree(pycache)
0362 
0363 if os.path.exists(f"{pkg_root}\\lib\\site-packages"):
0364     print(f"Deleting unnecessary Python packages")
0365     for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\packaging*"):
0366         shutil.rmtree(f)
0367     for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\pip*"):
0368         shutil.rmtree(f)
0369     for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\ply*"):
0370         shutil.rmtree(f)
0371     for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\pyparsing*"):
0372         shutil.rmtree(f)
0373     for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\PyQt_builder*"):
0374         shutil.rmtree(f)
0375     for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\setuptools.pth"):
0376         os.remove(f)
0377     for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\setuptools*"):
0378         shutil.rmtree(f)
0379     for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\sip*"):
0380         shutil.rmtree(f)
0381     for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\toml*"):
0382         shutil.rmtree(f)
0383     for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\easy-install.pth"):
0384         os.remove(f)
0385 
0386 if args.pre_zip_hook:
0387     print("Running pre-zip hook...")
0388     subprocess.run(["cmd", "/c", args.pre_zip_hook,
0389                    f"{pkg_root}\\"], check=True)
0390 
0391 print("\nPackaging stripped binaries...")
0392 subprocess.run([os.environ["SEVENZIP_EXE"], "a", "-tzip",
0393                f"{pkg_name}.zip", f"{pkg_root}\\", "-xr!**\*.pdb"], check=True)
0394 print("--------\n")
0395 print("Packaging debug info...")
0396 # (note that the top-level package dir is not included)
0397 subprocess.run([os.environ["SEVENZIP_EXE"], "a", "-tzip",
0398                f"{pkg_name}-dbg.zip", "-r", f"{pkg_root}\\**\\*.pdb"], check=True)
0399 print("--------\n")
0400 
0401 print("\n")
0402 print(f"Krita packaged as {pkg_name}.zip")
0403 if os.path.isfile(f"{pkg_name}-dbg.zip"):
0404     print(f"Debug info packaged as {pkg_name}-dbg.zip")
0405 print(f"Packaging dir is {pkg_root}")
0406 print("NOTE: Do not create installer with packaging dir. Extract from")
0407 print(f"      {pkg_name}.zip instead")
0408 print("and do _not_ run krita inside the extracted directory because it will")
0409 print("       create extra unnecessary files.\n")
0410 print("Please remember to actually test the package before releasing it.\n")