From 2a63948440fe2d60a5573b829f27000d5c0389e2 Mon Sep 17 00:00:00 2001 From: Eli Schwartz Date: Thu, 9 May 2019 16:14:22 -0400 Subject: [PATCH 1/5] install: when using a staging root, setup XDG_DATA_DIRS magic In order for xdg-utils programs to successfully install resources to the staging root instead of /usr, this variable needs to be set and additionally some arcane directories must be created that xdg-utils, astoundingly, does not know how to just gracefully handle. xdg-mime is simply hopeless as it does not have a --noupdate flag. When using a staged install, copy it with shutil instead. --- setup/install.py | 2 ++ src/calibre/linux.py | 22 ++++++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/setup/install.py b/setup/install.py index 362c6d057b..1a99507fe2 100644 --- a/setup/install.py +++ b/setup/install.py @@ -214,6 +214,8 @@ class Install(Develop): the environment variables: XDG_DATA_DIRS=/usr/share equivalent XDG_UTILS_INSTALL_MODE=system + For staged installs this will be automatically set to: + /share ''') short_description = 'Install calibre from source' diff --git a/src/calibre/linux.py b/src/calibre/linux.py index 2266181a5c..df9a153820 100644 --- a/src/calibre/linux.py +++ b/src/calibre/linux.py @@ -688,6 +688,12 @@ class PostInstall: self.opts.staging_etc = '/etc' if self.opts.staging_root == '/usr' else \ os.path.join(self.opts.staging_root, 'etc') + prefix = getattr(self.opts, 'prefix', None) + if prefix and prefix != self.opts.staging_root: + self.opts.staged_install = True + os.environ['XDG_DATA_DIRS'] = os.path.join(self.opts.staging_root, 'share') + os.environ['XDG_UTILS_INSTALL_MODE'] = 'system' + from calibre.utils.serialize import msgpack_loads scripts = msgpack_loads(P('scripts.calibre_msgpack', data=True)) self.manifest = manifest or [] @@ -799,6 +805,14 @@ class PostInstall: env['LD_LIBRARY_PATH'] = os.pathsep.join(npaths) cc = partial(check_call, env=env) + if getattr(self.opts, 'staged_install', False): + for d in {'applications', 'desktop-directories', 'icons/hicolor', 'mime/packages'}: + try: + os.makedirs(os.path.join(self.opts.staging_root, 'share', d)) + except OSError: + # python2 does not have exist_ok=True, failure will be reported by xdg-utils + pass + with TemporaryDirectory() as tdir, CurrentDir(tdir), PreserveMIMEDefaults(): def install_single_icon(iconsrc, basename, size, context, is_last_icon=False): @@ -876,10 +890,14 @@ class PostInstall: ak = x.partition('.')[0] if ak in APPDATA and os.access(appdata, os.W_OK): self.appdata_resources.append(write_appdata(ak, APPDATA[ak], appdata, translators)) - cc(['xdg-desktop-menu', 'forceupdate']) MIME = P('calibre-mimetypes.xml') self.mime_resources.append(MIME) - cc(['xdg-mime', 'install', MIME]) + if not getattr(self.opts, 'staged_install', False): + cc(['xdg-mime', 'install', MIME]) + cc(['xdg-desktop-menu', 'forceupdate']) + else: + from shutil import copyfile + copyfile(MIME, os.path.join(env['XDG_DATA_DIRS'], 'mime', 'packages', os.path.basename(MIME))) except Exception: if self.opts.fatal_errors: raise From adcc2c5539908e28fc93f9e7f3c3cc7ecc4ecc09 Mon Sep 17 00:00:00 2001 From: Eli Schwartz Date: Thu, 9 May 2019 21:25:55 -0400 Subject: [PATCH 2/5] install: fix calibre-uninstall relying on the source tree Injecting the location of the mime_resources file from P() in non-frozen builds will depend on the mime resource from the source tree rather than the same location during install time... a source tree which may be deleted after successful installation. On a frozen build, or when using 'develop' instead of 'install', this path will be the same, and things just work. The solution is to add *both* paths, and try to uninstall whichever one exists. --- src/calibre/linux.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/calibre/linux.py b/src/calibre/linux.py index df9a153820..8c8d31fc3e 100644 --- a/src/calibre/linux.py +++ b/src/calibre/linux.py @@ -127,11 +127,12 @@ if not frozen_path or not os.path.exists(os.path.join(frozen_path, 'resources', frozen_path = None for f in {mime_resources!r}: - cmd = ['xdg-mime', 'uninstall', f] - print ('Removing mime resource:', os.path.basename(f)) - ret = subprocess.call(cmd, shell=False) - if ret != 0: - print ('WARNING: Failed to remove mime resource', f) + if os.path.exists(f): + cmd = ['xdg-mime', 'uninstall', f] + print ('Removing mime resource:', os.path.basename(f)) + ret = subprocess.call(cmd, shell=False) + if ret != 0: + print ('WARNING: Failed to remove mime resource', f) for x in tuple({manifest!r}) + tuple({appdata_resources!r}) + (os.path.abspath(__file__), __file__, frozen_path): if not x or not os.path.exists(x): @@ -890,14 +891,16 @@ class PostInstall: ak = x.partition('.')[0] if ak in APPDATA and os.access(appdata, os.W_OK): self.appdata_resources.append(write_appdata(ak, APPDATA[ak], appdata, translators)) - MIME = P('calibre-mimetypes.xml') + MIME_BASE = 'calibre-mimetypes.xml' + MIME = P(MIME_BASE) self.mime_resources.append(MIME) + self.mime_resources.append(os.path.join(self.opts.staging_sharedir, MIME_BASE)) if not getattr(self.opts, 'staged_install', False): cc(['xdg-mime', 'install', MIME]) cc(['xdg-desktop-menu', 'forceupdate']) else: from shutil import copyfile - copyfile(MIME, os.path.join(env['XDG_DATA_DIRS'], 'mime', 'packages', os.path.basename(MIME))) + copyfile(MIME, os.path.join(env['XDG_DATA_DIRS'], 'mime', 'packages', MIME_BASE)) except Exception: if self.opts.fatal_errors: raise From 121a5acad06444373229baf5cb0c59bd21fe2cdb Mon Sep 17 00:00:00 2001 From: Eli Schwartz Date: Thu, 9 May 2019 21:54:19 -0400 Subject: [PATCH 3/5] install: don't create calibre-uninstall for staged installs The uninstaller is only meant to be used on systems where calibre is being installed live. For staged installs, it can be assumed that the same mechanism which takes care of installing files from the staging dir to live systems, is also able to remove calibre when desired. Usually this will be a linux package manager. --- src/calibre/linux.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/linux.py b/src/calibre/linux.py index 8c8d31fc3e..6d692c61ce 100644 --- a/src/calibre/linux.py +++ b/src/calibre/linux.py @@ -723,7 +723,8 @@ class PostInstall: self.setup_completion() if islinux or isbsd: self.setup_desktop_integration() - self.create_uninstaller() + if not getattr(self.opts, 'staged_install', False): + self.create_uninstaller() from calibre.utils.config import config_dir if os.path.exists(config_dir): From ae02c30a172f9a908808b24f22c9d5b28b63da9a Mon Sep 17 00:00:00 2001 From: Eli Schwartz Date: Sun, 12 May 2019 01:48:20 -0400 Subject: [PATCH 4/5] install: first install code, then binaries Since installing code will also delete a directory tree, it should run first. This avoids exotic cases where it deletes the binaries it just installed. Use case: co-installing python2/python3 using a bindir bundled with the rest of calibre's code, in order to maintain an alternatives system pointing symlinks in /usr/bin at the configured binaries. --- setup/install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/install.py b/setup/install.py index 1a99507fe2..d9380d761c 100644 --- a/setup/install.py +++ b/setup/install.py @@ -135,8 +135,8 @@ class Develop(Command): self.opts = opts self.regain_privileges() self.consolidate_paths() - self.write_templates() self.install_files() + self.write_templates() self.run_postinstall() self.install_env_module() self.success() From 1aecd238525315197bdc88f2878836a07ebd605b Mon Sep 17 00:00:00 2001 From: Eli Schwartz Date: Sun, 12 May 2019 09:35:53 -0400 Subject: [PATCH 5/5] calibre-uninstall: be even more thorough about deleting mime files Since xdg-mime only cares about basename(filename) and that filename is an existing file (and we don't know where the installed one is), create an empty file with the right name and then delete that. --- src/calibre/linux.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/calibre/linux.py b/src/calibre/linux.py index 6d692c61ce..3805877aa0 100644 --- a/src/calibre/linux.py +++ b/src/calibre/linux.py @@ -110,7 +110,7 @@ UNINSTALL = '''\ from __future__ import print_function, unicode_literals euid = {euid} -import os, subprocess, shutil +import os, subprocess, shutil, tempfile try: raw_input @@ -126,15 +126,18 @@ frozen_path = {frozen_path!r} if not frozen_path or not os.path.exists(os.path.join(frozen_path, 'resources', 'calibre-mimetypes.xml')): frozen_path = None +dummy_mime_path = tempfile.mkdtemp(prefix='mime-hack.') for f in {mime_resources!r}: - if os.path.exists(f): - cmd = ['xdg-mime', 'uninstall', f] - print ('Removing mime resource:', os.path.basename(f)) - ret = subprocess.call(cmd, shell=False) - if ret != 0: - print ('WARNING: Failed to remove mime resource', f) + # dummyfile + file = os.path.join(dummy_mime_path, f) + open(file, 'w').close() + cmd = ['xdg-mime', 'uninstall', file] + print ('Removing mime resource:', f) + ret = subprocess.call(cmd, shell=False) + if ret != 0: + print ('WARNING: Failed to remove mime resource', f) -for x in tuple({manifest!r}) + tuple({appdata_resources!r}) + (os.path.abspath(__file__), __file__, frozen_path): +for x in tuple({manifest!r}) + tuple({appdata_resources!r}) + (os.path.abspath(__file__), __file__, frozen_path, dummy_mime_path): if not x or not os.path.exists(x): continue print ('Removing', x) @@ -894,8 +897,7 @@ class PostInstall: self.appdata_resources.append(write_appdata(ak, APPDATA[ak], appdata, translators)) MIME_BASE = 'calibre-mimetypes.xml' MIME = P(MIME_BASE) - self.mime_resources.append(MIME) - self.mime_resources.append(os.path.join(self.opts.staging_sharedir, MIME_BASE)) + self.mime_resources.append(MIME_BASE) if not getattr(self.opts, 'staged_install', False): cc(['xdg-mime', 'install', MIME]) cc(['xdg-desktop-menu', 'forceupdate'])