File indexing completed on 2024-05-12 16:02:32

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}\\lib", exist_ok=True)
0213     os.makedirs(f"{pkg_root}\\share", exist_ok=True)
0214 except:
0215     warnings.warn("ERROR: Cannot create packaging dir tree!")
0216     exit(1)
0217 
0218 print("\nCopying files...")
0219 # krita.exe
0220 shutil.copy(f"{KRITA_INSTALL_DIR}\\bin\\krita.exe", f"{pkg_root}\\bin\\")
0221 shutil.copy(f"{KRITA_INSTALL_DIR}\\bin\\krita.com", f"{pkg_root}\\bin\\")
0222 shutil.copy(f"{KRITA_INSTALL_DIR}\\bin\\krita.pdb", f"{pkg_root}\\bin\\")
0223 # kritarunner.exe
0224 shutil.copy(f"{KRITA_INSTALL_DIR}\\bin\\kritarunner.exe", f"{pkg_root}\\bin\\")
0225 shutil.copy(f"{KRITA_INSTALL_DIR}\\bin\\kritarunner.pdb",
0226             f"{pkg_root}\\bin\\")
0227 shutil.copy(f"{KRITA_INSTALL_DIR}\\bin\\kritarunner_com.com",
0228             f"{pkg_root}\\bin\\")
0229 
0230 if os.path.isfile(f"{KRITA_INSTALL_DIR}\\bin\\FreehandStrokeBenchmark.exe"):
0231     shutil.copy(f"{KRITA_INSTALL_DIR}\\bin\\FreehandStrokeBenchmark.exe", f"{pkg_root}\\bin\\")
0232     subprocess.run(["xcopy", "/S", "/Y", "/I",
0233                    f"{DEPS_INSTALL_DIR}\\bin\\data\\", f"{pkg_root}\\bin\\data\\"])
0234 
0235 # DLLs from bin/
0236 print("INFO: Copying all DLLs except Qt5 * from bin/")
0237 files = glob.glob(f"{KRITA_INSTALL_DIR}\\bin\\*.dll")
0238 pdbs = glob.glob(f"{KRITA_INSTALL_DIR}\\bin\\*.pdb")
0239 for f in itertools.chain(files, pdbs):
0240     if not os.path.basename(f).startswith("Qt5"):
0241         shutil.copy(f, f"{pkg_root}\\bin")
0242 files = glob.glob(f"{DEPS_INSTALL_DIR}\\bin\\*.dll")
0243 for f in files:
0244     pdb = f"{os.path.dirname(f)}\\{os.path.splitext(os.path.basename(f))[0]}.pdb"
0245     if not os.path.basename(f).startswith("Qt5"):
0246         shutil.copy(f, f"{pkg_root}\\bin")
0247         if os.path.isfile(pdb):
0248             shutil.copy(pdb, f"{pkg_root}\\bin")
0249 # symsrv.yes for Dr. Mingw
0250 shutil.copy(f"{DEPS_INSTALL_DIR}\\bin\\symsrv.yes", f"{pkg_root}\\bin")
0251 # KF5 plugins may be placed at different locations depending on how Qt is built
0252 subprocess.run(["xcopy", "/S", "/Y", "/I",
0253                f"{DEPS_INSTALL_DIR}\\lib\\plugins\\imageformats\\", f"{pkg_root}\\bin\\imageformats\\"])
0254 subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\plugins\\imageformats\\".format(
0255     DEPS_INSTALL_DIR), f"{pkg_root}\\bin\\imageformats\\"])
0256 subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\plugins\\kf5\\".format(
0257     DEPS_INSTALL_DIR), f"{pkg_root}\\bin\\kf5\\"])
0258 
0259 # Copy the sql drivers explicitly
0260 subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\plugins\\sqldrivers\\".format(
0261     DEPS_INSTALL_DIR), f"{pkg_root}\\bin\\sqldrivers"], check=True)
0262 
0263 # Qt Translations
0264 # it seems that windeployqt does these, but only * some * of these???
0265 os.makedirs(f"{pkg_root}\\bin\\translations", exist_ok=True)
0266 files = glob.glob(f"{DEPS_INSTALL_DIR}\\translations\\qt_*.qm")
0267 for f in files:
0268     # Exclude qt_help_*.qm
0269     if not os.path.basename(f).startswith("qt_help"):
0270         shutil.copy(f, f"{pkg_root}\\bin\\translations")
0271 
0272 # Krita plugins
0273 subprocess.run(["xcopy", "/Y", "{}\\lib\\kritaplugins\\*.dll".format(
0274     KRITA_INSTALL_DIR), f"{pkg_root}\\lib\\kritaplugins\\"], check=True)
0275 subprocess.run(["xcopy", "/Y", "{}\\lib\\kritaplugins\\*.pdb".format(
0276     KRITA_INSTALL_DIR), f"{pkg_root}\\lib\\kritaplugins\\"], check=True)
0277 if os.path.isdir(f"{DEPS_INSTALL_DIR}\\lib\\krita-python-libs"):
0278     subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\lib\\krita-python-libs".format(DEPS_INSTALL_DIR), f"{pkg_root}\\lib\\krita-python-libs"], check=True)
0279 if os.path.isdir(f"{KRITA_INSTALL_DIR}\\lib\\krita-python-libs"):
0280     subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\lib\\krita-python-libs".format(
0281         KRITA_INSTALL_DIR), f"{pkg_root}\\lib\\krita-python-libs"], check=True)
0282 if os.path.isdir(f"{DEPS_INSTALL_DIR}\\lib\\site-packages"):
0283     subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\lib\\site-packages".format(
0284         DEPS_INSTALL_DIR), f"{pkg_root}\\lib\\site-packages"], check=True)
0285 
0286 # Share
0287 subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\share\\color".format(
0288     KRITA_INSTALL_DIR), f"{pkg_root}\\share\\color"], check=True)
0289 subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\share\\color-schemes".format(
0290     KRITA_INSTALL_DIR), f"{pkg_root}\\share\\color-schemes"], check=True)
0291 subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\share\\icons".format(
0292     KRITA_INSTALL_DIR), f"{pkg_root}\\share\\icons"], check=True)
0293 subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\share\\krita".format(
0294     KRITA_INSTALL_DIR), f"{pkg_root}\\share\\krita"])
0295 subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\share\\kritaplugins".format(
0296     KRITA_INSTALL_DIR), f"{pkg_root}\\share\\kritaplugins"], check=True)
0297 subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\share\\kf5".format(
0298     DEPS_INSTALL_DIR), f"{pkg_root}\\share\\kf5"], check=True)
0299 subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\share\\mime".format(
0300     DEPS_INSTALL_DIR), f"{pkg_root}\\share\\mime"], check=True)
0301 # Python libs are copied by share\krita above
0302 # Copy locale to bin
0303 subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\share\\locale".format(
0304     KRITA_INSTALL_DIR), f"{pkg_root}\\bin\\locale"])
0305 subprocess.run(["xcopy", "/S", "/Y", "/I", "{}\\share\\locale".format(
0306     DEPS_INSTALL_DIR), f"{pkg_root}\\bin\\locale"], check=True)
0307 
0308 # Copy shortcut link from source (can't create it dynamically)
0309 shutil.copy(f"{KRITA_SRC_DIR}\\packaging\\windows\\krita.lnk", pkg_root)
0310 shutil.copy(
0311     f"{KRITA_SRC_DIR}\\packaging\\windows\\krita-minimal.lnk", pkg_root)
0312 shutil.copy(
0313     f"{KRITA_SRC_DIR}\\packaging\\windows\\krita-animation.lnk", pkg_root)
0314 
0315 QMLDIR_ARGS = ["--qmldir", f"{DEPS_INSTALL_DIR}\\qml"]
0316 if os.path.isdir(f"{KRITA_INSTALL_DIR}\\lib\\qml"):
0317     subprocess.run(["xcopy", "/S", "/Y", "/I",
0318                    f"{KRITA_INSTALL_DIR}\\lib\\qml", f"{pkg_root}\\bin\\"], check=True)
0319     # This doesn't really seem to do anything
0320     QMLDIR_ARGS.extend(["--qmldir", f"{KRITA_INSTALL_DIR}\\lib\\qml"])
0321 
0322 # windeployqt
0323 subprocess.run(["windeployqt.exe", *QMLDIR_ARGS, "--release", "-gui", "-core", "-concurrent", "-network", "-printsupport", "-svg",
0324                "-xml", "-sql", "-multimedia", "-qml", "-quick", "-quickwidgets", f"{pkg_root}\\bin\\krita.exe", f"{pkg_root}\\bin\\krita.dll"], check=True)
0325 
0326 # ffmpeg
0327 if os.path.exists(f"{DEPS_INSTALL_DIR}\\bin\\ffmpeg.exe"):
0328     shutil.copy(f"{DEPS_INSTALL_DIR}\\bin\\ffmpeg.exe", f"{pkg_root}\\bin")
0329     shutil.copy(f"{DEPS_INSTALL_DIR}\\bin\\ffmpeg_LICENSE.txt",
0330                 f"{pkg_root}\\bin")
0331     shutil.copy(f"{DEPS_INSTALL_DIR}\\bin\\ffmpeg_README.txt",
0332                 f"{pkg_root}\\bin")
0333 
0334 # Copy embedded Python
0335 subprocess.run(["xcopy", "/S", "/Y", "/I",
0336                f"{DEPS_INSTALL_DIR}\\python", f"{pkg_root}\\python"], check=True)
0337 if os.path.exists(f"{pkg_root}\\python\\python.exe"):
0338     os.remove(f"{pkg_root}\\python\\python.exe")
0339 if os.path.exists(f"{pkg_root}\\python\\pythonw.exe"):
0340     os.remove(f"{pkg_root}\\python\\pythonw.exe")
0341 
0342 # Remove Python cache files
0343 for d in os.walk(pkg_root):
0344     pycache = f"{d[0]}\\__pycache__"
0345     if os.path.isdir(pycache):
0346         print(f"Deleting Python cache {pycache}")
0347         shutil.rmtree(pycache)
0348 
0349 if os.path.exists(f"{pkg_root}\\lib\\site-packages"):
0350     print(f"Deleting unnecessary Python packages")
0351     for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\packaging*"):
0352         shutil.rmtree(f)
0353     for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\pip*"):
0354         shutil.rmtree(f)
0355     for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\pyparsing*"):
0356         shutil.rmtree(f)
0357     for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\PyQt_builder*"):
0358         shutil.rmtree(f)
0359     for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\setuptools*"):
0360         shutil.rmtree(f)
0361     for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\sip*"):
0362         shutil.rmtree(f)
0363     for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\toml*"):
0364         shutil.rmtree(f)
0365     for f in glob.glob(f"{pkg_root}\\lib\\site-packages\\easy-install.pth"):
0366         os.remove(f)
0367 
0368 if args.pre_zip_hook:
0369     print("Running pre-zip hook...")
0370     subprocess.run(["cmd", "/c", args.pre_zip_hook,
0371                    f"{pkg_root}\\"], check=True)
0372 
0373 print("\nPackaging stripped binaries...")
0374 subprocess.run([os.environ["SEVENZIP_EXE"], "a", "-tzip",
0375                f"{pkg_name}.zip", f"{pkg_root}\\", "-xr!**\*.pdb"], check=True)
0376 print("--------\n")
0377 print("Packaging debug info...")
0378 # (note that the top-level package dir is not included)
0379 subprocess.run([os.environ["SEVENZIP_EXE"], "a", "-tzip",
0380                f"{pkg_name}-dbg.zip", "-r", f"{pkg_root}\\**\\*.pdb"], check=True)
0381 print("--------\n")
0382 
0383 print("\n")
0384 print(f"Krita packaged as {pkg_name}.zip")
0385 if os.path.isfile(f"{pkg_name}-dbg.zip"):
0386     print(f"Debug info packaged as {pkg_name}-dbg.zip")
0387 print(f"Packaging dir is {pkg_root}")
0388 print("NOTE: Do not create installer with packaging dir. Extract from")
0389 print(f"      {pkg_name}.zip instead")
0390 print("and do _not_ run krita inside the extracted directory because it will")
0391 print("       create extra unnecessary files.\n")
0392 print("Please remember to actually test the package before releasing it.\n")