From d49a486b4bcb34f9e08ab0068ed6083e501a377c Mon Sep 17 00:00:00 2001 From: Philip Peterson <1326208+philip-peterson@users.noreply.github.com> Date: Sat, 10 Oct 2026 10:46:35 -0800 Subject: [PATCH] Resolve virtual provides and surface engine failures in package check dnf repoquery only matches exact package names, so virtual provides like "vim" (satisfied by vim-enhanced etc.) falsely failed the Check step even though rpm-ostree install would resolve them fine. It also silently reported a broken dnf (e.g. a plugin enabled in config but missing on disk) as "package not found" instead of flagging it as its own distinct failure. Co-Authored-By: Claude Sonnet 5 --- lib/ostree_service.dart | 32 +++++++++++++++++++++++++++++--- lib/wizard_page.dart | 23 ++++++++++++++++++++++- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/lib/ostree_service.dart b/lib/ostree_service.dart index 841b7f0..d34e06a 100644 --- a/lib/ostree_service.dart +++ b/lib/ostree_service.dart @@ -1,6 +1,18 @@ import 'dart:convert'; import 'dart:io'; +/// Thrown when `dnf` itself fails to run (e.g. a plugin that's enabled in +/// config but missing on disk) rather than genuinely finding no match. +/// Distinct from "package not found" so the UI can tell a broken search +/// engine apart from a bad package name. +class PackageEngineException implements Exception { + final String stderr; + PackageEngineException(this.stderr); + + @override + String toString() => stderr.isEmpty ? 'dnf failed to run' : stderr; +} + class DeploymentInfo { final String version; final String checksum; @@ -81,10 +93,24 @@ class OstreeService { } /// Unprivileged existence check so typos surface before escalating to - /// pkexec. Matches on exact name or name.arch. + /// pkexec. Matches on exact name or name.arch, then falls back to + /// `--whatprovides` so virtual names like "vim" (satisfied by + /// vim-enhanced etc., never a package themselves) check out the same way + /// `rpm-ostree install` would actually resolve them. A nonzero exit means + /// dnf itself failed to run (e.g. a broken plugin) rather than "not + /// found", so that's thrown instead of silently reported as false. static Future checkPackageExists(String name) async { - final result = await Process.run('dnf', ['repoquery', name]); - return result.exitCode == 0 && (result.stdout as String).trim().isNotEmpty; + final byName = await Process.run('dnf', ['repoquery', name]); + if (byName.exitCode != 0) { + throw PackageEngineException((byName.stderr as String).trim()); + } + if ((byName.stdout as String).trim().isNotEmpty) return true; + + final byProvides = await Process.run('dnf', ['repoquery', '--whatprovides', name]); + if (byProvides.exitCode != 0) { + throw PackageEngineException((byProvides.stderr as String).trim()); + } + return (byProvides.stdout as String).trim().isNotEmpty; } /// Permanently layers a package via `rpm-ostree install`. Streams combined diff --git a/lib/wizard_page.dart b/lib/wizard_page.dart index 7c68914..3f9384f 100644 --- a/lib/wizard_page.dart +++ b/lib/wizard_page.dart @@ -20,6 +20,7 @@ class _WizardHomePageState extends State { Future? _statusFuture; bool _checking = false; String? _checkError; + bool _checkErrorIsEngineFailure = false; bool _packageVerified = false; _ApplyState _applyState = _ApplyState.idle; @@ -39,6 +40,7 @@ class _WizardHomePageState extends State { setState(() { _checking = true; _checkError = null; + _checkErrorIsEngineFailure = false; _packageVerified = false; }); try { @@ -47,6 +49,12 @@ class _WizardHomePageState extends State { _packageVerified = exists; _checkError = exists ? null : 'No package named "$name" was found.'; }); + } on PackageEngineException catch (e) { + setState(() { + _checkErrorIsEngineFailure = true; + _checkError = 'Package search isn\'t working right now, so this ' + 'can\'t be verified: $e'; + }); } catch (e) { setState(() => _checkError = e.toString()); } finally { @@ -220,7 +228,20 @@ class _WizardHomePageState extends State { if (_checkError != null) Padding( padding: const EdgeInsets.only(top: 8), - child: Text(_checkError!, style: TextStyle(color: Theme.of(context).colorScheme.error)), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + _checkErrorIsEngineFailure ? Icons.warning_amber : Icons.info_outline, + color: Theme.of(context).colorScheme.error, + size: 18, + ), + const SizedBox(width: 6), + Expanded( + child: Text(_checkError!, style: TextStyle(color: Theme.of(context).colorScheme.error)), + ), + ], + ), ), ], );