Back up portal configuration during explicit development upgrades

This commit is contained in:
Samuel Onoja committed 2026-09-30 05:22:26 +01:00
1 parent 35537b32c8
commit 46d33751c6
4 files changed
+119 -8

No files matched your search

+6
View File
@@ -267,6 +267,12 @@ git pull --ff-only
Log out and back in to use the new release. Running sessions keep their existing
processes and release paths until restarted.
The installer records shipped portal configuration with each release for future
upgrades. Modified or untracked administrator files stop installation. To replace
Ferese portal configuration from an earlier development install, use
`./scripts/install.sh --skip-build --replace-portal-config` after building. Existing
files are backed up under the new release's `portal-config.previous/` directory.
Installer options:
```sh
+35 -6
View File
@@ -8,8 +8,21 @@ session_entry_matches() {
cmp -s -- <(sed '/^Icon=/d' "$1") <(sed '/^Icon=/d' "$2")
}
managed_file_matches() {
local relative=$1 destination=$2
cmp -s -- "$repo_dir/packaging/$relative" "$destination" ||
cmp -s -- "$install_root/current/installer-files/$relative" "$destination"
}
repo_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
release_id=${1:?usage: install-session.sh RELEASE_ID}
release_id=${1:?usage: install-session.sh RELEASE_ID [--replace-portal-config]}
replace_portal_config=false
if [[ ${2:-} == --replace-portal-config && $# == 2 ]]; then
replace_portal_config=true
elif (($# > 1)); then
echo 'usage: install-session.sh RELEASE_ID [--replace-portal-config]' >&2
exit 2
fi
[[ $release_id =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]] || exit 2
[[ $EUID == 0 ]] || { echo 'Run this installer with sudo.' >&2; exit 1; }
@@ -64,12 +77,14 @@ for entry in \
'ferese-portals.conf:ferese-portals.conf'; do
source=${entry%%:*}
destination=/usr/share/xdg-desktop-portal/${entry#*:}
if [[ -e $destination ]] && ! cmp -s "$repo_dir/packaging/portal/$source" "$destination"; then
echo "Unmanaged portal configuration exists: $destination" >&2; exit 1
if [[ -e $destination ]] && ! managed_file_matches "portal/$source" "$destination" && ! $replace_portal_config; then
echo "Unmanaged portal configuration exists: $destination" >&2
echo 'Use --replace-portal-config to back up and replace Ferese portal configuration.' >&2
exit 1
fi
done
service=/usr/local/share/dbus-1/services/org.freedesktop.impl.portal.desktop.ferese.service
if [[ -e $service ]] && ! cmp -s "$repo_dir/packaging/portal/$(basename "$service")" "$service"; then
if [[ -e $service ]] && ! managed_file_matches "portal/$(basename "$service")" "$service"; then
# Upgrade the exact activation file shipped before systemd supervision.
if ! cmp -s <(printf '%s\n' '[D-BUS Service]' \
'Name=org.freedesktop.impl.portal.desktop.ferese' \
@@ -79,12 +94,12 @@ if [[ -e $service ]] && ! cmp -s "$repo_dir/packaging/portal/$(basename "$servic
fi
portal_unit=/usr/local/lib/systemd/user/xdg-desktop-portal-ferese.service
if [[ -e $portal_unit ]] && ! cmp -s "$repo_dir/packaging/systemd/xdg-desktop-portal-ferese.service" "$portal_unit"; then
if [[ -e $portal_unit ]] && ! managed_file_matches "systemd/xdg-desktop-portal-ferese.service" "$portal_unit"; then
echo "Unmanaged portal unit exists: $portal_unit" >&2; exit 1
fi
session_target=/usr/local/lib/systemd/user/ferese-session.target
if [[ -e $session_target ]] && ! cmp -s "$repo_dir/packaging/systemd/ferese-session.target" "$session_target"; then
if [[ -e $session_target ]] && ! managed_file_matches "systemd/ferese-session.target" "$session_target"; then
echo "Unmanaged session target exists: $session_target" >&2; exit 1
fi
@@ -92,6 +107,20 @@ desktop-file-validate "$repo_dir/packaging/ferese.desktop"
desktop-file-validate "$repo_dir/packaging/dev.ferese.Settings.desktop"
install -d -m 0755 -- "$release_dir" /usr/local/bin /usr/share/wayland-sessions
for relative in portals/ferese.portal ferese-portals.conf; do
destination=/usr/share/xdg-desktop-portal/$relative
if [[ -e $destination ]]; then
install -D -m 0644 -- "$destination" "$release_dir/portal-config.previous/$relative"
fi
done
for relative in \
portal/ferese.portal \
portal/ferese-portals.conf \
portal/org.freedesktop.impl.portal.desktop.ferese.service \
systemd/xdg-desktop-portal-ferese.service \
systemd/ferese-session.target; do
install -D -m 0644 -- "$repo_dir/packaging/$relative" "$release_dir/installer-files/$relative"
done
if [[ -e $session_entry ]]; then
install -m 0644 -- "$session_entry" "$release_dir/session.previous.desktop"
fi
+4
View File
@@ -11,6 +11,7 @@ Run from any directory as your normal user. Existing user config is preserved.
--release-id ID Name the installed release (default: UTC timestamp + PID)
--skip-build Install existing target/release binaries
--replace-portal-config Back up and replace existing Ferese portal configuration
--offline Build using only cached Cargo dependencies
--dry-run Print commands without building or installing
-h, --help Show this help
@@ -24,6 +25,7 @@ fail() { echo "ferese installer: $*" >&2; exit 1; }
repo_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
release_id="$(date -u +%Y%m%d-%H%M%S)-$$"
skip_build=false
replace_portal_config=false
offline=false
dry_run=false
while (($#)); do
@@ -34,6 +36,7 @@ while (($#)); do
shift 2
;;
--skip-build) skip_build=true; shift ;;
--replace-portal-config) replace_portal_config=true; shift ;;
--offline) offline=true; shift ;;
--dry-run) dry_run=true; shift ;;
-h|--help) usage; exit 0 ;;
@@ -57,6 +60,7 @@ if ! $skip_build; then
fi
installer=(bash "$repo_dir/scripts/install-session.sh" "$release_id")
if $replace_portal_config; then installer+=(--replace-portal-config); fi
if ((EUID != 0)); then
if command -v sudo >/dev/null; then
installer=(sudo "${installer[@]}")
+74 -2
View File
@@ -14,7 +14,8 @@ class PortalInstallTest(unittest.TestCase):
def preflight(self, service=None, unit=None):
script = (REPO / 'scripts/install-session.sh').read_text()
start = script.index('service=/usr/local/share/dbus-1/services/')
script = script[start:script.index('session_target=', start)]
helpers = script[:script.index('repo_dir=')]
script = helpers + script[start:script.index('session_target=', start)]
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
if service is not None:
@@ -30,9 +31,80 @@ class PortalInstallTest(unittest.TestCase):
'portal_unit=/usr/local/lib/systemd/user/xdg-desktop-portal-ferese.service',
'portal_unit="$TEST_ROOT/portal.service"')
return subprocess.run(['bash', '-eu', '-c', script],
env={'repo_dir': str(REPO), 'TEST_ROOT': directory},
env={'repo_dir': str(REPO), 'TEST_ROOT': directory, 'install_root': directory},
capture_output=True, text=True)
def configuration_preflight(self, portal=None, preferences=None, previous=None, replace=False):
script = (REPO / 'scripts/install-session.sh').read_text()
helpers = script[:script.index('repo_dir=')]
start = script.index('for entry in ')
check = script[start:script.index('service=/usr/local/share/dbus-1/services/', start)]
check = check.replace('/usr/share/xdg-desktop-portal/', '$TEST_ROOT/config/')
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
for relative, content in [('portals/ferese.portal', portal), ('ferese-portals.conf', preferences)]:
if content is not None:
destination = root / 'config' / relative
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(content)
if previous:
for relative, content in previous.items():
destination = root / 'current/installer-files/portal' / relative
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(content)
return subprocess.run(['bash', '-eu', '-c', helpers + check],
env={'repo_dir': str(REPO), 'TEST_ROOT': directory, 'install_root': directory,
'replace_portal_config': str(replace).lower()},
capture_output=True, text=True)
def test_existing_development_install_requires_explicit_replacement(self):
portal = ('[portal]\nDBusName=org.freedesktop.impl.portal.desktop.ferese\n'
'Interfaces=org.freedesktop.impl.portal.ScreenCast;\nUseIn=Ferese;\n')
preferences = '[preferred]\ndefault=gtk;\norg.freedesktop.impl.portal.ScreenCast=ferese;\n'
result = self.configuration_preflight(portal, preferences)
self.assertNotEqual(result.returncode, 0)
self.assertIn('--replace-portal-config', result.stderr)
result = self.configuration_preflight(portal, preferences, replace=True)
self.assertEqual(result.returncode, 0, result.stderr)
def test_previous_portal_configuration_is_backed_up(self):
script = (REPO / 'scripts/install-session.sh').read_text()
start = script.index('for relative in portals/ferese.portal')
backup = script[start:script.index('for relative in ', start + 1)]
backup = backup.replace('/usr/share/xdg-desktop-portal/', '$TEST_ROOT/config/')
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
for relative in ('portals/ferese.portal', 'ferese-portals.conf'):
source = root / 'config' / relative
source.parent.mkdir(parents=True, exist_ok=True)
source.write_text('existing custom content\n')
result = subprocess.run(['bash', '-eu', '-c', backup],
env={'TEST_ROOT': directory, 'release_dir': str(root / 'release')},
capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
for relative in ('portals/ferese.portal', 'ferese-portals.conf'):
self.assertEqual((root / 'release/portal-config.previous' / relative).read_text(),
'existing custom content\n')
self.assertEqual((root / 'config' / relative).read_text(), 'existing custom content\n')
def test_upgrades_recorded_previous_release(self):
old_portal = '[portal]\nInterfaces=org.freedesktop.impl.portal.Settings;\n'
old_preferences = '[preferred]\norg.freedesktop.impl.portal.Settings=ferese;\n'
result = self.configuration_preflight(old_portal, old_preferences,
{'ferese.portal': old_portal, 'ferese-portals.conf': old_preferences})
self.assertEqual(result.returncode, 0, result.stderr)
result = self.configuration_preflight(old_portal + 'UseIn=Custom;\n', old_preferences,
{'ferese.portal': old_portal, 'ferese-portals.conf': old_preferences})
self.assertNotEqual(result.returncode, 0)
self.assertIn('Unmanaged portal configuration', result.stderr)
def test_preserves_custom_configuration(self):
portal = (REPO / 'packaging/portal/ferese.portal').read_text()
preferences = '[preferred]\ndefault=custom;\n'
result = self.configuration_preflight(portal, preferences)
self.assertNotEqual(result.returncode, 0)
self.assertIn('Unmanaged portal configuration', result.stderr)
def test_accepts_fresh_install_and_previous_activation_file(self):
self.assertEqual(self.preflight().returncode, 0)
self.assertEqual(self.preflight(LEGACY).returncode, 0)