#!/usr/bin/python3 # # eos-update-flatpak-repos: adds standard flatpak repos, removes legacy remotes, # migrates installed flatpaks between origins, and other fix-ups/workarounds # # Copyright (C) 2017 Endless Mobile, Inc. # Authors: # Mario Sanchez Prada # Philip Chimento # Robert McQueen # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import configparser import glob import logging import os import shutil import subprocess from systemd import journal import gi gi.require_version('Flatpak', '1.0') from gi.repository import Flatpak from gi.repository import GLib def _flatpak_inst_get_remote(inst, name): try: remote = inst.get_remote_by_name(name) except GLib.Error: remote = None return remote FLATPAK_REPO_DIR = os.getenv('EOS_FLATPAK_REPO_DIR', '/usr/share/eos-boot-helper/flatpak-repos') def _add_flatpak_repos(): insts = Flatpak.get_system_installations() inst = insts[0] for f in os.listdir(FLATPAK_REPO_DIR): if not f.endswith('.flatpakrepo'): continue name = f[:-12] repo_file = os.path.join(FLATPAK_REPO_DIR, f) if not os.path.isfile(repo_file): continue remote = _flatpak_inst_get_remote(inst, name) if remote: logging.debug("Remote {} already configured in {}" .format(name, inst.get_path().get_path())) if not remote.get_title(): # update title if it's missing, as EOS versions prior to 3.1.2 # added the GNOME remote with no title (T15580) c = configparser.ConfigParser() c.read(repo_file) title = c['Flatpak Repo']['Title'] logging.info("Setting missing title \"{}\" on remote {} in {}" .format(title, name, inst.get_path().get_path())) subprocess.check_call(['flatpak', 'remote-modify', '--system', '--title', title, name]) else: logging.info("Adding remote {} to {} from {}" .format(name, inst.get_path().get_path(), repo_file)) subprocess.check_call(['flatpak', 'remote-add', '--system', '--from', name, repo_file]) REMOTES_TO_REMOVE = [ # legacy eos-external-apps local repo (T14442) 'eos-external-apps', # legacy gnome-apps repo (T19898) 'gnome-apps', # legacy gnome runtimes repo (T20443) 'gnome' ] def _remove_remotes(): insts = Flatpak.get_system_installations() inst = insts[0] for name in REMOTES_TO_REMOVE: if _flatpak_inst_get_remote(inst, name): logging.info("Removing remote {} from {}" .format(name, inst.get_path().get_path())) subprocess.check_call(['flatpak', 'remote-delete', '--system', '--force', name]) EXT_APPS_REPO = '/var/lib/flatpak-external-apps' def _remove_old_external_apps_repo(): if os.path.isdir(EXT_APPS_REPO): logging.info("Removing repo {}".format(EXT_APPS_REPO)) shutil.rmtree(EXT_APPS_REPO, ignore_errors=True) RUNTIMES_TO_REMOVE = { 'eos-external-apps': { 'com.dropbox.Client', 'com.google.Chrome', 'com.microsoft.Skype', 'com.spotify.Client' } } def _remove_runtimes(): insts = Flatpak.get_system_installations() inst = insts[0] for ref in inst.list_installed_refs_by_kind(Flatpak.RefKind.RUNTIME): origin = ref.get_origin() if ref.get_origin() not in RUNTIMES_TO_REMOVE: continue name = ref.get_name() if name not in RUNTIMES_TO_REMOVE[origin]: continue refspec = '{}/{}/{}'.format(name, ref.get_arch(), ref.get_branch()) logging.info("Removing runtime {} from {}" .format(refspec, inst.get_path().get_path())) subprocess.check_call(['flatpak', 'uninstall', '--system', '--runtime', refspec]) def _update_deploy_file_with_origin(deploy_file_path, new_origin_name): _update_deploy_file(deploy_file_path, 0, new_origin_name) def _update_deploy_file_with_subpaths(deploy_file_path, new_subpaths): _update_deploy_file(deploy_file_path, 2, new_subpaths) def _update_deploy_file(deploy_file_path, idx, new_val): logging.debug("Reading data from the deploy file at {}..." .format(deploy_file_path)) src_file_contents = None with open(deploy_file_path, 'rb') as f: src_file_contents = GLib.Bytes.new(f.read()) # We need to read the GVariant in the deploy file and generate a # new one with all the same content but the right remote set. variant_type = GLib.VariantType.new('(ssasta{sv})') orig_variant = GLib.Variant.new_from_bytes(variant_type, src_file_contents, False) logging.debug("Original variant: {}".format(str(orig_variant))) cur_val = orig_variant[idx] if cur_val == new_val or (isinstance(cur_val, list) and isinstance(new_val, list) and set(cur_val) == set(new_val)): logging.info('Nothing to do, {}[{}] already set to: {}' .format(deploy_file_path, idx, new_val)) return builder = GLib.VariantBuilder.new(variant_type) for i, val in enumerate(orig_variant): child = orig_variant.get_child_value(i) if i == idx: builder.add_value(GLib.Variant(child.get_type_string(), new_val)) else: builder.add_value(child) new_variant = builder.end() logging.debug("New variant: {}".format(str(new_variant))) # Replace the original deploy file with the new GVariant logging.info("Writing new deploy file for {}, setting [{}] to: {}" .format(deploy_file_path, idx, new_val)) GLib.file_set_contents(deploy_file_path, new_variant.get_data_as_bytes().get_data()) FLATPAKS_TO_MIGRATE = [ # migrate EknServices from old eos-apps remote to eos-sdk (T17863) { 'name': 'com.endlessm.EknServices', 'kind': Flatpak.RefKind.APP, 'old-origin': 'eos-apps', 'new-origin': 'eos-sdk' }, # fix up image-builder bug where com.endlessm.apps.* runtimes were # mistakenly installed with eos-runtimes origin (T18366) { 'name': 'com.endlessm.apps.Platform', 'kind': Flatpak.RefKind.RUNTIME, 'old-origin': 'eos-runtimes', 'new-origin': 'eos-sdk' }, { 'name': 'com.endlessm.apps.Sdk', 'kind': Flatpak.RefKind.RUNTIME, 'old-origin': 'eos-runtimes', 'new-origin': 'eos-sdk' }, # fix up image builder bug on 'sea' (south-east asia) which set # an invalid subpath, causing no Locales to be installed (T19472) { 'name': 'com.endlessm.apps.Platform.Locale', 'kind': Flatpak.RefKind.RUNTIME, 'old-subpaths': ['/en,id,th,vi'], 'new-subpaths': ['/en', '/id', '/th', '/vi'] }, { 'name': 'com.endlessm.apps.Sdk.Locale', 'kind': Flatpak.RefKind.RUNTIME, 'old-subpaths': ['/en,id,th,vi'], 'new-subpaths': ['/en', '/id', '/th', '/vi'] }, # migrate Steam from eos-apps to flathub (T20322) { 'name': 'com.valvesoftware.Steam', 'kind': Flatpak.RefKind.APP, 'old-branch': 'eos3', 'new-branch': 'stable', 'old-origin': 'eos-apps', 'new-origin': 'flathub' }, # migrate org.gnome.* from gnome-apps to flathub (T19898) { 'prefix': 'org.gnome.', 'kind': Flatpak.RefKind.APP, 'old-branch': 'stable', 'old-origin': 'gnome-apps', 'new-origin': 'flathub' }, # migrate Teeworlds and MegaGlest from eos-apps to flathub (T20411) { 'name': 'com.teeworlds.Teeworlds', 'kind': Flatpak.RefKind.APP, 'old-branch': 'eos3', 'new-branch': 'stable', 'old-origin': 'eos-apps', 'new-origin': 'flathub' }, { 'name': 'org.megaglest.MegaGlest', 'kind': Flatpak.RefKind.APP, 'old-branch': 'eos3', 'new-branch': 'stable', 'old-origin': 'eos-apps', 'new-origin': 'flathub' }, # migrate Atom from eos-apps to flathub (T20301) { 'name': 'io.atom.Atom', 'kind': Flatpak.RefKind.APP, 'old-branch': 'eos3', 'new-branch': 'stable', 'old-origin': 'eos-apps', 'new-origin': 'flathub' }, # migrate gnome 3.2[46] and freedesktop 1.6 runtimes to flathub (T20443) { 'prefix': 'org.freedesktop.', 'kind': Flatpak.RefKind.RUNTIME, 'old-branch': '1.6', 'old-origin': 'gnome', 'new-origin': 'flathub' }, { 'prefix': 'org.gnome.', 'kind': Flatpak.RefKind.RUNTIME, 'old-branch': '3.22', 'old-origin': 'gnome', 'new-origin': 'flathub' }, { 'prefix': 'org.gnome.', 'kind': Flatpak.RefKind.RUNTIME, 'old-branch': '3.24', 'old-origin': 'gnome', 'new-origin': 'flathub' }, { 'prefix': 'org.gnome.', 'kind': Flatpak.RefKind.RUNTIME, 'old-branch': '3.26', 'old-origin': 'gnome', 'new-origin': 'flathub' }, { 'prefix': 'org.gnome.', 'kind': Flatpak.RefKind.RUNTIME, 'old-branch': '3.28', 'old-origin': 'gnome', 'new-origin': 'flathub' }, # migrate eos-apps to flathub where the app ID is the same (T20724) { 'name': 'com.google.AndroidStudio', 'kind': Flatpak.RefKind.APP, 'old-branch': 'eos3', 'new-branch': 'stable', 'old-origin': 'eos-apps', 'new-origin': 'flathub' }, { 'name': 'com.slack.Slack', 'kind': Flatpak.RefKind.APP, 'old-branch': 'eos3', 'new-branch': 'stable', 'old-origin': 'eos-apps', 'new-origin': 'flathub' }, { 'name': 'com.spotify.Client', 'kind': Flatpak.RefKind.APP, 'old-branch': 'eos3', 'new-branch': 'stable', 'old-origin': 'eos-apps', 'new-origin': 'flathub' }, { 'name': 'com.transmissionbt.Transmission', 'kind': Flatpak.RefKind.APP, 'old-branch': 'eos3', 'new-branch': 'stable', 'old-origin': 'eos-apps', 'new-origin': 'flathub' }, { 'name': 'net.minetest.Minetest', 'kind': Flatpak.RefKind.APP, 'old-branch': 'eos3', 'new-branch': 'stable', 'old-origin': 'eos-apps', 'new-origin': 'flathub' }, { 'name': 'org.freeciv.Freeciv', 'kind': Flatpak.RefKind.APP, 'old-branch': 'eos3', 'new-branch': 'stable', 'old-origin': 'eos-apps', 'new-origin': 'flathub' }, { 'name': 'org.gnome.Builder', 'kind': Flatpak.RefKind.APP, 'old-branch': 'eos3', 'new-branch': 'stable', 'old-origin': 'eos-apps', 'new-origin': 'flathub' }, { 'name': 'org.gnome.Genius', 'kind': Flatpak.RefKind.APP, 'old-branch': 'eos3', 'new-branch': 'stable', 'old-origin': 'eos-apps', 'new-origin': 'flathub' }, { 'name': 'org.gnome.Gnote', 'kind': Flatpak.RefKind.APP, 'old-branch': 'eos3', 'new-branch': 'stable', 'old-origin': 'eos-apps', 'new-origin': 'flathub' }, { 'name': 'org.inkscape.Inkscape', 'kind': Flatpak.RefKind.APP, 'old-branch': 'eos3', 'new-branch': 'stable', 'old-origin': 'eos-apps', 'new-origin': 'flathub' }, { 'name': 'org.pitivi.Pitivi', 'kind': Flatpak.RefKind.APP, 'old-branch': 'eos3', 'new-branch': 'stable', 'old-origin': 'eos-apps', 'new-origin': 'flathub' }, { 'name': 'org.tuxpaint.Tuxpaint', 'kind': Flatpak.RefKind.APP, 'old-branch': 'eos3', 'new-branch': 'stable', 'old-origin': 'eos-apps', 'new-origin': 'flathub' }, { 'name': 'org.videolan.VLC', 'kind': Flatpak.RefKind.APP, 'old-branch': 'eos3', 'new-branch': 'stable', 'old-origin': 'eos-apps', 'new-origin': 'flathub' }, { 'name': 'org.wesnoth.Wesnoth', 'kind': Flatpak.RefKind.APP, 'old-branch': 'eos3', 'new-branch': 'stable', 'old-origin': 'eos-apps', 'new-origin': 'flathub' } ] def _filter_refs(refs, name=None, prefix=None, kind=None, arch=None, branch=None, origin=None, subpaths=None): ret = [] for ref in refs: if kind and kind != ref.get_kind(): continue if name and name != ref.get_name(): continue if prefix and not ref.get_name().startswith(prefix): continue if arch and arch != ref.get_arch(): continue if branch and branch != ref.get_branch(): continue if origin and origin != ref.get_origin(): continue if subpaths and set(subpaths) != set(ref.get_subpaths()): continue ret.append(ref) return ret def _replace_in_file(path, old, new): _, data = GLib.file_get_contents(path) assert data GLib.file_set_contents(path, data.replace(old.encode('utf-8'), new.encode('utf-8'))) def _update_branch(ref, new_branch, refs): kind = ref.get_kind() name = ref.get_name() arch = ref.get_arch() branch = ref.get_branch() deploy_dir = ref.get_deploy_dir() clashing_refs = _filter_refs(refs, name=name, kind=kind, arch=arch, branch=new_branch) if len(clashing_refs) > 0: logging.warning('Not migrating {}/{}/{}, new branch {} already exists' .format(name, arch, branch, new_branch)) raise FileExistsError logging.info('Migrating {}/{}/{} to new branch {}' .format(name, arch, branch, new_branch)) # .../${kind}/${name}/${arch}/${branch} , ${ref}-${subpaths} old_branch_dir, ref_dir = os.path.split(deploy_dir) # .../${kind}/${name}/${arch} arch_dir = os.path.dirname(old_branch_dir) # .../${kind}/${name} name_dir = os.path.dirname(arch_dir) current = os.path.join(name_dir, 'current') current_tmp = current + '.tmp' # .../${kind}/${name}/${arch}/${new_branch} new_branch_dir = os.path.join(arch_dir, new_branch) # .../${kind}/${name}/${arch}/${new_branch}/${ref}-${subpaths} new_deploy_dir = os.path.join(new_branch_dir, ref_dir) try: os.mkdir(new_branch_dir, mode=0o755) subprocess.check_call(['cp', '-al', deploy_dir, new_deploy_dir]) os.symlink(ref_dir, os.path.join(new_branch_dir, 'active')) if kind == Flatpak.RefKind.APP: # update current symlink (specific to apps - runtimes # always have a specified branch) os.symlink('{}/{}'.format(arch, new_branch), current_tmp) os.rename(current_tmp, current) # update Exec= line in exported .desktop and .service files old_exec = '/flatpak run --branch={} --arch={}'.format(branch, arch) new_exec = '/flatpak run --branch={} --arch={}'.format(new_branch, arch) for path in glob.glob(os.path.join(new_deploy_dir, 'export/share/applications/*.desktop')): logging.debug('Updating branch in {}'.format(path)) _replace_in_file(path, old_exec, new_exec) for path in glob.glob(os.path.join(new_deploy_dir, 'export/share/dbus-1/services/*.service')): logging.debug('Updating branch in {}'.format(path)) _replace_in_file(path, old_exec, new_exec) except Exception: shutil.rmtree(new_branch_dir, ignore_errors=True) if os.path.exists(current_tmp): os.unlink(current_tmp) raise # this is unsafe whilst flatpaks are running # however: this is a startup job shutil.rmtree(old_branch_dir, ignore_errors=True) ref.set_property("branch", new_branch) ref.set_property("deploy-dir", new_deploy_dir) def _migrate_installed_flatpaks(): insts = Flatpak.get_system_installations() inst = insts[0] refs = inst.list_installed_refs() for migrate in FLATPAKS_TO_MIGRATE: kind = migrate['kind'] name = migrate.get('name') prefix = migrate.get('prefix') assert name or prefix old_branch = migrate.get('old-branch') old_origin = migrate.get('old-origin') old_subpaths = migrate.get('old-subpaths') matching_refs = _filter_refs(refs, name=name, prefix=prefix, kind=kind, branch=old_branch, origin=old_origin, subpaths=old_subpaths) # also search for name.* runtimes to migrate to catch extensions # unless we are already searching runtimes by prefix - in this case # we will already have found the matching extensions if name or kind == Flatpak.RefKind.APP: if not prefix: prefix = name + '.' matching_refs += _filter_refs(refs, prefix=prefix, kind=Flatpak.RefKind.RUNTIME, branch=old_branch, origin=old_origin, subpaths=old_subpaths) if len(matching_refs) == 0: if name: logging.debug('Found no matches to migrate for {} {}' .format(kind.value_nick, name)) else: logging.debug('Found no matches to migrate for {} {}*' .format(kind.value_nick, prefix)) continue for ref in matching_refs: name = ref.get_name() arch = ref.get_arch() branch = ref.get_branch() logging.info('Found {}/{}/{} to migrate' .format(name, arch, branch)) try: new_branch = migrate.get('new-branch') if new_branch: _update_branch(ref, new_branch, refs) deploy_dir = ref.get_deploy_dir() deploy = os.path.join(deploy_dir, 'deploy') new_origin = migrate.get('new-origin') if new_origin: _update_deploy_file_with_origin(deploy, new_origin) new_subpaths = migrate.get('new-subpaths') if new_subpaths: _update_deploy_file_with_subpaths(deploy, new_subpaths) except Exception: logging.exception('Failure applying migration to {}' .format(name)) continue if __name__ == '__main__': # Send logging messages both to the console and the journal logging.basicConfig(level=logging.INFO) logging.root.addHandler(journal.JournalHandler()) # ensure default remotes exist, such as eos-sdk and flathub _add_flatpak_repos() # remove unused remotes, such as eos-external-apps and gnome-apps _remove_remotes() # clear away legacy eos-external-apps repo and runtimes (T14442) _remove_old_external_apps_repo() _remove_runtimes() # move apps and runtimes between branches and origins as specified above _migrate_installed_flatpaks()