Kickoff fork: an update bar in the start menu
All checks were successful
Image, disk image and live ISO / iso (push) Successful in 32m17s
All checks were successful
Image, disk image and live ISO / iso (push) Successful in 32m17s
When a newer image is on the forge, Kickoff shows "A system update is available" with Update Now (Aurora's System Update); once one is downloaded, "Restart to finish updating" with Restart. Hidden otherwise. - patches/kickoff/: Kickoff's QML plus UpdateBar.qml as its footer. The kickoff-builder stage rebuilds just that plugin from the image's own plasma-desktop version (KDE's source, via the forge's mirror-kde-plasma-desktop) and the final stage swaps it in. - quine-update-check (root; bootc needs it): bootc upgrade --check, then writes /run/quine-linux/update-status.json for the bar. At boot, every 4 hours, and after uupd's nightly update. Tested in a VM from a disk image of this tree: up to date (hidden), available and staged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
de365c4227
commit
a6b5a836b7
7 changed files with 266 additions and 1 deletions
|
|
@ -35,6 +35,43 @@ RUN set -eux; \
|
||||||
mkdir -p /output; \
|
mkdir -p /output; \
|
||||||
cp snd-hda-codec-cs8409.ko /output/
|
cp snd-hda-codec-cs8409.ko /output/
|
||||||
|
|
||||||
|
# --- Builds Kickoff (the start menu) with our update bar. ---
|
||||||
|
# Kickoff is QML packed into one plugin of plasma-desktop. This rebuilds
|
||||||
|
# just that plugin from the same plasma-desktop version the image ships
|
||||||
|
# (KDE's source, via the forge's mirror), with patches/kickoff/ on top, and
|
||||||
|
# the final stage puts it in place of the stock one. Like kmod-builder, it's
|
||||||
|
# built FROM BASE_IMAGE so it can't drift from the Plasma it runs on; the
|
||||||
|
# -devel packages are pinned to the installed versions (older ones come
|
||||||
|
# from Fedora's updates-archive).
|
||||||
|
FROM ${BASE_IMAGE} AS kickoff-builder
|
||||||
|
|
||||||
|
COPY patches/kickoff/ /tmp/kickoff/
|
||||||
|
|
||||||
|
RUN set -eux; \
|
||||||
|
v() { rpm -q --qf '%{VERSION}-%{RELEASE}' "$1"; }; \
|
||||||
|
devel=(cmake gcc-c++ extra-cmake-modules patch \
|
||||||
|
"libplasma-devel-$(v libplasma)" \
|
||||||
|
"qt6-qtbase-devel-$(v qt6-qtbase)" \
|
||||||
|
"qt6-qtdeclarative-devel-$(v qt6-qtdeclarative)"); \
|
||||||
|
for f in kcoreaddons kconfig kpackage kwindowsystem kservice ki18n kguiaddons knotifications; do \
|
||||||
|
devel+=("kf6-$f-devel-$(v "kf6-$f")"); \
|
||||||
|
done; \
|
||||||
|
dnf install -y --setopt=install_weak_deps=False "${devel[@]}"; \
|
||||||
|
V="$(rpm -q --qf '%{VERSION}' plasma-desktop)"; \
|
||||||
|
echo "Building Kickoff from plasma-desktop ${V}"; \
|
||||||
|
curl -fsSL --retry 3 -o /tmp/plasma-desktop.tar.gz \
|
||||||
|
"https://forge.quinefoundation.com/quine-global-labs/mirror-kde-plasma-desktop/archive/v${V}.tar.gz"; \
|
||||||
|
mkdir -p /build-src; \
|
||||||
|
tar -xzf /tmp/plasma-desktop.tar.gz -C /build-src --strip-components=2 \
|
||||||
|
mirror-kde-plasma-desktop/applets/kickoff; \
|
||||||
|
cp /tmp/kickoff/CMakeLists.txt /build-src/; \
|
||||||
|
cd /build-src; \
|
||||||
|
for p in /tmp/kickoff/*.patch; do patch -p1 --fuzz=0 < "$p"; done; \
|
||||||
|
cmake -S /build-src -B /build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr; \
|
||||||
|
cmake --build /build -j"$(nproc)"; \
|
||||||
|
DESTDIR=/out cmake --install /build --strip; \
|
||||||
|
ls -l /out/usr/lib64/qt6/plugins/plasma/applets/org.kde.plasma.kickoff.so
|
||||||
|
|
||||||
# --- Final image. ---
|
# --- Final image. ---
|
||||||
FROM ${BASE_IMAGE} AS final
|
FROM ${BASE_IMAGE} AS final
|
||||||
|
|
||||||
|
|
@ -67,6 +104,12 @@ RUN dnf install -y \
|
||||||
--setopt=quine.gpgkey=https://forge.quinefoundation.com/api/packages/quine-global-labs/rpm/repository.key \
|
--setopt=quine.gpgkey=https://forge.quinefoundation.com/api/packages/quine-global-labs/rpm/repository.key \
|
||||||
layer-wizard dnf-package-store swap-watchdog
|
layer-wizard dnf-package-store swap-watchdog
|
||||||
|
|
||||||
|
# Kickoff with our update bar (kickoff-builder, above), over plasma-desktop's.
|
||||||
|
COPY --from=kickoff-builder /out/usr/lib64/qt6/plugins/plasma/applets/org.kde.plasma.kickoff.so \
|
||||||
|
/usr/lib64/qt6/plugins/plasma/applets/org.kde.plasma.kickoff.so
|
||||||
|
|
||||||
# System-level units are enabled explicitly. User units are picked up
|
# System-level units are enabled explicitly. User units are picked up
|
||||||
# automatically via overlay/usr/lib/systemd/user-preset/.
|
# automatically via overlay/usr/lib/systemd/user-preset/.
|
||||||
RUN systemctl enable rpm-ostree-cleanup.timer cs8409-intmike-power-fixup.service
|
# quine-update-check.timer: the status Kickoff's update bar shows.
|
||||||
|
RUN systemctl enable rpm-ostree-cleanup.timer cs8409-intmike-power-fixup.service \
|
||||||
|
quine-update-check.timer
|
||||||
|
|
|
||||||
10
overlay/usr/lib/systemd/system/quine-update-check.service
Normal file
10
overlay/usr/lib/systemd/system/quine-update-check.service
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Check whether a newer Quine Linux is available
|
||||||
|
# Only on an installed (bootc) system, not the live ISO's session.
|
||||||
|
ConditionPathExists=/run/ostree-booted
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/libexec/quine-linux/update-check
|
||||||
10
overlay/usr/lib/systemd/system/quine-update-check.timer
Normal file
10
overlay/usr/lib/systemd/system/quine-update-check.timer
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Check for a newer Quine Linux a few minutes after boot, then every 4 hours
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=3min
|
||||||
|
OnUnitActiveSec=4h
|
||||||
|
RandomizedDelaySec=10min
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
# After Aurora's nightly update (uupd) downloads a new image, refresh the
|
||||||
|
# status Kickoff's update bar shows: "Restart to finish updating".
|
||||||
|
[Unit]
|
||||||
|
OnSuccess=quine-update-check.service
|
||||||
47
overlay/usr/libexec/quine-linux/update-check
Executable file
47
overlay/usr/libexec/quine-linux/update-check
Executable file
|
|
@ -0,0 +1,47 @@
|
||||||
|
#!/usr/bin/bash
|
||||||
|
# Is a newer Quine Linux available, or downloaded and waiting for a restart?
|
||||||
|
# Writes the answer to /run/quine-linux/update-status.json, which Kickoff's
|
||||||
|
# update bar reads (patches/kickoff). Run as root by
|
||||||
|
# quine-update-check.service: bootc needs root.
|
||||||
|
#
|
||||||
|
# state: "staged" an update is downloaded; it applies at the next boot
|
||||||
|
# "available" the registry has a newer image than the running one
|
||||||
|
# "current" neither
|
||||||
|
# new: the staged or available image's version and build time
|
||||||
|
# error: why the registry check failed, if it did (state then falls back
|
||||||
|
# to what bootc already knew)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
out_dir=/run/quine-linux
|
||||||
|
out="$out_dir/update-status.json"
|
||||||
|
mkdir -p "$out_dir"
|
||||||
|
|
||||||
|
# Fetches the image's manifest and config only, not its layers, and records
|
||||||
|
# what it found as the booted entry's cachedUpdate.
|
||||||
|
error=""
|
||||||
|
if ! check_output=$(bootc upgrade --check 2>&1); then
|
||||||
|
error=$(tail -n 1 <<<"$check_output")
|
||||||
|
fi
|
||||||
|
|
||||||
|
bootc status --json | jq --arg error "$error" --arg checked "$(date -Is)" '
|
||||||
|
.status as $s
|
||||||
|
| ($s.booted.image.imageDigest // null) as $booted
|
||||||
|
| ($s.staged.image // null) as $staged
|
||||||
|
| ($s.booted.cachedUpdate // null) as $cached
|
||||||
|
| {
|
||||||
|
checked: $checked,
|
||||||
|
error: (if $error == "" then null else $error end),
|
||||||
|
current: {version: $s.booted.image.version, timestamp: $s.booted.image.timestamp},
|
||||||
|
state: (
|
||||||
|
if $staged != null and $staged.imageDigest != $booted then "staged"
|
||||||
|
elif $cached != null and $cached.imageDigest != $booted then "available"
|
||||||
|
else "current" end),
|
||||||
|
new: (
|
||||||
|
if $staged != null and $staged.imageDigest != $booted then $staged
|
||||||
|
elif $cached != null and $cached.imageDigest != $booted then $cached
|
||||||
|
else null end
|
||||||
|
| if . == null then null else {version, timestamp} end)
|
||||||
|
}' >"$out.tmp"
|
||||||
|
chmod 0644 "$out.tmp"
|
||||||
|
mv "$out.tmp" "$out"
|
||||||
|
jq -c '{state, new, error}' "$out"
|
||||||
132
patches/kickoff/0001-update-bar.patch
Normal file
132
patches/kickoff/0001-update-bar.patch
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
diff --git a/kickoff/CMakeLists.txt b/kickoff/CMakeLists.txt
|
||||||
|
index 70e361c..d860cb4 100644
|
||||||
|
--- a/kickoff/CMakeLists.txt
|
||||||
|
+++ b/kickoff/CMakeLists.txt
|
||||||
|
@@ -26,6 +26,7 @@ QML_SOURCES
|
||||||
|
ListOfGridsView.qml
|
||||||
|
ListOfGridsViewDelegate.qml
|
||||||
|
NormalPage.qml
|
||||||
|
+ UpdateBar.qml
|
||||||
|
PlacesPage.qml
|
||||||
|
SectionView.qml
|
||||||
|
VerticalStackView.qml
|
||||||
|
diff --git a/kickoff/FullRepresentation.qml b/kickoff/FullRepresentation.qml
|
||||||
|
index aa25d3c..7d9a6bb 100644
|
||||||
|
--- a/kickoff/FullRepresentation.qml
|
||||||
|
+++ b/kickoff/FullRepresentation.qml
|
||||||
|
@@ -77,6 +77,9 @@ EmptyPage {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+ // Quine Linux: system update status (UpdateBar.qml).
|
||||||
|
+ footer: UpdateBar {}
|
||||||
|
+
|
||||||
|
contentItem: VerticalStackView {
|
||||||
|
id: contentItemStackView
|
||||||
|
focus: true
|
||||||
|
diff --git a/kickoff/UpdateBar.qml b/kickoff/UpdateBar.qml
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000..3ebe376
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/kickoff/UpdateBar.qml
|
||||||
|
@@ -0,0 +1,100 @@
|
||||||
|
+/*
|
||||||
|
+ SPDX-FileCopyrightText: 2026 Quine Foundation
|
||||||
|
+ SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
+*/
|
||||||
|
+
|
||||||
|
+// Quine Linux: a bar at the bottom of Kickoff when a newer system image is
|
||||||
|
+// available, or downloaded and waiting for a restart. It reads the status
|
||||||
|
+// that quine-update-check.service writes as root (quine-linux's overlay,
|
||||||
|
+// /usr/libexec/quine-linux/update-check), and is hidden otherwise.
|
||||||
|
+
|
||||||
|
+pragma ComponentBehavior: Bound
|
||||||
|
+
|
||||||
|
+import QtQuick
|
||||||
|
+import org.kde.kirigami as Kirigami
|
||||||
|
+import org.kde.plasma.plasma5support as P5Support
|
||||||
|
+import org.kde.plasma.private.sessions as Sessions
|
||||||
|
+
|
||||||
|
+Kirigami.InlineMessage {
|
||||||
|
+ id: root
|
||||||
|
+
|
||||||
|
+ readonly property string statusFile: "/run/quine-linux/update-status.json"
|
||||||
|
+ readonly property string readCommand: "cat " + statusFile
|
||||||
|
+ // "current", "available" or "staged"; anything else hides the bar.
|
||||||
|
+ property string updateState: ""
|
||||||
|
+ property string newBuild: ""
|
||||||
|
+
|
||||||
|
+ position: Kirigami.InlineMessage.Position.Footer
|
||||||
|
+ // Hidden, InlineMessage's implicit height is 0, so the menu doesn't keep
|
||||||
|
+ // room for it (EmptyPage sizes by the footer's implicit height).
|
||||||
|
+ visible: updateState === "available" || updateState === "staged"
|
||||||
|
+ showCloseButton: false
|
||||||
|
+ type: updateState === "staged" ? Kirigami.MessageType.Positive : Kirigami.MessageType.Information
|
||||||
|
+ icon.name: updateState === "staged" ? "system-reboot-update" : "system-software-update"
|
||||||
|
+ text: updateState === "staged"
|
||||||
|
+ ? i18nc("@info", "An update is ready. Restart to finish updating.")
|
||||||
|
+ : i18nc("@info", "A system update is available%1.",
|
||||||
|
+ newBuild ? i18nc("@info build date", " (built %1)", newBuild) : "")
|
||||||
|
+
|
||||||
|
+ actions: [
|
||||||
|
+ Kirigami.Action {
|
||||||
|
+ visible: root.updateState === "available"
|
||||||
|
+ text: i18nc("@action:button", "Update Now")
|
||||||
|
+ icon.name: "system-software-update"
|
||||||
|
+ // Aurora's System Update: `ujust update` in a terminal.
|
||||||
|
+ onTriggered: {
|
||||||
|
+ executable.connectSource("kioclient exec /usr/share/applications/dev.getaurora.system-update.desktop")
|
||||||
|
+ kickoff.expanded = false
|
||||||
|
+ }
|
||||||
|
+ },
|
||||||
|
+ Kirigami.Action {
|
||||||
|
+ visible: root.updateState === "staged" && sessions.canReboot
|
||||||
|
+ text: i18nc("@action:button", "Restart")
|
||||||
|
+ icon.name: "system-reboot"
|
||||||
|
+ onTriggered: {
|
||||||
|
+ kickoff.expanded = false
|
||||||
|
+ sessions.requestReboot()
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ]
|
||||||
|
+
|
||||||
|
+ Sessions.SessionManagement {
|
||||||
|
+ id: sessions
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ P5Support.DataSource {
|
||||||
|
+ id: executable
|
||||||
|
+ engine: "executable"
|
||||||
|
+ connectedSources: []
|
||||||
|
+ onNewData: (source, data) => {
|
||||||
|
+ disconnectSource(source)
|
||||||
|
+ if (source !== root.readCommand) {
|
||||||
|
+ return
|
||||||
|
+ }
|
||||||
|
+ let status = null
|
||||||
|
+ try {
|
||||||
|
+ status = JSON.parse(data["stdout"])
|
||||||
|
+ } catch (e) {
|
||||||
|
+ // No file yet (the first check hasn't run, or a live session).
|
||||||
|
+ }
|
||||||
|
+ root.updateState = status?.state ?? ""
|
||||||
|
+ const built = status?.new?.timestamp
|
||||||
|
+ root.newBuild = built ? Qt.formatDate(new Date(built), Qt.locale(), Locale.ShortFormat) : ""
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ function refresh() {
|
||||||
|
+ executable.connectSource(readCommand)
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ Component.onCompleted: refresh()
|
||||||
|
+
|
||||||
|
+ Connections {
|
||||||
|
+ target: kickoff
|
||||||
|
+ function onExpandedChanged() {
|
||||||
|
+ if (kickoff.expanded) {
|
||||||
|
+ root.refresh()
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
19
patches/kickoff/CMakeLists.txt
Normal file
19
patches/kickoff/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Builds Kickoff on its own, outside plasma-desktop's tree: the Containerfile's
|
||||||
|
# kickoff-builder stage puts plasma-desktop's applets/kickoff/ next to this
|
||||||
|
# file as kickoff/. The rest of plasma-desktop's top-level CMakeLists.txt
|
||||||
|
# isn't needed for this one applet.
|
||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
project(kickoff)
|
||||||
|
# ECM's QML helpers otherwise pick their Qt5 variant.
|
||||||
|
set(QT_MAJOR_VERSION 6)
|
||||||
|
find_package(ECM REQUIRED NO_MODULE)
|
||||||
|
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
|
||||||
|
find_package(Qt6 REQUIRED COMPONENTS Core Quick Qml)
|
||||||
|
include(KDEInstallDirs)
|
||||||
|
include(KDECMakeSettings)
|
||||||
|
include(KDECompilerSettings NO_POLICY_SCOPE)
|
||||||
|
include(ECMQmlModule)
|
||||||
|
# Plasma's CMake package needs these found first.
|
||||||
|
find_package(KF6 REQUIRED COMPONENTS Config CoreAddons Package I18n)
|
||||||
|
find_package(Plasma REQUIRED)
|
||||||
|
add_subdirectory(kickoff)
|
||||||
Loading…
Reference in a new issue