mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-10 10:55:33 +00:00
Installing into the shared area (`/usr/local/share`) requires `root` permissions, so in order to install into the shared area the user has to call `km-package-install` with `sudo`. But we also want to add the keyboard to the user's list of available keyboards so that he can switch to it. That needs to be done in the user context. However, when `sudo` ran the command in the user context, the environment variables to connect to the user's session dbus were not set. This change fixes this by passing `DBUS_SESSION_BUS_ADDRESS` along with the `sudo -u <user>` call. Since `root` doesn't have this variable set (or at least not pointing to the user's dbus), we construct the dbus address based on the user's uid. On newer Ubuntu versions (>=25.04) we also have to set `XDG_RUNTIME_DIR`, at least when running the autopkgtests. Fixes: #14911
355 lines
15 KiB
Python
Executable file
355 lines
15 KiB
Python
Executable file
#!/usr/bin/python3
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import packaging.version
|
|
import zipfile
|
|
from enum import Enum
|
|
from shutil import rmtree
|
|
|
|
from keyman_config import _, __version__, secure_lookup
|
|
from keyman_config.canonical_language_code_utils import CanonicalLanguageCodeUtils
|
|
from keyman_config.convertico import checkandsaveico, extractico
|
|
from keyman_config.custom_keyboards import CustomKeyboards
|
|
from keyman_config.dbus_util import keyboard_list_changed
|
|
from keyman_config.fcitx_util import is_fcitx_running
|
|
from keyman_config.get_kmp import (InstallLocation, get_keyboard_data,
|
|
get_keyboard_dir, get_keyman_doc_dir,
|
|
get_keyman_font_dir)
|
|
from keyman_config.gnome_keyboards_util import (GnomeKeyboardsUtil,
|
|
get_ibus_keyboard_id,
|
|
is_gnome_desktop)
|
|
from keyman_config.ibus_util import get_ibus_bus, install_to_ibus, restart_ibus
|
|
from keyman_config.kmpmetadata import KMFileTypes, get_metadata
|
|
from keyman_config.kvk2ldml import convert_kvk_to_ldml, output_ldml
|
|
from keyman_config.sentry_handling import SentryErrorHandling
|
|
|
|
# TODO userdir install
|
|
# special processing for kmn if needed
|
|
# TODO optionally standardise throughout on variable names
|
|
# packageID for kmps and keyboardID for keyboards
|
|
# see https://docs.google.com/\
|
|
# document/d/1sj7W6pCiN-_iRss5iRdib1aHaSTmYoLIueQSKJeNy8Q/edit#heading=h.mq0rc28mf031
|
|
|
|
|
|
class InstallStatus(Enum):
|
|
Continue = 0
|
|
Warning = 1
|
|
Abort = 2
|
|
|
|
|
|
class InstallError(Exception):
|
|
"""Exception raised for errors in KMP installation.
|
|
|
|
Attributes:
|
|
status -- InstallStatus for what to do when the error occurrs
|
|
message -- explanation of the error
|
|
"""
|
|
|
|
def __init__(self, status, message):
|
|
self.status = status
|
|
self.message = message
|
|
|
|
|
|
class InstallKmp():
|
|
def __init__(self):
|
|
self.packageID = ''
|
|
self.packageDir = ''
|
|
self.kmpdocdir = ''
|
|
self.kmpfontdir = ''
|
|
|
|
def _check_keyman_dir(self, basedir, error_message):
|
|
# check if keyman subdir exists
|
|
keyman_dir = os.path.join(basedir, "keyman")
|
|
if os.path.isdir(keyman_dir):
|
|
# Check for write access of keyman dir to be able to create subdir
|
|
if not os.access(keyman_dir, os.X_OK | os.W_OK):
|
|
raise InstallError(InstallStatus.Abort, error_message)
|
|
elif os.access(basedir, os.X_OK | os.W_OK):
|
|
os.mkdir(keyman_dir)
|
|
else:
|
|
raise InstallError(InstallStatus.Abort, error_message)
|
|
|
|
def _extract_package_id(self, inputfile):
|
|
packageID, ext = os.path.splitext(os.path.basename(inputfile))
|
|
return packageID.lower()
|
|
|
|
def install_kmp_shared(self, inputfile, language=None):
|
|
"""
|
|
Install a kmp file to /usr/local/share/keyman
|
|
|
|
Args:
|
|
inputfile (str): path to kmp file
|
|
"""
|
|
self._check_keyman_dir(
|
|
'/usr/local/share',
|
|
_("You do not have permissions to install the keyboard files to the shared area "
|
|
"/usr/local/share/keyman"))
|
|
self._check_keyman_dir(
|
|
'/usr/local/share/doc',
|
|
_("You do not have permissions to install the documentation to the shared "
|
|
"documentation area /usr/local/share/doc/keyman"))
|
|
self._check_keyman_dir(
|
|
'/usr/local/share/fonts',
|
|
_("You do not have permissions to install the font files to the shared font area "
|
|
"/usr/local/share/fonts"))
|
|
|
|
return self._install_kmp(inputfile, language, InstallLocation.Shared)
|
|
|
|
def install_kmp_user(self, inputfile, language=None):
|
|
return self._install_kmp(inputfile, language, InstallLocation.User)
|
|
|
|
def _install_kmp(self, inputfile, language, area):
|
|
self.packageID = self._extract_package_id(inputfile)
|
|
self.packageDir = get_keyboard_dir(area, self.packageID)
|
|
self.kmpdocdir = get_keyman_doc_dir(area, self.packageID)
|
|
self.kmpfontdir = get_keyman_font_dir(area, self.packageID)
|
|
|
|
sentry = SentryErrorHandling()
|
|
sentry.add_breadcrumb(category='install', message=f'Installing kmp: {self.packageID} from "{inputfile}" for lang: {language}; area: {"OS" if area == 1 else "Shared" if area == 2 else "User" if area == 3 else "Unknown"}')
|
|
|
|
if not os.path.isfile(inputfile):
|
|
message = _("File {kmpfile} doesn't exist").format(kmpfile=inputfile)
|
|
logging.error("install_kmp.py: %s", message)
|
|
raise InstallError(InstallStatus.Abort, message)
|
|
|
|
if not self._safeMakeDirs(self.packageDir):
|
|
return
|
|
|
|
extract_kmp(inputfile, self.packageDir)
|
|
|
|
info, system, options, keyboards, files = get_metadata(self.packageDir)
|
|
|
|
self._check_version(inputfile, system)
|
|
|
|
if not keyboards:
|
|
logging.error("install_kmp.py: error: No kmp.json or kmp.inf found in %s", inputfile)
|
|
logging.info("Contents of %s:", inputfile)
|
|
for o in os.listdir(self.packageDir):
|
|
logging.info(o)
|
|
rmtree(self.packageDir)
|
|
message = _("No kmp.json or kmp.inf found in {packageFile}").format(
|
|
packageFile=inputfile)
|
|
raise InstallError(InstallStatus.Abort, message)
|
|
|
|
# sourcery skip: extract-method
|
|
logging.info("Installing %s", secure_lookup(info, 'name', 'description'))
|
|
process_keyboard_data(self.packageID, self.packageDir)
|
|
for kb in keyboards:
|
|
if kb['id'] != self.packageID:
|
|
process_keyboard_data(kb['id'], self.packageDir)
|
|
|
|
if files is not None:
|
|
self._install_files(keyboards, files)
|
|
|
|
return self._install_keyboards(keyboards, self.packageDir, language)
|
|
|
|
def _check_version(self, inputfile, system):
|
|
if not system:
|
|
return
|
|
|
|
fileVersion = secure_lookup(system, 'fileVersion')
|
|
if not fileVersion:
|
|
fileVersion = '7.0'
|
|
if packaging.version.parse(fileVersion) > packaging.version.parse(__version__):
|
|
logging.error("install_kmp.py: error: %s requires a newer version of Keyman (%s)",
|
|
inputfile, fileVersion)
|
|
rmtree(self.packageDir)
|
|
message = _("{packageFile} requires Keyman {keymanVersion} or higher").format(
|
|
packageFile=inputfile, keymanVersion=fileVersion)
|
|
raise InstallError(InstallStatus.Abort, message)
|
|
|
|
def _install_files(self, keyboards, files):
|
|
for f in files:
|
|
fpath = os.path.join(self.packageDir, f['name'])
|
|
ftype = f['type']
|
|
|
|
if ftype in [KMFileTypes.KM_DOC, KMFileTypes.KM_IMAGE]:
|
|
# Special handling of doc and images to hard link them into doc dir
|
|
logging.info("Installing %s as documentation", f['name'])
|
|
if self._safeMakeDirs(self.kmpdocdir):
|
|
kmpdocpath = os.path.join(self.kmpdocdir, f['name'])
|
|
self._safeLinkFile(fpath, kmpdocpath)
|
|
elif ftype == KMFileTypes.KM_FONT:
|
|
# Special handling of font to hard link it into font dir
|
|
logging.info("Installing %s as font", f['name'])
|
|
if self._safeMakeDirs(self.kmpfontdir):
|
|
fontpath = os.path.join(self.kmpfontdir, f['name'])
|
|
self._safeLinkFile(fpath, fontpath)
|
|
elif ftype == KMFileTypes.KM_OSK:
|
|
# Special handling to convert kvk into LDML
|
|
logging.info("Converting %s to LDML and installing both as as keyman file",
|
|
f['name'])
|
|
name, ext = os.path.splitext(f['name'])
|
|
ldml = convert_kvk_to_ldml(name, fpath)
|
|
if ldml is not None and len(ldml) > 0:
|
|
ldmlfile = os.path.join(self.packageDir, f"{name}.ldml")
|
|
output_ldml(ldmlfile, ldml)
|
|
elif ftype == KMFileTypes.KM_ICON:
|
|
# Special handling of icon to convert to PNG
|
|
logging.info("Converting %s to PNG and installing both as keyman files",
|
|
f['name'])
|
|
checkandsaveico(fpath)
|
|
elif ftype == KMFileTypes.KM_SOURCE:
|
|
# TODO for the moment just leave it for ibus-keyman to ignore if it doesn't load
|
|
pass
|
|
elif ftype == KMFileTypes.KM_KMX:
|
|
# Sanitize keyboard filename if not lower case
|
|
kmx_id, ext = os.path.splitext(os.path.basename(f['name']))
|
|
for kb in keyboards:
|
|
if kmx_id.lower() == kb['id'] and kmx_id != kb['id']:
|
|
os.rename(os.path.join(self.packageDir, f['name']),
|
|
os.path.join(self.packageDir, kb['id'] + '.kmx'))
|
|
fpath = os.path.join(self.packageDir, kb['id'] + '.kmx')
|
|
extractico(fpath)
|
|
|
|
def _safeMakeDirs(self, dir):
|
|
if not os.path.isdir(dir):
|
|
try:
|
|
os.makedirs(dir)
|
|
except NotADirectoryError:
|
|
logging.error("Can't create directory %s", dir)
|
|
return None
|
|
except PermissionError:
|
|
logging.error("No permissions to create directory %s", dir)
|
|
return None
|
|
except Exception as e:
|
|
logging.warning('Exception %s creating %s %s', type(e), dir, e.args)
|
|
return None
|
|
return dir
|
|
|
|
def _safeLinkFile(self, source, target):
|
|
if not os.path.isfile(source):
|
|
logging.error("Can't link file %s - source file doesn't exist", source)
|
|
return
|
|
if os.path.isfile(target):
|
|
return
|
|
if os.path.exists(target):
|
|
logging.error("Can't link file %s to %s - something exists at the target", source, target)
|
|
return
|
|
os.link(source, target)
|
|
|
|
def _normalize_language(self, supportedLanguages, language):
|
|
if len(supportedLanguages) <= 0:
|
|
return ''
|
|
|
|
if not language:
|
|
return language
|
|
|
|
language = CanonicalLanguageCodeUtils.findBestTag(language, False, False)
|
|
for supportedLanguage in supportedLanguages:
|
|
tag = CanonicalLanguageCodeUtils.findBestTag(supportedLanguage['id'], False, False)
|
|
if tag == language:
|
|
return tag
|
|
return None
|
|
|
|
def _add_custom_keyboard(self, keyboard, package_dir, requested_language):
|
|
if not requested_language:
|
|
return None
|
|
language = CanonicalLanguageCodeUtils.findBestTag(requested_language, False, False)
|
|
new_keyboard = get_ibus_keyboard_id(keyboard, package_dir, language)
|
|
CustomKeyboards().add(new_keyboard)
|
|
return language
|
|
|
|
def _install_keyboards(self, keyboards, packageDir, requested_language=None):
|
|
language = requested_language
|
|
# TODO: add other keyboards as well (#9757)
|
|
firstKeyboard = keyboards[0]
|
|
if secure_lookup(firstKeyboard, 'languages') and firstKeyboard['languages']:
|
|
language = CanonicalLanguageCodeUtils.normalize_language(firstKeyboard['languages'], language)
|
|
|
|
if not language:
|
|
language = self._add_custom_keyboard(firstKeyboard, packageDir, requested_language)
|
|
|
|
sentry = SentryErrorHandling()
|
|
sentry.add_breadcrumb(category='install', message=f'Installing keyboards for lang: {language}')
|
|
|
|
if is_fcitx_running():
|
|
return self._install_keyboards_to_fcitx()
|
|
|
|
restart_ibus()
|
|
if is_gnome_desktop():
|
|
return self._install_keyboards_to_gnome(keyboards, packageDir, language)
|
|
else:
|
|
return self._install_keyboards_to_ibus(keyboards, packageDir, language)
|
|
|
|
def _install_keyboards_to_ibus(self, keyboards, packageDir, language=None):
|
|
# sourcery skip: split-or-ifs
|
|
bus = get_ibus_bus()
|
|
if bus or os.environ.get('SUDO_USER'):
|
|
# install all kmx for first lang not just packageID
|
|
for kb in keyboards:
|
|
ibus_keyboard_id = get_ibus_keyboard_id(kb, packageDir, language)
|
|
install_to_ibus(bus, ibus_keyboard_id)
|
|
restart_ibus(bus)
|
|
if bus:
|
|
bus.destroy()
|
|
else:
|
|
logging.debug("could not install keyboards to IBus")
|
|
return ''
|
|
|
|
def _install_keyboards_to_gnome(self, keyboards, packageDir, language=None):
|
|
gnomeKeyboardsUtil = GnomeKeyboardsUtil()
|
|
sources = gnomeKeyboardsUtil.read_input_sources()
|
|
|
|
# install all kmx for first lang not just packageID
|
|
for kb in keyboards:
|
|
ibus_keyboard_id = get_ibus_keyboard_id(kb, packageDir, language)
|
|
input_source = ('ibus', ibus_keyboard_id)
|
|
if input_source not in sources:
|
|
sources.append(input_source)
|
|
|
|
gnomeKeyboardsUtil.write_input_sources(sources)
|
|
return ''
|
|
|
|
def _install_keyboards_to_fcitx(self):
|
|
return _('Please use fcitx5-configtool to add the keyboard to the desired group')
|
|
|
|
|
|
def extract_kmp(kmpfile, directory):
|
|
try:
|
|
with zipfile.ZipFile(kmpfile, "r") as zip_ref:
|
|
zip_ref.extractall(directory)
|
|
except zipfile.BadZipFile as e:
|
|
raise InstallError(InstallStatus.Abort, e) from e
|
|
|
|
|
|
def process_keyboard_data(keyboardID, packageDir) -> bool:
|
|
kbdata = get_keyboard_data(keyboardID)
|
|
if not kbdata or len(kbdata) <= 0:
|
|
return False
|
|
if not os.path.isdir(packageDir) and os.access(os.path.join(packageDir, os.pardir), os.X_OK | os.W_OK):
|
|
try:
|
|
os.makedirs(packageDir)
|
|
except Exception as e:
|
|
logging.warning('Exception %s creating %s %s', type(e), packageDir, e.args)
|
|
|
|
if os.access(packageDir, os.X_OK | os.W_OK):
|
|
try:
|
|
with open(os.path.join(packageDir, f'{keyboardID}.json'), 'w', encoding='utf-8') as outfile:
|
|
json.dump(kbdata, outfile)
|
|
logging.info("Installing api data file %s.json as keyman file", keyboardID)
|
|
return True
|
|
except Exception as e:
|
|
logging.warning('Exception %s writing %s/%s.json %s', type(e), packageDir, keyboardID, e.args)
|
|
return False
|
|
|
|
|
|
def install_kmp(inputfile, sharedarea=False, language=None):
|
|
"""
|
|
Install a kmp file
|
|
|
|
Args:
|
|
inputfile (str): path to kmp file
|
|
sharedarea(bool, default=False): whether install kmp to shared area or user directory
|
|
language(str, default=None): language to install keyboard for
|
|
has_ui(bool, default=True): whether we're displaying a window or running UI less from the command line
|
|
"""
|
|
if sharedarea:
|
|
return_value = InstallKmp().install_kmp_shared(inputfile, language)
|
|
else:
|
|
return_value = InstallKmp().install_kmp_user(inputfile, language)
|
|
|
|
keyboard_list_changed()
|
|
return return_value
|