File indexing completed on 2024-06-02 05:34:38
0001 /* 0002 * SPDX-FileCopyrightText: 2021 Mariam Fahmy Sobhy <mariamfahmy66@gmail.com> 0003 * 0004 * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0005 */ 0006 0007 #include "RpmOstreeResource.h" 0008 #include "RpmOstreeBackend.h" 0009 0010 #include <appstream/AppStreamIntegration.h> 0011 0012 #include <KLocalizedString> 0013 #include <KOSRelease> 0014 0015 #include <ostree-repo.h> 0016 #include <ostree.h> 0017 0018 RpmOstreeResource::RpmOstreeResource(const QVariantMap &map, RpmOstreeBackend *parent) 0019 : AbstractResource(parent) 0020 // All available deployments are by definition already installed 0021 , m_state(AbstractResource::Installed) 0022 { 0023 #ifdef QT_DEBUG 0024 qDebug() << "rpm-ostree-backend: Creating deployments from:"; 0025 QMapIterator<QString, QVariant> iter(map); 0026 while (iter.hasNext()) { 0027 iter.next(); 0028 qDebug() << "rpm-ostree-backend: " << iter.key() << ": " << iter.value(); 0029 } 0030 qDebug() << ""; 0031 #endif 0032 0033 // Get as much as possible from rpm-ostree 0034 m_osname = map.value(QLatin1String("osname")).toString(); 0035 0036 // Look for the base-checksum first. This is the case where we have changes layered 0037 m_checksum = map.value(QLatin1String("base-checksum")).toString(); 0038 if (m_checksum.isEmpty()) { 0039 // If empty, look for the regular checksum (no layered changes) 0040 m_checksum = map.value(QLatin1String("checksum")).toString(); 0041 } 0042 0043 // Look for the base-version first. This is the case where we have changes layered 0044 m_version = map.value(QLatin1String("base-version")).toString(); 0045 if (m_version.isEmpty()) { 0046 // If empty, look for the regular version (no layered changes) 0047 m_version = map.value(QLatin1String("version")).toString(); 0048 } 0049 0050 // Look for the base-timestamp first. This is the case where we have changes layered 0051 auto timestamp = map.value(QLatin1String("base-timestamp")).toULongLong(); 0052 if (timestamp == 0) { 0053 // If "empty", look for the regular timestamp (no layered changes) 0054 timestamp = map.value(QLatin1String("timestamp")).toULongLong(); 0055 } 0056 if (timestamp == 0) { 0057 // If it's still empty, set an "empty" date 0058 m_timestamp = QDate(); 0059 } else { 0060 // Otherwise, convert the timestamp to a date 0061 m_timestamp = QDateTime::fromSecsSinceEpoch(timestamp).date(); 0062 } 0063 0064 m_pinned = map.value(QLatin1String("pinned")).toBool(); 0065 m_pending = map.value(QLatin1String("staged")).toBool(); 0066 m_booted = map.value(QLatin1String("booted")).toBool(); 0067 0068 if (m_booted) { 0069 // We can directly read the pretty name & variant from os-release 0070 // information if this is the currently booted deployment. 0071 auto osrelease = AppStreamIntegration::global()->osRelease(); 0072 m_name = osrelease->name(); 0073 m_variant = osrelease->variant(); 0074 // Also extract the version if we could not find it earlier 0075 if (m_version.isEmpty()) { 0076 m_version = osrelease->versionId(); 0077 } 0078 } 0079 0080 // Look for "classic" ostree origin format first 0081 QString origin = map.value(QLatin1String("origin")).toString(); 0082 if (!origin.isEmpty()) { 0083 m_ostreeFormat.reset(new OstreeFormat(OstreeFormat::Format::Classic, origin)); 0084 if (!m_ostreeFormat->isValid()) { 0085 // This should never happen 0086 qWarning() << "rpm-ostree-backend: Invalid origin for classic ostree format:" << origin; 0087 } 0088 } else { 0089 // Then look for OCI container format 0090 origin = map.value(QLatin1String("container-image-reference")).toString(); 0091 if (!origin.isEmpty()) { 0092 m_ostreeFormat.reset(new OstreeFormat(OstreeFormat::Format::OCI, origin)); 0093 if (!m_ostreeFormat->isValid()) { 0094 // This should never happen 0095 qWarning() << "rpm-ostree-backend: Invalid reference for OCI container ostree format:" << origin; 0096 } 0097 } else { 0098 // This should never happen 0099 m_ostreeFormat.reset(new OstreeFormat(OstreeFormat::Format::Unknown, {})); 0100 qWarning() << "rpm-ostree-backend: Could not find a valid remote for this deployment:" << m_checksum; 0101 } 0102 } 0103 0104 // Use ostree as tld for all ostree deployments and differentiate between them with the 0105 // remote/repo, ref/tag and commit.. 0106 // Example: ostree.fedora.fedora-34-x86-64-kinoite.abcd1234567890 0107 // Example: ostree.quay.io-fedora-ostree-desktops-kinoite.abcd1234567890 0108 // https://freedesktop.org/software/appstream/docs/chap-Metadata.html#tag-id-generic 0109 if (m_ostreeFormat->isClassic()) { 0110 m_appstreamid = m_ostreeFormat->remote() + QLatin1String(".") + m_ostreeFormat->ref(); 0111 } else if (m_ostreeFormat->isOCI()) { 0112 m_appstreamid = m_ostreeFormat->repo() + QLatin1String(".") + m_ostreeFormat->tag(); 0113 } else { 0114 m_appstreamid = QString(); 0115 } 0116 m_appstreamid = QStringLiteral("ostree.") + m_appstreamid.replace(QLatin1Char('/'), QLatin1Char('-')).replace(QLatin1Char('_'), QLatin1Char('-')) 0117 + QLatin1String(".") + m_checksum; 0118 #ifdef QT_DEBUG 0119 qInfo() << "rpm-ostree-backend: Found deployment:" << m_appstreamid; 0120 #endif 0121 0122 // Replaced & added packages 0123 m_requested_base_local_replacements = map.value(QLatin1String("requested-base-local-replacements")).toStringList(); 0124 m_requested_base_removals = map.value(QLatin1String("requested-base-removals")).toStringList(); 0125 m_requested_local_packages = map.value(QLatin1String("requested-local-packages")).toStringList(); 0126 m_requested_modules = map.value(QLatin1String("requested-modules")).toStringList(); 0127 m_requested_packages = map.value(QLatin1String("requested-packages")).toStringList(); 0128 m_requested_base_local_replacements.sort(); 0129 m_requested_base_removals.sort(); 0130 m_requested_local_packages.sort(); 0131 m_requested_modules.sort(); 0132 m_requested_packages.sort(); 0133 0134 // TODO: Extract signature information 0135 } 0136 0137 bool RpmOstreeResource::setNewMajorVersion(const QString &newMajorVersion) 0138 { 0139 if (!m_ostreeFormat->isValid()) { 0140 // Only operate on valid origin format 0141 return false; 0142 } 0143 0144 // This check mostly makes sense for the classic Ostree format. Skip most of it for the 0145 // OCI format case: it will fail later if the container tag does not exist. 0146 if (m_ostreeFormat->isOCI()) { 0147 // If we are using the latest tag then it means that we are not following a specific 0148 // major release and thus we don't need to rebase: it will automatically happen once 0149 // the latest tag points to a version build with the new major release. 0150 if (m_ostreeFormat->tag() == QLatin1String("latest")) { 0151 return false; 0152 } 0153 0154 // Set the new major version 0155 m_nextMajorVersion = newMajorVersion; 0156 // Replace the current version in the container tag by the new major version to find 0157 // the new tag to rebase to. This assumes that container tag names are lowercase. 0158 QString currentVersion = AppStreamIntegration::global()->osRelease()->versionId(); 0159 m_nextMajorVersionRef = m_ostreeFormat->tag().replace(currentVersion, newMajorVersion.toLower(), Qt::CaseInsensitive); 0160 return true; 0161 } 0162 0163 // Assume we're using the classic format from now on 0164 if (!m_ostreeFormat->isClassic()) { 0165 // Only operate on valid origin format 0166 return false; 0167 } 0168 0169 // Fetch the list of refs available on the remote for the deployment 0170 g_autoptr(GFile) path = g_file_new_for_path("/ostree/repo"); 0171 g_autoptr(OstreeRepo) repo = ostree_repo_new(path); 0172 if (repo == NULL) { 0173 qWarning() << "rpm-ostree-backend: Could not find ostree repo:" << path; 0174 return false; 0175 } 0176 0177 g_autoptr(GError) err = NULL; 0178 gboolean res = ostree_repo_open(repo, NULL, &err); 0179 if (!res) { 0180 qWarning() << "rpm-ostree-backend: Could not open ostree repo:" << path; 0181 return false; 0182 } 0183 0184 g_autoptr(GHashTable) refs; 0185 QByteArray rem = m_ostreeFormat->remote().toLocal8Bit(); 0186 res = ostree_repo_remote_list_refs(repo, rem.data(), &refs, NULL, &err); 0187 if (!res) { 0188 qWarning() << "rpm-ostree-backend: Could not get the list of refs for ostree repo:" << path; 0189 return false; 0190 } 0191 0192 // Replace the current version in current branch by the new major version to find the 0193 // new branch. This assumes that ostree branch names are lowercase. 0194 QString currentVersion = AppStreamIntegration::global()->osRelease()->versionId(); 0195 QString newVersionBranch = m_ostreeFormat->ref().replace(currentVersion, newMajorVersion.toLower(), Qt::CaseInsensitive); 0196 0197 // Iterate over the remote refs to verify that the new verions has a branch available 0198 GHashTableIter iter; 0199 gpointer key, value; 0200 g_hash_table_iter_init(&iter, refs); 0201 while (g_hash_table_iter_next(&iter, &key, &value)) { 0202 auto ref = QString::fromUtf8((char *)key); 0203 if (ref == newVersionBranch) { 0204 m_nextMajorVersion = newMajorVersion; 0205 m_nextMajorVersionRef = newVersionBranch; 0206 return true; 0207 } 0208 } 0209 0210 // If we reach here, it means that we could not find a matching branch. This 0211 // is unexpected and we should inform the user. 0212 qWarning() << "rpm-ostree-backend: Could not find a remote ref for the new major version in ostree repo"; 0213 return false; 0214 } 0215 0216 QString RpmOstreeResource::availableVersion() const 0217 { 0218 return m_newVersion; 0219 } 0220 0221 QString RpmOstreeResource::version() 0222 { 0223 return m_version; 0224 } 0225 0226 void RpmOstreeResource::setNewVersion(const QString &newVersion) 0227 { 0228 m_newVersion = newVersion; 0229 } 0230 0231 QString RpmOstreeResource::getNewVersion() const 0232 { 0233 return m_newVersion; 0234 } 0235 0236 QString RpmOstreeResource::getNextMajorVersion() const 0237 { 0238 return m_nextMajorVersion; 0239 } 0240 0241 QString RpmOstreeResource::getNextMajorVersionRef() const 0242 { 0243 return m_nextMajorVersionRef; 0244 } 0245 0246 QString RpmOstreeResource::appstreamId() const 0247 { 0248 return m_appstreamid; 0249 } 0250 0251 bool RpmOstreeResource::canExecute() const 0252 { 0253 return false; 0254 } 0255 0256 QVariant RpmOstreeResource::icon() const 0257 { 0258 return QStringLiteral("application-x-rpm"); 0259 } 0260 0261 QString RpmOstreeResource::installedVersion() const 0262 { 0263 return m_version; 0264 } 0265 0266 QUrl RpmOstreeResource::url() const 0267 { 0268 return QUrl(); 0269 } 0270 0271 QUrl RpmOstreeResource::donationURL() 0272 { 0273 return QUrl(); 0274 } 0275 0276 QUrl RpmOstreeResource::homepage() 0277 { 0278 return QUrl(AppStreamIntegration::global()->osRelease()->homeUrl()); 0279 } 0280 0281 QUrl RpmOstreeResource::helpURL() 0282 { 0283 return QUrl(AppStreamIntegration::global()->osRelease()->documentationUrl()); 0284 } 0285 0286 QUrl RpmOstreeResource::bugURL() 0287 { 0288 return QUrl(AppStreamIntegration::global()->osRelease()->bugReportUrl()); 0289 } 0290 0291 QJsonArray RpmOstreeResource::licenses() 0292 { 0293 if (m_osname == QLatin1String("fedora")) { 0294 return {QJsonObject{{QStringLiteral("name"), i18n("GPL and other licenses")}, 0295 {QStringLiteral("url"), QStringLiteral("https://fedoraproject.org/wiki/Legal:Licenses")}}}; 0296 } 0297 return {QJsonObject{{QStringLiteral("name"), i18n("Unknown")}}}; 0298 } 0299 0300 QString RpmOstreeResource::longDescription() 0301 { 0302 QString desc; 0303 if (!m_requested_packages.isEmpty()) { 0304 QTextStream(&desc) << i18n("Additional packages: ") << "\n<ul>"; 0305 for (const QString &package : std::as_const(m_requested_packages)) { 0306 QTextStream(&desc) << "<li>" << package << "</li>\n"; 0307 } 0308 QTextStream(&desc) << "</ul>\n"; 0309 } 0310 if (!m_requested_modules.isEmpty()) { 0311 QTextStream(&desc) << i18n("Additional modules: ") << "\n<ul>"; 0312 for (const QString &package : std::as_const(m_requested_modules)) { 0313 QTextStream(&desc) << "<li>" << package << "</li>\n"; 0314 } 0315 QTextStream(&desc) << "</ul>\n"; 0316 } 0317 if (!m_requested_local_packages.isEmpty()) { 0318 QTextStream(&desc) << i18n("Local packages: ") << "\n<ul>"; 0319 for (const QString &package : std::as_const(m_requested_local_packages)) { 0320 QTextStream(&desc) << "<li>" << package << "</li>\n"; 0321 } 0322 QTextStream(&desc) << "</ul>\n"; 0323 } 0324 if (!m_requested_base_local_replacements.isEmpty()) { 0325 QTextStream(&desc) << i18n("Replaced packages:") << "\n<ul>"; 0326 for (const QString &package : std::as_const(m_requested_base_local_replacements)) { 0327 QTextStream(&desc) << "<li>" << package << "</li>\n"; 0328 } 0329 QTextStream(&desc) << "</ul>\n"; 0330 } 0331 if (!m_requested_base_removals.isEmpty()) { 0332 QTextStream(&desc) << i18n("Removed packages:") << "\n<ul>"; 0333 for (const QString &package : std::as_const(m_requested_base_removals)) { 0334 QTextStream(&desc) << "<li>" << package << "</li>\n"; 0335 } 0336 QTextStream(&desc) << "</ul>\n"; 0337 } 0338 if (m_pinned) { 0339 desc += QStringLiteral("<br/>This version is pinned and won't be automatically removed on updates."); 0340 } 0341 return desc; 0342 } 0343 0344 QString RpmOstreeResource::name() const 0345 { 0346 return QStringLiteral("%1 %2").arg(packageName(), m_version); 0347 } 0348 0349 QString RpmOstreeResource::origin() const 0350 { 0351 if (m_ostreeFormat->isClassic()) { 0352 if (m_ostreeFormat->remote() == QLatin1String("fedora")) { 0353 return QStringLiteral("Fedora Project"); 0354 } else { 0355 return m_ostreeFormat->remote(); 0356 } 0357 } else if (m_ostreeFormat->isOCI()) { 0358 return m_ostreeFormat->repo(); 0359 } 0360 return i18n("Unknown"); 0361 } 0362 0363 QString RpmOstreeResource::packageName() const 0364 { 0365 if (m_osname == QLatin1String("fedora")) { 0366 return QStringLiteral("Fedora Kinoite"); 0367 } 0368 return m_osname; 0369 } 0370 0371 QString RpmOstreeResource::section() 0372 { 0373 return {}; 0374 } 0375 0376 AbstractResource::State RpmOstreeResource::state() 0377 { 0378 return m_state; 0379 } 0380 0381 QString RpmOstreeResource::author() const 0382 { 0383 if (m_osname == QLatin1String("fedora")) { 0384 return QStringLiteral("Fedora Project"); 0385 } 0386 return i18n("Unknown"); 0387 } 0388 0389 QString RpmOstreeResource::comment() 0390 { 0391 if (m_booted) { 0392 if (m_pinned) { 0393 return i18n("Currently booted version (pinned)"); 0394 } else { 0395 return i18n("Currently booted version"); 0396 } 0397 } else if (m_pending) { 0398 return i18n("Version that will be used after reboot"); 0399 } else if (m_pinned) { 0400 return i18n("Fallback version (pinned)"); 0401 } 0402 return i18n("Fallback version"); 0403 } 0404 0405 quint64 RpmOstreeResource::size() 0406 { 0407 return 0; 0408 } 0409 0410 QString RpmOstreeResource::sizeDescription() 0411 { 0412 return QStringLiteral("Unknown"); 0413 } 0414 0415 QDate RpmOstreeResource::releaseDate() const 0416 { 0417 return m_timestamp; 0418 } 0419 0420 void RpmOstreeResource::setState(AbstractResource::State state) 0421 { 0422 m_state = state; 0423 Q_EMIT stateChanged(); 0424 } 0425 0426 QString RpmOstreeResource::sourceIcon() const 0427 { 0428 return QStringLiteral("application-x-rpm"); 0429 } 0430 0431 QStringList RpmOstreeResource::extends() const 0432 { 0433 return {}; 0434 } 0435 AbstractResource::Type RpmOstreeResource::type() const 0436 { 0437 return Technical; 0438 } 0439 0440 bool RpmOstreeResource::isRemovable() const 0441 { 0442 // TODO: Add support for pinning, un-pinning and removing a specific 0443 // deployments. Until we have that, we consider all deployments as pinned by 0444 // default (and thus non-removable). 0445 // return !m_booted && !m_pinned; 0446 return false; 0447 } 0448 0449 QList<PackageState> RpmOstreeResource::addonsInformation() 0450 { 0451 return QList<PackageState>(); 0452 } 0453 0454 QStringList RpmOstreeResource::categories() 0455 { 0456 return {}; 0457 } 0458 0459 bool RpmOstreeResource::isBooted() 0460 { 0461 return m_booted; 0462 } 0463 0464 bool RpmOstreeResource::isPending() 0465 { 0466 return m_pending; 0467 } 0468 0469 bool RpmOstreeResource::isClassic() 0470 { 0471 return m_ostreeFormat->isValid() && m_ostreeFormat->isClassic(); 0472 } 0473 0474 bool RpmOstreeResource::isOCI() 0475 { 0476 return m_ostreeFormat->isValid() && m_ostreeFormat->isOCI(); 0477 } 0478 0479 QString RpmOstreeResource::OCIUrl() 0480 { 0481 // This will fail on non-remote transports (oci, oci-archive, containers-storage) but that's 0482 // OK as we can not check for updates in those cases. 0483 if (m_ostreeFormat->isValid() && m_ostreeFormat->isOCI()) { 0484 return QLatin1String("docker://") + m_ostreeFormat->repo() + QLatin1String(":") + m_ostreeFormat->tag(); 0485 ; 0486 } 0487 // Should never happen 0488 return {}; 0489 }