Skip to content

Commit 95d29a8

Browse files
committed
Migrate Chrome to Flathub
Some notes: - The Chrome migration service installs Flathub's Chrome if it is not installed and eos-apps' Chrome is installed, then uninstalls the eos-app's Chrome. - The Chrome's profile migration service runs if Flathub's Chrome is installed, '~/.config/google-chrome' (eos-apps Chrome' config) exits and '~/.var/app/com.google.Chrome/config/google-chrome' (Flathub Chrome's config) does not exist. - This is such that we can update '~/.config/mimeapps.list' to point the new Chrome in case the old Chrome is specified as default there but the user never ran it - Note that the actual user profile migration is skipped if '~/.var/app/com.google.Chrome/config/google-chrome' already exists or if '~/.config/google-chrome' doesn't exist by the time the migration runs - Some services credentials (e.g. gmail) are lost during profile migration (requiring re-login) https://phabricator.endlessm.com/T26944
1 parent 3641cad commit 95d29a8

7 files changed

+321
-0
lines changed

Makefile.am

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,14 @@ dist_systemdunit_DATA = \
2626
eos-firstboot.service \
2727
eos-fix-flatpak-overrides.service \
2828
eos-live-boot-overlayfs-setup.service \
29+
eos-migrate-chrome.service \
2930
eos-transient-setup.service \
3031
eos-update-flatpak-repos.service \
3132
eos-update-system-ca.service \
3233
$(NULL)
3334

3435
dist_systemduserunit_DATA = \
36+
eos-migrate-chrome-profile.service \
3537
eos-migrate-chromium-profile.service \
3638
eos-migrate-firefox-profile.service \
3739
eos-migrate-shotwell.service \
@@ -68,7 +70,10 @@ install-data-hook:
6870
ln -s 'no-wait.d/eos-firewall-localonly-nm' \
6971
'$(DESTDIR)$(networkscriptdir)/eos-firewall-localonly-nm'
7072
chmod +x '$(DESTDIR)$(networkscriptdir)/eos-firewall-localonly-nm'
73+
$(MKDIR_P) '$(DESTDIR)$(systemdunitdir)/multi-user.target.wants'
74+
ln -s ../eos-migrate-chrome.service '$(DESTDIR)$(systemdunitdir)/multi-user.target.wants'
7175
$(MKDIR_P) '$(DESTDIR)$(systemduserunitdir)/gnome-session.target.wants'
76+
ln -s ../eos-migrate-chrome-profile.service '$(DESTDIR)$(systemduserunitdir)/gnome-session.target.wants'
7277
ln -s ../eos-migrate-chromium-profile.service '$(DESTDIR)$(systemduserunitdir)/gnome-session.target.wants'
7378
ln -s ../eos-migrate-firefox-profile.service '$(DESTDIR)$(systemduserunitdir)/gnome-session.target.wants'
7479
ln -s ../eos-migrate-shotwell.service '$(DESTDIR)$(systemduserunitdir)/gnome-session.target.wants'
@@ -104,6 +109,8 @@ eos_update_efi_uuid_CFLAGS = $(AM_CFLAGS) $(EFIBOOT_CFLAGS)
104109
eos_update_efi_uuid_LDADD = $(EFIBOOT_LIBS)
105110

106111
dist_libexec_SCRIPTS = \
112+
eos-migrate-chrome \
113+
eos-migrate-chrome-profile \
107114
eos-migrate-chromium-profile \
108115
eos-migrate-firefox-profile \
109116
$(NULL)

eos-migrate-chrome

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/bin/bash
2+
3+
if ! [[ $(flatpak info -o com.google.Chrome//eos3) == "eos-apps" ]]; then
4+
echo "eos-apps Chrome is not installed"
5+
exit
6+
fi
7+
8+
err=0
9+
if ! [[ $(flatpak info -o com.google.Chrome//stable) == "flathub" ]]; then
10+
echo "Install Flathub Chrome"
11+
flatpak install flathub com.google.Chrome -y
12+
err=$?
13+
fi
14+
15+
if [ $err != 0 ]; then
16+
echo "Fialed to install Flathub Chrome with error code $err"
17+
exit $err
18+
fi
19+
20+
echo "Uninstall eos-apps Flathub"
21+
flatpak uninstall com.google.Chrome//eos3 -y

eos-migrate-chrome-profile

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#!/usr/bin/python3
2+
#
3+
# eos-migrate-chrome-profile: move users' Chrome profile to its new home
4+
#
5+
# This script is based on eos-migrate-chromium-profile.
6+
#
7+
# Copyright © 2025 Endless OS Foundation LLC
8+
#
9+
# This program is free software; you can redistribute it and/or modify
10+
# it under the terms of the GNU General Public License as published by
11+
# the Free Software Foundation; either version 2 of the License, or
12+
# (at your option) any later version.
13+
#
14+
# This program is distributed in the hope that it will be useful,
15+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
# GNU General Public License for more details.
18+
#
19+
# You should have received a copy of the GNU General Public License
20+
# along with this program; if not, write to the Free Software
21+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22+
23+
import fileinput
24+
import os
25+
import subprocess
26+
import sys
27+
from gi.repository import GLib
28+
29+
30+
USER_HOME_DIR = os.path.expanduser("~/")
31+
OLD_CHROME_CONFIG_DIR = os.path.join(USER_HOME_DIR, ".config", "google-chrome")
32+
NEW_CHROME_DATA_DIR = os.path.join(USER_HOME_DIR, ".var", "app", "com.google.Chrome")
33+
NEW_CHROME_CONFIG_DIR = os.path.join(NEW_CHROME_DATA_DIR, "config", "google-chrome")
34+
35+
OLD_WIDEVINE_SYSTEM_DIR = os.path.join(os.path.sep, "usr", "lib",
36+
"google-chrome", "WidevineCdm")
37+
38+
MIMEAPPS_LIST = os.path.join(GLib.get_user_config_dir(), "mimeapps.list")
39+
MIMEAPPS_GROUPS = [
40+
"Default Applications",
41+
"Added Associations",
42+
"Removed Associations",
43+
]
44+
OLD_DESKTOP_FILE = "google-chrome.desktop"
45+
NEW_DESKTOP_FILE = "com.google.Chrome.desktop"
46+
47+
48+
def update_mimeapps_list(path):
49+
"""Update file associations, which in particular includes x-scheme-handler/http and
50+
friends to specify the default web browser.
51+
52+
We cannot use GLib's own API to query what mime types the old Chrome desktop file
53+
is a handler for, because we can't construct a GDesktopAppInfo for it, because it
54+
tries to execute the old Chrome which no longer exists. Update to the new installed
55+
Flathub Chrome's desktop.
56+
"""
57+
58+
keyfile = GLib.KeyFile()
59+
try:
60+
keyfile.load_from_file(
61+
path, GLib.KeyFileFlags.KEEP_COMMENTS | GLib.KeyFileFlags.KEEP_TRANSLATIONS,
62+
)
63+
except GLib.GError as gerror:
64+
if gerror.matches(GLib.file_error_quark(), GLib.FileError.NOENT):
65+
return
66+
67+
raise
68+
69+
changed = False
70+
71+
for group in MIMEAPPS_GROUPS:
72+
if not keyfile.has_group(group):
73+
continue
74+
75+
keys, _length = keyfile.get_keys(group)
76+
for key in keys:
77+
values = keyfile.get_string_list(group, key)
78+
try:
79+
i = values.index(OLD_DESKTOP_FILE)
80+
except ValueError:
81+
pass
82+
else:
83+
values[i] = NEW_DESKTOP_FILE
84+
keyfile.set_string_list(group, key, values)
85+
changed = True
86+
87+
if changed:
88+
keyfile.save_to_file(path)
89+
90+
91+
def update_old_config_references(path):
92+
if not os.path.isfile(path):
93+
return
94+
95+
for line in fileinput.input(path, inplace=True):
96+
line = line.replace(OLD_CHROME_CONFIG_DIR, NEW_CHROME_CONFIG_DIR)
97+
line = line.replace(OLD_WIDEVINE_SYSTEM_DIR, "")
98+
print(line, end='')
99+
100+
101+
def main():
102+
res = subprocess.run(["flatpak", "info", "-o", "com.google.Chrome//stable"],
103+
capture_output=True)
104+
if res.stdout != b"flathub\n":
105+
print("Flathub Chrome has not been installed", file=sys.stderr)
106+
sys.exit(1)
107+
108+
update_mimeapps_list(MIMEAPPS_LIST)
109+
110+
if (
111+
os.path.isdir(OLD_CHROME_CONFIG_DIR) and
112+
not os.path.isdir(NEW_CHROME_CONFIG_DIR)
113+
):
114+
update_old_config_references(
115+
os.path.join(OLD_CHROME_CONFIG_DIR,
116+
"WidevineCdm", "latest-component-updated-widevine-cdm"))
117+
os.makedirs(os.path.dirname(NEW_CHROME_CONFIG_DIR), exist_ok=True)
118+
os.rename(OLD_CHROME_CONFIG_DIR, NEW_CHROME_CONFIG_DIR)
119+
120+
121+
if __name__ == "__main__":
122+
main()

eos-migrate-chrome-profile.service

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[Unit]
2+
Description=Migrate Chrome profile to new path
3+
4+
ConditionPathExists=%h/.config/google-chrome
5+
ConditionPathExists=!%h/.var/app/com.google.Chrome/config/google-chrome
6+
ConditionPathExists=/var/lib/flatpak/app/com.google.Chrome/x86_64/stable
7+
8+
[Service]
9+
Type=oneshot
10+
ExecStart=/usr/lib/eos-boot-helper/eos-migrate-chrome-profile
11+
Restart=no
12+
RemainAfterExit=yes
13+
14+
[Install]
15+
WantedBy=gnome-session.target

eos-migrate-chrome.service

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[Unit]
2+
Description=Migrate eos-apps Chrome to Flathub Chrome
3+
4+
ConditionPathExists=/var/lib/flatpak/app/com.google.Chrome/x86_64/eos3
5+
After=network-online.target
6+
Wants=network-online.target
7+
Before=gdm.service
8+
9+
[Service]
10+
Type=oneshot
11+
ExecStart=/usr/lib/eos-boot-helper/eos-migrate-chrome
12+
Restart=no
13+
RemainAfterExit=yes
14+
15+
[Install]
16+
WantedBy=multi-user.target

tests/Makefile.am

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ EXTRA_DIST = \
2323
test_esp_generator.py \
2424
test_image_boot.py \
2525
test_live_storage.py \
26+
test_migrate_chrome_profile.py \
2627
test_migrate_chromium_profile.py \
2728
test_migrate_firefox_profile.py \
2829
test_repartition.py \
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#!/usr/bin/python3
2+
"""
3+
Tests eos-migrate-chrome-profile
4+
5+
This test is based on test_migrate_chromium_profile.py.
6+
"""
7+
8+
import tempfile
9+
import textwrap
10+
import os
11+
12+
from .util import BaseTestCase, system_script, import_script_as_module
13+
14+
emfp = import_script_as_module("emfp", system_script("eos-migrate-chrome-profile"))
15+
16+
17+
class TestUpdateMimeappsList(BaseTestCase):
18+
def setUp(self):
19+
super().setUp()
20+
21+
self.tmp = tempfile.TemporaryDirectory()
22+
23+
def tearDown(self):
24+
self.tmp.cleanup()
25+
26+
def test_nonexistent(self):
27+
emfp.update_mimeapps_list(os.path.join(self.tmp.name, "mimeapps.list"))
28+
29+
def test_not_there(self):
30+
orig_data = textwrap.dedent(
31+
"""
32+
[Default Applications]
33+
image/jpeg=eog.desktop
34+
"""
35+
).lstrip()
36+
expected_data = orig_data
37+
38+
self._test(orig_data, expected_data)
39+
40+
def test_there(self):
41+
orig_data = textwrap.dedent(
42+
"""
43+
[Default Applications]
44+
text/html=google-chrome.desktop
45+
image/jpeg=eog.desktop
46+
47+
[Added Associations]
48+
text/xml=google-chrome.desktop;org.mozilla.firefox.desktop;org.chromium.Chromium.desktop;
49+
"""
50+
).lstrip()
51+
"""
52+
The trailing semicolon in the [Default Applications] text/html entry is legal.
53+
54+
https://specifications.freedesktop.org/mime-apps-spec/latest/ar01s03.html says:
55+
56+
The value is a semicolon-separated list of desktop file IDs (as defined in
57+
the desktop entry spec).
58+
59+
https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s04.html
60+
says:
61+
62+
The multiple values should be separated by a semicolon and the value of the
63+
key may be optionally terminated by a semicolon.
64+
65+
GKeyFile always adds the semicolon. What's fun is that when GLib itself updates
66+
[Default Applications], it uses set_string() rather than set_string_list(), even
67+
though it parses these as lists.
68+
"""
69+
expected_data = textwrap.dedent(
70+
"""
71+
[Default Applications]
72+
text/html=com.google.Chrome.desktop;
73+
image/jpeg=eog.desktop
74+
75+
[Added Associations]
76+
text/xml=com.google.Chrome.desktop;org.mozilla.firefox.desktop;org.chromium.Chromium.desktop;
77+
"""
78+
).lstrip()
79+
80+
self._test(orig_data, expected_data)
81+
82+
def _test(self, orig_data, expected_data):
83+
mimeapps_list = os.path.join(self.tmp.name, "mimeapps.list")
84+
with open(mimeapps_list, "w") as f:
85+
f.write(orig_data)
86+
87+
emfp.update_mimeapps_list(mimeapps_list)
88+
with open(mimeapps_list, "r") as f:
89+
new_data = f.read()
90+
91+
self.assertEqual(expected_data, new_data)
92+
93+
94+
class TestUpdateOldConfigReferences(BaseTestCase):
95+
def setUp(self):
96+
super().setUp()
97+
98+
self.tmp = tempfile.TemporaryDirectory()
99+
100+
def tearDown(self):
101+
self.tmp.cleanup()
102+
103+
def test_nonexistent(self):
104+
emfp.update_mimeapps_list(os.path.join(self.tmp.name, "mimeapps.list"))
105+
106+
def test_not_there(self):
107+
orig_data = ""
108+
expected_data = orig_data
109+
110+
self._test(orig_data, expected_data)
111+
112+
def test_there_system(self):
113+
orig_data = '{"Path":"/usr/lib/google-chrome/WidevineCdm"}'
114+
expected_data = '{"Path":""}'
115+
116+
self._test(orig_data, expected_data)
117+
118+
def test_there_local(self):
119+
user_home_dir = os.path.expanduser("~/")
120+
orig_data = \
121+
'{"Path":"' + user_home_dir + \
122+
'.config/google-chrome/WidevineCdm/4.10.1610.0"}'
123+
expected_data = \
124+
'{"Path":"' + user_home_dir + \
125+
'.var/app/com.google.Chrome/config/google-chrome/WidevineCdm/4.10.1610.0"}'
126+
127+
self._test(orig_data, expected_data)
128+
129+
def _test(self, orig_data, expected_data):
130+
last_updated_widevine = os.path.join(self.tmp.name,
131+
"latest-component-updated-widevine-cdm")
132+
with open(last_updated_widevine, "w") as f:
133+
f.write(orig_data)
134+
135+
emfp.update_old_config_references(last_updated_widevine)
136+
with open(last_updated_widevine, "r") as f:
137+
new_data = f.read()
138+
139+
self.assertEqual(expected_data, new_data)

0 commit comments

Comments
 (0)