Skip to content

Instantly share code, notes, and snippets.

@Zren
Created April 29, 2023 21:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Zren/c933d4ae1b3549b2a6df97ff9cfdcd3b to your computer and use it in GitHub Desktop.
Save Zren/c933d4ae1b3549b2a6df97ff9cfdcd3b to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
import argparse
import configparser
import datetime
import glob
import json
import logging
import os
import re
import subprocess
import shutil
import sys
sourceDir="package"
buildTag="-plasma5.18"
buildFilenameFormat="{packageName}-v{packageVersion}{buildTag}"
buildExt="plasmoid" # Renamed zip
#---
__version__ = '1'
logger = logging.getLogger('kpac')
logger.setLevel(logging.DEBUG)
logger.addHandler(logging.StreamHandler())
# KDE rc files differences:
# Keys are cAsE sensitive
# No spaces around the =
# [Sections can have spaces and : colons]
# Parses [Sub][Sections] as "Sub][Sections", but cannot have comments on the [Section] line
class KdeConfig(configparser.ConfigParser):
def __init__(self, filename):
super().__init__()
# Keep case sensitive keys
# http://stackoverflow.com/questions/19359556/configparser-reads-capital-keys-and-make-them-lower-case
self.optionxform = str
# Parse SubSections as "Sub][Sections"
self.SECTCRE = re.compile(r"\[(?P<header>.+?)]\w*$")
self.filename = filename
self.read(self.filename)
#---
def isCommandInstalled(name):
return shutil.which(name) is not None
class LineReplace:
def __init__(self, filepath):
self.filepath = filepath
self.lines = None
self.output = ''
def __enter__(self):
with open(self.filepath, 'r') as fin:
self.lines = self.readlines()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
with open(self.filepath, 'w') as fout:
fout.write(self.output)
def write(self, text):
self.output += text
#--- Parse metadata as global variables
jsonMetaFilepath = os.path.join(sourceDir, 'metadata.json')
desktopMetaFilepath = os.path.join(sourceDir, 'metadata.desktop')
if os.path.exists(jsonMetaFilepath):
# .json
with open(jsonMetaFilepath, 'r') as fin:
metadata = json.load(fin)
packageNamespace = metadata['KPlugin']['Id']
packageName = packageNamespace.split('.')[-1]
packageVersion = metadata['KPlugin']['Version']
packageAuthor = metadata['KPlugin']['Authors']['Name']
packageAuthorEmail = metadata['KPlugin']['Authors']['Email']
packageWebsite = metadata['KPlugin']['Website']
packageComment = metadata['KPlugin']['Description']
elif os.path.exists(desktopMetaFilepath):
# .desktop
metadata = KdeConfig(desktopMetaFilepath)
packageNamespace = metadata.get('Desktop Entry', 'X-KDE-PluginInfo-Name')
packageName = packageNamespace.split('.')[-1]
packageVersion = metadata.get('Desktop Entry', 'X-KDE-PluginInfo-Version')
packageAuthor = metadata.get('Desktop Entry', 'X-KDE-PluginInfo-Author')
packageAuthorEmail = metadata.get('Desktop Entry', 'X-KDE-PluginInfo-Email')
packageWebsite = metadata.get('Desktop Entry', 'X-KDE-PluginInfo-Website')
packageComment = metadata.get('Desktop Entry', 'Comment')
else:
print("Could not find metadata.json or metadata.desktop in '{}'".format(sourceDir))
sys.exit(1)
def printMetadata():
logger.info("Namespace: %s", packageNamespace)
logger.info("Name: %s", packageName)
logger.info("Version: %s", packageVersion)
logger.info("Author: %s", packageAuthor)
logger.info("AuthorEmail: %s", packageAuthorEmail)
logger.info("Website: %s", packageWebsite)
logger.info("Comment: %s", packageComment)
logger.info("")
#---
def kpac_install(args):
# kpackagetool5 -t Plasma/Applet -s package
p = subprocess.Popen([
'kpackagetool5',
'--type', 'Plasma/Applet',
'--show', packageNamespace,
], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p.wait()
# Not installed: "Error: Can't find plugin metadata: org.kde.plasma.name" is returncode=3
isAlreadyInstalled = p.returncode == 0
# logger.debug("isInstalledReturnCode: %s", p.returncode)
if isAlreadyInstalled:
# kpackagetool5 -t Plasma/Applet -u package
subprocess.call(['kpackagetool5',
'--type', 'Plasma/Applet',
'--upgrade', sourceDir,
])
if args.restart:
subprocess.call(['kstart5',
'--',
'plasmashell', '--replace',
])
else:
# kpackagetool5 -t Plasma/Applet -i package
subprocess.call(['kpackagetool5',
'--type', 'Plasma/Applet',
'--install', sourceDir,
])
def kpac_uninstall(args):
# kpackagetool5 -t Plasma/Applet -r package
subprocess.call(['kpackagetool5',
'--type', 'Plasma/Applet',
'--remove', sourceDir,
])
def kpac_i18n(args):
translateDir = os.path.join(sourceDir, 'translate')
translateRoot = '..' # package/tranlate/ => package/tranlate/../
bugAddress = packageWebsite
translationDomain = "plasma_applet_{}".format(packageNamespace)
if not isCommandInstalled('xgettext'):
print("[kpac-i18n] Error: xgettext command not found. Need to install gettext")
print("[kpac-i18n] Run 'sudo apt install gettext' on Kubuntu/KDE Neon first")
sys.exit(1)
potArgs = [
'--from-code=UTF-8',
'--width=200', # Don't wrap on short sentences
'--add-location=file', # Filename only, no line numbers
]
# TODO:
# Extract messages from metadata.desktop + metadata.json
# See Ki18n's extract-messages.sh for a full example:
# https://invent.kde.org/sysadmin/l10n-scripty/-/blob/master/extract-messages.sh#L25
# The -kN_ and -kaliasLocale keywords are mentioned in the Outside_KDE_repositories wiki.
# We don't need -kN_ since we don't use intltool-extract but might as well keep it.
# I have no idea what -kaliasLocale is used for. Googling aliasLocale found only listed kde1 code.
# We don't need to parse -ki18nd since that'll extract messages from other domains.
infilesPath = os.path.join(translateDir, 'infiles.list')
with open(infilesPath, 'w') as fout:
findProc = subprocess.Popen([
'find',
translateRoot,
'-name', '*.cpp',
'-o', '-name', '*.h',
'-o', '-name', '*.c',
'-o', '-name', '*.qml',
'-o', '-name', '*.js',
], stdout=subprocess.PIPE, cwd=translateDir)
sortProc = subprocess.call([
'sort'
], stdin=findProc.stdout, stdout=fout)
findProc.wait()
oldTemplateFilename = os.path.join('template.pot')
newTemplateFilename = os.path.join('template.pot.new')
oldTemplatePath = os.path.join(translateDir, oldTemplateFilename)
newTemplatePath = os.path.join(translateDir, newTemplateFilename)
potArgs += [
# '--files-from', infilesPath,
'--files-from', 'infiles.list',
'-C', '-kde',
'-ci18n',
'-ki18n:1',
'-ki18nc:1c,2',
'-ki18np:1,2',
'-ki18ncp:1c,2,3',
'-kki18n:1',
'-kki18nc:1c,2',
'-kki18np:1,2',
'-kki18ncp:1c,2,3',
'-kxi18n:1',
'-kxi18nc:1c,2',
'-kxi18np:1,2',
'-kxi18ncp:1c,2,3',
'-kkxi18n:1',
'-kkxi18nc:1c,2',
'-kkxi18np:1,2',
'-kkxi18ncp:1c,2,3',
'-kI18N_NOOP:1',
'-kI18NC_NOOP:1c,2',
'-kI18N_NOOP2:1c,2',
'-kI18N_NOOP2_NOSTRIP:1c,2',
'-ktr2i18n:1',
'-ktr2xi18n:1',
'-kN_:1',
'-kaliasLocale',
'--package-name', packageName,
'--msgid-bugs-address', bugAddress,
]
subprocess.call(['xgettext'] + potArgs + [
'-D', translateRoot,
'-D', '.', # cwd should be translateDir
'-o', newTemplateFilename,
], cwd=translateDir)
if not os.path.exists(newTemplatePath):
# Error generating template.pot.new
print('template.pot.new does not exist')
sys.exit(1)
# Replace gettext placeholders
with LineReplace(newTemplatePath) as rep:
for line in rep.lines:
line = line.replace(
"Content-Type: text\/plain; charset=CHARSET",
"Content-Type: text\/plain; charset=UTF-8",
)
line = line.replace(
"# SOME DESCRIPTIVE TITLE.",
"# Translation of {} in LANGUAGE".format(packageName),
)
line = line.replace(
"# Copyright (C) YEAR THE PACKAGE\'S COPYRIGHT HOLDER",
"# Copyright (C) {}".format(datetime.date.today().year),
)
rep.write(line)
if os.path.exists(oldTemplatePath):
with open()
newPotDate = ''
oldPotDate = ''
with LineReplace(newTemplatePath) as rep:
for line in rep.lines:
line = line.replace(
"Content-Type: text\/plain; charset=CHARSET",
"Content-Type: text\/plain; charset=UTF-8",
)
rep.write(line)
else:
# template.pot didn't already exist
os.rename(newTemplatePath, oldTemplatePath)
def kpac_localetest(args):
# https://stackoverflow.com/questions/3191664/list-of-all-locales-and-their-short-codes/28357857#28357857
langArr = [
["af_ZA", "af", "Afrikaans (South Africa)"],
["ak_GH", "ak", "Akan (Ghana)"],
["am_ET", "am", "Amharic (Ethiopia)"],
["ar_EG", "ar", "Arabic (Egypt)"],
["as_IN", "as", "Assamese (India)"],
["az_AZ", "az", "Azerbaijani (Azerbaijan)"],
["be_BY", "be", "Belarusian (Belarus)"],
["bem_ZM", "bem", "Bemba (Zambia)"],
["bg_BG", "bg", "Bulgarian (Bulgaria)"],
["bo_IN", "bo", "Tibetan (India)"],
["bs_BA", "bs", "Bosnian (Bosnia and Herzegovina)"],
["ca_ES", "ca", "Catalan (Spain)"],
["chr_US", "ch", "Cherokee (United States)"],
["cs_CZ", "cs", "Czech (Czech Republic)"],
["cy_GB", "cy", "Welsh (United Kingdom)"],
["da_DK", "da", "Danish (Denmark)"],
["de_DE", "de", "German (Germany)"],
["el_GR", "el", "Greek (Greece)"],
["es_MX", "es", "Spanish (Mexico)"],
["et_EE", "et", "Estonian (Estonia)"],
["eu_ES", "eu", "Basque (Spain)"],
["fa_IR", "fa", "Persian (Iran)"],
["ff_SN", "ff", "Fulah (Senegal)"],
["fi_FI", "fi", "Finnish (Finland)"],
["fo_FO", "fo", "Faroese (Faroe Islands)"],
["fr_CA", "fr", "French (Canada)"],
["ga_IE", "ga", "Irish (Ireland)"],
["gl_ES", "gl", "Galician (Spain)"],
["gu_IN", "gu", "Gujarati (India)"],
["gv_GB", "gv", "Manx (United Kingdom)"],
["ha_NG", "ha", "Hausa (Nigeria)"],
["he_IL", "he", "Hebrew (Israel)"],
["hi_IN", "hi", "Hindi (India)"],
["hr_HR", "hr", "Croatian (Croatia)"],
["hu_HU", "hu", "Hungarian (Hungary)"],
["hy_AM", "hy", "Armenian (Armenia)"],
["id_ID", "id", "Indonesian (Indonesia)"],
["ig_NG", "ig", "Igbo (Nigeria)"],
["is_IS", "is", "Icelandic (Iceland)"],
["it_IT", "it", "Italian (Italy)"],
["ja_JP", "ja", "Japanese (Japan)"],
["ka_GE", "ka", "Georgian (Georgia)"],
["kk_KZ", "kk", "Kazakh (Kazakhstan)"],
["kl_GL", "kl", "Kalaallisut (Greenland)"],
["km_KH", "km", "Khmer (Cambodia)"],
["kn_IN", "kn", "Kannada (India)"],
["ko_KR", "ko", "Korean (South Korea)"],
["ko_KR", "ko", "Korean (South Korea)"],
["lg_UG", "lg", "Ganda (Uganda)"],
["lt_LT", "lt", "Lithuanian (Lithuania)"],
["lv_LV", "lv", "Latvian (Latvia)"],
["mg_MG", "mg", "Malagasy (Madagascar)"],
["mk_MK", "mk", "Macedonian (Macedonia)"],
["ml_IN", "ml", "Malayalam (India)"],
["mr_IN", "mr", "Marathi (India)"],
["ms_MY", "ms", "Malay (Malaysia)"],
["mt_MT", "mt", "Maltese (Malta)"],
["my_MM", "my", "Burmese (Myanmar [Burma])"],
["nb_NO", "nb", "Norwegian Bokmål (Norway)"],
["ne_NP", "ne", "Nepali (Nepal)"],
["nl_NL", "nl", "Dutch (Netherlands)"],
["nn_NO", "nn", "Norwegian Nynorsk (Norway)"],
["om_ET", "om", "Oromo (Ethiopia)"],
["or_IN", "or", "Oriya (India)"],
["pa_PK", "pa", "Punjabi (Pakistan)"],
["pl_PL", "pl", "Polish (Poland)"],
["ps_AF", "ps", "Pashto (Afghanistan)"],
["pt_BR", "pt", "Portuguese (Brazil)"],
["ro_RO", "ro", "Romanian (Romania)"],
["ru_RU", "ru", "Russian (Russia)"],
["rw_RW", "rw", "Kinyarwanda (Rwanda)"],
["si_LK", "si", "Sinhala (Sri Lanka)"],
["sk_SK", "sk", "Slovak (Slovakia)"],
["sl_SI", "sl", "Slovenian (Slovenia)"],
["so_SO", "so", "Somali (Somalia)"],
["sq_AL", "sq", "Albanian (Albania)"],
["sr_RS", "sr", "Serbian (Serbia)"],
["sv_SE", "sv", "Swedish (Sweden)"],
["sw_KE", "sw", "Swahili (Kenya)"],
["ta_IN", "ta", "Tamil (India)"],
["te_IN", "te", "Telugu (India)"],
["th_TH", "th", "Thai (Thailand)"],
["ti_ER", "ti", "Tigrinya (Eritrea)"],
["to_TO", "to", "Tonga (Tonga)"],
["tr_TR", "tr", "Turkish (Turkey)"],
["uk_UA", "uk", "Ukrainian (Ukraine)"],
["ur_IN", "ur", "Urdu (India)"],
["uz_UZ", "uz", "Uzbek (Uzbekistan)"],
["vi_VN", "vi", "Vietnamese (Vietnam)"],
["yo_NG", "yo", "Yoruba (Nigeria)"],
["yo_NG", "yo", "Yoruba (Nigeria)"],
["yue_HK", "yu", "Cantonese (Hong Kong)"],
["zh_CN", "zh", "Chinese (China)"],
["zu_ZA", "zu", "Zulu (South Africa)"],
]
def getLang(langcode):
for lang in langArr:
if lang[1] == langcode:
return lang
raise Exception("localetest doesn't recognize the language " + langcode + '.')
if ":" in args.langcode:
l1, l2, _ = args.langcode.split(":")
else:
lang = getLang(args.langcode)
l1, l2, l3 = lang
language = l1 + ':' + l2
langUtf8 = l1 + '.UTF-8'
widgetEnv = os.environ.copy()
widgetEnv['LANGUAGE'] = language
widgetEnv['LANG'] = langUtf8
widgetEnv['LC_TIME'] = langUtf8
logger.info("LANGUAGE=%s", language)
logger.info("LANG=%s", langUtf8)
logger.info("LC_TIME=%s", langUtf8)
# Build .mo files
# TODO
subprocess.call([
'plasmoidviewer',
'-a', sourceDir,
'-l', 'topedge',
'-f', 'horizontal',
'-x', '0', '-y', '0',
], env=widgetEnv)
def kpac_build(args):
buildFilename = buildFilenameFormat.format(
packageName=packageName,
packageVersion=packageVersion,
buildTag=buildTag,
)
logger.info("buildTag: %s", buildTag)
logger.info("buildFilenameFormat: %s", buildFilenameFormat)
logger.info("buildExt: .%s", buildExt)
logger.info("buildFilename: %s", buildFilename + '.' + buildExt)
logger.info("")
# Cleanup
oldPackageList = glob.glob('*.' + buildExt)
for oldPackage in oldPackageList:
print("DELETED: {}".format(oldPackage))
if not args.dryrun:
os.remove(oldPackage)
# Zip
zipLogger = logging.getLogger('build')
zipLogger.setLevel(logging.DEBUG)
zipLogger.addHandler(logging.StreamHandler())
shutil.make_archive(buildFilename, 'zip', sourceDir,
dry_run=args.dryrun,
logger=zipLogger,
)
if not args.dryrun:
os.rename(buildFilename + '.zip', buildFilename + '.' + buildExt)
# Checksums
# echo "[plasmoid] md5: $(md5sum $filename | awk '{ print $1 }')"
# echo "[plasmoid] sha256: $(sha256sum $filename | awk '{ print $1 }')"
def main():
parser = argparse.ArgumentParser(
prog='kpac',
description='v{} - Misc tools for a plasma widget like kpackages.'.format(__version__),
)
subparsers = parser.add_subparsers()
parser_install = subparsers.add_parser('install', help='kpac install')
parser_install.set_defaults(func=kpac_install)
parser_install.add_argument('--no-restart', dest='restart', action='store_false', default=True, help='Do not restart plasmashell after upgrading')
parser_uninstall = subparsers.add_parser('uninstall', help='kpac uninstall')
parser_uninstall.set_defaults(func=kpac_uninstall)
parser_mergei18n = subparsers.add_parser('i18n', help='kpac i18n (Run xgettext translation tools)')
parser_mergei18n.set_defaults(func=kpac_i18n)
parser_localetest = subparsers.add_parser('localetest', help='kpac localetest [langcode] (Eg: fr=French)')
parser_localetest.set_defaults(func=kpac_localetest)
parser_localetest.add_argument('langcode', help='Can use just "ar" for the default Arabic locale, or specify a specific locale with "ar_EG:ar".')
parser_build = subparsers.add_parser('build', help='kpac build')
parser_build.set_defaults(func=kpac_build)
parser_build.add_argument('--dryrun', action='store_true', default=False)
printMetadata()
args = parser.parse_args()
if 'func' in args:
try:
args.func(args)
except KeyboardInterrupt:
pass
else:
parser.print_help()
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment