From eb2bbfbbc06744c2dd2abeb156f89b66fdad583f Mon Sep 17 00:00:00 2001 From: nicksinas Date: Fri, 16 Oct 2026 16:34:51 -0500 Subject: [PATCH 1/4] ext install: probe the rpm database, not the directory, before skipping (ENG-2757) A project with runtime rt-a installed adds rt-b. `avocado build -r rt-b` before `avocado install -r rt-b` creates the empty rt-b sysroots with `mkdir -p`; `install -r rt-b` then reads them as installed (directory exists, pins from rt-a, the per-extension stamp from rt-a matches) and skips; the next build ships empty extension images with exit 0. - ext install: the fast path and the seed decision test /var/lib/rpm, which only the seed step writes. A failed first dnf transaction removes the seeded database so the retry installs. - ext build: refuse a package extension whose sysroot has no rpm database, naming `avocado install -r `. - scheduler: register every graph task on the renderer it was given. The fallback passthrough renderer used without a TTY had no entries, so set_error kept nothing and a failed task printed only " failed". - renderer: passthrough prints a failed task's error message before its captured output, as the TTY summary does. Plain multi-runtime install and build without -r still go through the legacy extensions link; that is ENG-2691. --- src/commands/ext/build.rs | 90 ++++++++++++++++++- src/commands/ext/install.rs | 148 ++++++++++++++++++++++++++++++-- src/utils/scheduler/executor.rs | 39 +++++++++ src/utils/tui/renderer.rs | 32 ++++++- 4 files changed, 298 insertions(+), 11 deletions(-) diff --git a/src/commands/ext/build.rs b/src/commands/ext/build.rs index d168b1f1..92f7e353 100644 --- a/src/commands/ext/build.rs +++ b/src/commands/ext/build.rs @@ -16,6 +16,32 @@ use crate::utils::stamps::{ use crate::utils::target::resolve_target_required; use crate::utils::tui::{TaskId, TuiGuard}; +/// Does `extensions..packages` list at least one package? Only such an +/// extension needs an installed sysroot before it can build; an overlay-only +/// extension has nothing for dnf to install. +fn declares_packages(parsed: &serde_yaml::Value, extension: &str, target: &str) -> bool { + // Template keys such as `avocado-bsp-{{ avocado.target.board }}` are + // matched the same way the rest of the ext commands match them. + crate::commands::ext::find_ext_in_mapping(parsed, extension, target) + .and_then(|e| e.get("packages")) + .and_then(|p| p.as_mapping()) + .is_some_and(|m| !m.is_empty()) +} + +/// The error for a package extension whose sysroot was never installed. +fn packages_not_installed_message(extension: &str, runtime: Option<&str>) -> String { + match runtime { + Some(rt) => format!( + "Extension '{extension}' declares packages, but its sysroot for runtime '{rt}' \ + has no package database. Run 'avocado install -r {rt}' first." + ), + None => format!( + "Extension '{extension}' declares packages, but its sysroot has no package \ + database. Run 'avocado install' first." + ), + } +} + #[derive(Debug, Clone)] struct OverlayConfig { dir: String, @@ -289,13 +315,14 @@ impl ExtBuildCommand { // sysroot it would build — the two facts the skip below needs. let own_req = crate::utils::stamps::StampRequirement::ext_build(&self.extension); let batch_script = format!( - "{}\n{}\n{}", + "{}\n{}\n{}{}", generate_batch_read_stamps_script(&required), generate_batch_read_stamps_script(std::slice::from_ref(&own_req)), crate::utils::stamps::generate_output_listing_probe(&format!( "\"$AVOCADO_EXT_SYSROOTS/{}\"", self.extension )), + crate::commands::ext::install::sysroot_probe_line(&self.extension), ); let run_config = RunConfig { container_image: container_image.to_string(), @@ -371,6 +398,25 @@ impl ExtBuildCommand { return Err(anyhow::anyhow!("{err}")); } + // A package extension builds from the packages dnf installed into + // its sysroot. The install stamp above is per extension, not per + // runtime, so it is satisfied by another runtime's install while + // this runtime's sysroot has nothing in it. Building anyway would + // create the directory with `mkdir -p` below and ship an empty + // image with exit 0. The rpm database is the sign that an install + // reached this sysroot; without it, stop and say what to run. + if declares_packages(parsed, &self.extension, &target) + && crate::utils::stamps::parse_batch_stamps_output(batch) + .get(&format!("sysroot:{}", self.extension)) + .and_then(|v| v.as_deref()) + != Some("yes") + { + return Err(anyhow::anyhow!( + "{}", + packages_not_installed_message(&self.extension, self.runtime.as_deref()) + )); + } + // Skip: this step's own stamp is current for every input computed // above — config, compile and install script content, the // `package_files` source tree, the overlay — and the sysroot it @@ -1844,6 +1890,48 @@ fi mod tests { use super::*; + /// Only an extension with packages needs an installed sysroot before it + /// builds. Overlay-only, empty, and missing `packages:` all read as none. + #[test] + fn declares_packages_needs_a_non_empty_packages_map() { + let parsed: serde_yaml::Value = serde_yaml::from_str( + r#" +extensions: + with-pkgs: + packages: + curl: "*" + empty-pkgs: + packages: {} + overlay-only: + overlay: app/overlay + avocado-bsp-{{ avocado.target }}: + packages: + avocado-bsp: "*" +"#, + ) + .unwrap(); + let t = "qemux86-64"; + assert!(declares_packages(&parsed, "with-pkgs", t)); + assert!(!declares_packages(&parsed, "empty-pkgs", t)); + assert!(!declares_packages(&parsed, "overlay-only", t)); + assert!(!declares_packages(&parsed, "not-there", t)); + // A template key still in the mapping resolves through the same + // lookup the other ext commands use (`find_ext_in_mapping`). + assert!(declares_packages(&parsed, "avocado-bsp-qemux86-64", t)); + } + + /// The refusal tells the user the command that repairs it, scoped to the + /// runtime when one is in play. + #[test] + fn packages_not_installed_message_names_the_install_command() { + let with_rt = packages_not_installed_message("app", Some("rt-b")); + assert!(with_rt.contains("runtime 'rt-b'")); + assert!(with_rt.contains("avocado install -r rt-b")); + let no_rt = packages_not_installed_message("app", None); + assert!(no_rt.contains("Run 'avocado install' first")); + assert!(!no_rt.contains("-r")); + } + #[test] fn test_create_sysext_build_script_basic() { let cmd = ExtBuildCommand { diff --git a/src/commands/ext/install.rs b/src/commands/ext/install.rs index 98080063..af8ef095 100644 --- a/src/commands/ext/install.rs +++ b/src/commands/ext/install.rs @@ -95,6 +95,38 @@ fn stamp_allows_skip(status: &StampStatus) -> bool { /// to date" over a sysroot that no longer holds its work, and `ext image` would /// then image an empty extension. Same rule as `--no-stamps`: a step that /// destroys an output invalidates the stamp claiming it exists. +/// `[ -d /var/lib/rpm ]`: the sysroot has been seeded and dnf has a +/// database to install into. +/// +/// A bare directory test is not enough. `ext build` creates the directory +/// with `mkdir -p` when it runs ahead of install, and the install stamp is +/// per extension, not per runtime, so a directory test let a new runtime's +/// empty tree pass the fast path as installed and ship an empty image. The +/// rpm database is written by the seed step and by nothing else, so its +/// presence means an install reached dnf. Same rule as the runtime install +/// (`rpmdb_seeded_check`). +pub(crate) fn ext_sysroot_seeded_test(extension: &str) -> String { + format!("[ -d \"$AVOCADO_EXT_SYSROOTS/{extension}/var/lib/rpm\" ]") +} + +/// One line for a batch read: `sysroot::::yes|no`, keyed so +/// `parse_batch_stamps_output` returns it next to the stamps. +pub(crate) fn sysroot_probe_line(extension: &str) -> String { + format!( + "\nprintf 'sysroot:{extension}:::%s\\n' \"$({} && echo yes || echo no)\"", + ext_sysroot_seeded_test(extension) + ) +} + +/// Undo the seed after a failed first transaction. The seed copies the rpm +/// database before dnf runs; left behind, it makes an empty sysroot look +/// installed to [`ext_sysroot_seeded_test`], and a stamp written by another +/// runtime's install then skips the retry. Only the database goes: the +/// directory may hold `ext build` output that is not ours to remove. +fn drop_seeded_rpmdb_command(extension: &str) -> String { + format!("rm -rf \"$AVOCADO_EXT_SYSROOTS/{extension}/var/lib/rpm\"") +} + fn clean_ext_sysroot_command(extension: &str) -> String { format!( r#"rm -rf "$AVOCADO_EXT_SYSROOTS/{extension}" @@ -637,14 +669,13 @@ impl ExtInstallCommand { .filter(|(name, _)| stamp_safe(name)) .map(|(name, _)| StampRequirement::ext_install(name)) .collect(); - // Read the stamps AND probe each sysroot's existence in the same - // call, so a surviving stamp over a manually-removed sysroot does - // not skip a needed reinstall. + // Read the stamps AND probe each sysroot in the same call, so a + // surviving stamp over a manually-removed sysroot does not skip a + // needed reinstall. The probe is the rpm database, not the + // directory: see `ext_sysroot_seeded_test`. let mut command = generate_batch_read_stamps_script(&reqs); for (name, _) in extensions_to_install.iter().filter(|(n, _)| stamp_safe(n)) { - command.push_str(&format!( - "\nprintf 'sysroot:{name}:::%s\\n' \"$([ -d \"$AVOCADO_EXT_SYSROOTS/{name}\" ] && echo yes || echo no)\"" - )); + command.push_str(&sysroot_probe_line(name)); } let run_config = RunConfig { container_image: container_image.to_string(), @@ -1086,13 +1117,17 @@ impl ExtInstallCommand { // clean. On any failure the in-memory lock is dropped and the one on // disk still says what the sysroot actually holds. - // Does the sysroot exist? Decides whether a first pin counts as a + // Is the sysroot seeded? Decides whether a first pin counts as a // change (a cleared pin over a populated sysroot) and, further down, - // whether a fresh rpmdb has to be seeded. + // whether a fresh rpmdb has to be seeded. The test is the rpm + // database, not the directory: a directory that `ext build` created + // ahead of install has no database, and dnf against it would resolve + // the whole dependency closure into the extension instead of only + // the net-new packages. let run_config = RunConfig { container_image: container_image.to_string(), target: target.to_string(), - command: format!("[ -d $AVOCADO_EXT_SYSROOTS/{extension} ]"), + command: ext_sysroot_seeded_test(extension), verbose: self.verbose, source_environment: false, interactive: false, @@ -1507,6 +1542,41 @@ $DNF_SDK_HOST \ &format!("Failed to install dependencies for extension '{extension}'."), OutputLevel::Normal, ); + // This call seeded the database; take it back so the + // retry seeds and installs again instead of reading as + // up to date against another runtime's stamp. A sysroot + // that existed before this call keeps what it had. + if !sysroot_exists { + let run_config = RunConfig { + container_image: container_image.to_string(), + target: target.to_string(), + command: drop_seeded_rpmdb_command(extension), + verbose: self.verbose, + source_environment: false, + interactive: false, + repo_url: repo_url.cloned(), + feeds: feeds.cloned(), + repo_release: repo_release.cloned(), + container_args: merged_container_args.clone(), + dnf_args: self.dnf_args.clone(), + tui_context: effective_tui_context.clone(), + env_vars: self.runtime_env_vars(), + ..Default::default() + }; + if !run_container_command(container_helper, run_config, runs_on_context) + .await + .unwrap_or(false) + { + print_warning( + &format!( + "Could not remove the seeded rpm database for extension \ + '{extension}'; the next install may skip it. Run \ + 'avocado ext clean {extension}' before retrying." + ), + OutputLevel::Normal, + ); + } + } return Ok(false); } @@ -1617,6 +1687,66 @@ async fn run_container_command( mod tests { use super::*; + /// The fast path must not read a bare directory as an installed sysroot. + /// Run the probe under `sh`: an extension directory with no rpm database + /// (what `ext build` leaves behind) reports `no`, a seeded one reports + /// `yes`, and both land in the batch map under `sysroot:`. + #[cfg(unix)] + #[test] + fn sysroot_probe_reports_yes_only_for_a_seeded_sysroot() { + let root = tempfile::tempdir().unwrap(); + std::fs::create_dir_all(root.path().join("bare/usr/lib/extension-release.d")).unwrap(); + std::fs::create_dir_all(root.path().join("seeded/var/lib/rpm")).unwrap(); + let script = format!( + "{}{}{}", + sysroot_probe_line("bare"), + sysroot_probe_line("seeded"), + sysroot_probe_line("absent") + ); + let out = std::process::Command::new("sh") + .arg("-c") + .arg(&script) + .env("AVOCADO_EXT_SYSROOTS", root.path()) + .output() + .unwrap(); + assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr)); + let parsed = parse_batch_stamps_output(&String::from_utf8_lossy(&out.stdout)); + let get = |k: &str| parsed.get(k).and_then(|v| v.as_deref()).map(str::to_string); + assert_eq!(get("sysroot:bare").as_deref(), Some("no")); + assert_eq!(get("sysroot:seeded").as_deref(), Some("yes")); + assert_eq!(get("sysroot:absent").as_deref(), Some("no")); + } + + /// A failed first transaction takes its seeded database with it and + /// nothing else: `ext build` output already in the directory stays. + #[cfg(unix)] + #[test] + fn a_failed_first_install_drops_only_the_seeded_rpmdb() { + let root = tempfile::tempdir().unwrap(); + let ext = root.path().join("my-ext"); + std::fs::create_dir_all(ext.join("var/lib/rpm")).unwrap(); + std::fs::write(ext.join("var/lib/rpm/Packages"), b"x").unwrap(); + std::fs::create_dir_all(ext.join("usr/lib/extension-release.d")).unwrap(); + std::fs::write(ext.join("usr/lib/extension-release.d/release"), b"ID=_any").unwrap(); + let out = std::process::Command::new("sh") + .arg("-c") + .arg(drop_seeded_rpmdb_command("my-ext")) + .env("AVOCADO_EXT_SYSROOTS", root.path()) + .output() + .unwrap(); + assert!(out.status.success()); + assert!(!ext.join("var/lib/rpm").exists()); + assert!(ext.join("usr/lib/extension-release.d/release").exists()); + // And the probe now says so. + assert!(!std::process::Command::new("sh") + .arg("-c") + .arg(ext_sysroot_seeded_test("my-ext")) + .env("AVOCADO_EXT_SYSROOTS", root.path()) + .status() + .unwrap() + .success()); + } + #[test] fn fast_path_only_accepts_shell_safe_extension_names() { // Ordinary names the fast path may interpolate directly. diff --git a/src/utils/scheduler/executor.rs b/src/utils/scheduler/executor.rs index b40ef29f..65420ce2 100644 --- a/src/utils/scheduler/executor.rs +++ b/src/utils/scheduler/executor.rs @@ -36,6 +36,17 @@ impl TaskScheduler { where F: Fn(TaskId) -> Pin> + Send>> + Send + Sync + 'static, { + // Every task gets a renderer entry before it can start. The TTY + // renderer registers them up front with these same labels; the + // fallback passthrough renderer the commands hand over when there is + // no TTY does not, and `set_error` / `set_status` keep nothing for a + // task the renderer has never seen, so a failure printed " failed" + // with no reason behind it. `register_task` is idempotent, so a + // renderer that already holds the task keeps its entry and its order. + for id in self.graph.all_pending() { + self.renderer.register_task(id.clone(), id.to_string()); + } + let semaphore = Arc::new(Semaphore::new(self.max_parallel)); let spawn_task = Arc::new(spawn_task); let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<(TaskId, Result<()>)>(); @@ -126,3 +137,31 @@ impl TaskScheduler { } } } + +#[cfg(test)] +mod tests { + use super::*; + + /// A command's error must reach the renderer the scheduler was given, + /// whether or not the caller registered the tasks on it. The fallback + /// renderer used when there is no TTY arrives empty; without + /// registration the error was dropped and the output said only + /// " failed". + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn a_failing_task_keeps_its_error_on_an_unregistered_renderer() { + let id = TaskId::ExtBuild("foo".to_string()); + let mut graph = TaskGraph::new(); + graph.add_task(id.clone(), vec![]); + let renderer = Arc::new(TaskRenderer::new(true)); + let mut scheduler = TaskScheduler::new(graph, renderer.clone(), 1); + + let result = scheduler + .run(|_| Box::pin(async { Err(anyhow::anyhow!("no package database")) })) + .await; + + assert!(result.is_err()); + let (status, error) = renderer.get_task_state(&id).expect("task registered"); + assert_eq!(status, TaskStatus::Failed); + assert_eq!(error.as_deref(), Some("no package database")); + } +} diff --git a/src/utils/tui/renderer.rs b/src/utils/tui/renderer.rs index b540bc28..330497b8 100644 --- a/src/utils/tui/renderer.rs +++ b/src/utils/tui/renderer.rs @@ -592,9 +592,15 @@ impl TaskRenderer { } TaskStatus::Failed => { self.emit_line(&format!("\x1b[91m[ERROR]\x1b[0m {id} failed")); - // Dump full output in passthrough mode too + // Dump the error and the full output in passthrough mode too, + // in the same order as the TTY summary. A task can fail before + // running any container (a precondition check), so full_output + // may be empty and the message is all there is to show. let state = self.state.lock().unwrap(); if let Some(task) = state.iter().find(|t| &t.id == id) { + if let Some(ref msg) = task.error_message { + self.emit_line(&format!(" {msg}")); + } for line in &task.full_output { self.emit_line(&format!(" {}", strip_ansi(line))); } @@ -871,6 +877,30 @@ mod tests { assert!(task.error_message.as_deref() == Some("package not found")); } + /// A task that fails before it runs a container has no captured output; + /// its error message is the only explanation. Passthrough mode (logs, CI, + /// the desktop, the MCP) must print it, as the TTY summary already does. + #[test] + fn passthrough_failed_task_prints_its_error_message() { + let r = test_renderer(); + let id = TaskId::ExtBuild("foo".to_string()); + r.register_task(id.clone(), "ext build foo".to_string()); + r.set_status(&id, TaskStatus::Running); + r.set_error( + &id, + "Extension 'foo' declares packages, but its sysroot has no package database." + .to_string(), + ); + r.set_status(&id, TaskStatus::Failed); + + let output = clean_output(&r); + let failed = output.iter().position(|l| l.ends_with("failed")).unwrap(); + assert_eq!( + output[failed + 1].trim(), + "Extension 'foo' declares packages, but its sysroot has no package database." + ); + } + #[test] fn test_register_task_idempotent() { let r = test_renderer(); From f294814bb339e1212b4815909b3fab331de46928 Mon Sep 17 00:00:00 2001 From: nicksinas Date: Fri, 16 Oct 2026 17:05:14 -0500 Subject: [PATCH 2/4] style: rustfmt the sysroot probe test --- src/commands/ext/install.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/commands/ext/install.rs b/src/commands/ext/install.rs index af8ef095..bca47b91 100644 --- a/src/commands/ext/install.rs +++ b/src/commands/ext/install.rs @@ -1709,7 +1709,11 @@ mod tests { .env("AVOCADO_EXT_SYSROOTS", root.path()) .output() .unwrap(); - assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr)); + assert!( + out.status.success(), + "{}", + String::from_utf8_lossy(&out.stderr) + ); let parsed = parse_batch_stamps_output(&String::from_utf8_lossy(&out.stdout)); let get = |k: &str| parsed.get(k).and_then(|v| v.as_deref()).map(str::to_string); assert_eq!(get("sysroot:bare").as_deref(), Some("no")); From 3ba7d54c1fe4b260a78a040624cde3989eebae28 Mon Sep 17 00:00:00 2001 From: nicksinas Date: Fri, 16 Oct 2026 17:18:30 -0500 Subject: [PATCH 3/4] ext install/build: count override packages, clean up on interrupt, scope the clean hint Review follow-ups on #302. declares_packages now sees packages declared only inside target-* / kernel-* override blocks. The seeded rpm database is removed when the dnf transaction returns Err (Ctrl-C), not only on a failed exit. The recovery hint passes -r when one is set. --- src/commands/ext/build.rs | 53 ++++++++++++++++++++++++++++++++----- src/commands/ext/install.rs | 26 +++++++++++++----- 2 files changed, 66 insertions(+), 13 deletions(-) diff --git a/src/commands/ext/build.rs b/src/commands/ext/build.rs index 92f7e353..5fc8a04b 100644 --- a/src/commands/ext/build.rs +++ b/src/commands/ext/build.rs @@ -16,16 +16,34 @@ use crate::utils::stamps::{ use crate::utils::target::resolve_target_required; use crate::utils::tui::{TaskId, TuiGuard}; -/// Does `extensions..packages` list at least one package? Only such an -/// extension needs an installed sysroot before it can build; an overlay-only -/// extension has nothing for dnf to install. +/// Does `extensions.` list at least one package, at the top level or +/// inside a `target-*`, `kernel-*`, or bare-target override block? Only such +/// an extension needs an installed sysroot before it can build; an +/// overlay-only extension has nothing for dnf to install. +/// +/// Override blocks count whether or not they match this target or kernel. +/// Matching the kernel needs the dnf round trip install does, and the extra +/// breadth never refuses a build install prepared: install seeds the rpm +/// database for every extension before it reads `packages`, so a sysroot +/// that went through install always passes the probe. fn declares_packages(parsed: &serde_yaml::Value, extension: &str, target: &str) -> bool { // Template keys such as `avocado-bsp-{{ avocado.target.board }}` are // matched the same way the rest of the ext commands match them. crate::commands::ext::find_ext_in_mapping(parsed, extension, target) - .and_then(|e| e.get("packages")) - .and_then(|p| p.as_mapping()) - .is_some_and(|m| !m.is_empty()) + .is_some_and(|ext| lists_packages(ext, target)) +} + +fn lists_packages(block: &serde_yaml::Value, target: &str) -> bool { + let Some(map) = block.as_mapping() else { + return false; + }; + map.iter().any(|(key, value)| match key.as_str() { + Some("packages") => value.as_mapping().is_some_and(|m| !m.is_empty()), + Some(k) if k.starts_with("target-") || k.starts_with("kernel-") || k == target => { + lists_packages(value, target) + } + _ => false, + }) } /// The error for a package extension whose sysroot was never installed. @@ -1907,6 +1925,22 @@ extensions: avocado-bsp-{{ avocado.target }}: packages: avocado-bsp: "*" + target-only: + target-qemux86-64: + packages: + curl: "*" + kernel-only: + kernel-6.6: + packages: + kernel-module-foo: "*" + nested-only: + target-qemuarm64: + kernel-6.6: + packages: + kernel-module-foo: "*" + override-without-pkgs: + target-qemux86-64: + overlay: app/overlay "#, ) .unwrap(); @@ -1915,6 +1949,13 @@ extensions: assert!(!declares_packages(&parsed, "empty-pkgs", t)); assert!(!declares_packages(&parsed, "overlay-only", t)); assert!(!declares_packages(&parsed, "not-there", t)); + // Packages declared only inside an override block still count, + // including a block for another target or kernel (see the doc + // comment on `declares_packages` for why that is safe). + assert!(declares_packages(&parsed, "target-only", t)); + assert!(declares_packages(&parsed, "kernel-only", t)); + assert!(declares_packages(&parsed, "nested-only", t)); + assert!(!declares_packages(&parsed, "override-without-pkgs", t)); // A template key still in the mapping resolves through the same // lookup the other ext commands use (`find_ext_in_mapping`). assert!(declares_packages(&parsed, "avocado-bsp-qemux86-64", t)); diff --git a/src/commands/ext/install.rs b/src/commands/ext/install.rs index bca47b91..df8005fa 100644 --- a/src/commands/ext/install.rs +++ b/src/commands/ext/install.rs @@ -1534,14 +1534,20 @@ $DNF_SDK_HOST \ env_vars: self.runtime_env_vars(), ..Default::default() }; - let install_success = - run_container_command(container_helper, run_config, runs_on_context).await?; + // The result is read before `?` so an interrupted transaction + // (Ctrl-C comes back as `Err`) gets the same cleanup as a + // failed one. + let install_result = + run_container_command(container_helper, run_config, runs_on_context).await; + let install_success = matches!(install_result, Ok(true)); if !install_success { - print_error( - &format!("Failed to install dependencies for extension '{extension}'."), - OutputLevel::Normal, - ); + if let Ok(false) = install_result { + print_error( + &format!("Failed to install dependencies for extension '{extension}'."), + OutputLevel::Normal, + ); + } // This call seeded the database; take it back so the // retry seeds and installs again instead of reading as // up to date against another runtime's stamp. A sysroot @@ -1567,16 +1573,22 @@ $DNF_SDK_HOST \ .await .unwrap_or(false) { + let runtime_flag = self + .runtime + .as_deref() + .map(|rt| format!(" -r {rt}")) + .unwrap_or_default(); print_warning( &format!( "Could not remove the seeded rpm database for extension \ '{extension}'; the next install may skip it. Run \ - 'avocado ext clean {extension}' before retrying." + 'avocado ext clean {extension}{runtime_flag}' before retrying." ), OutputLevel::Normal, ); } } + install_result?; return Ok(false); } From fb31371021c4221a76ff7fae993c181f4412abdb Mon Sep 17 00:00:00 2001 From: nicksinas Date: Fri, 16 Oct 2026 17:25:58 -0500 Subject: [PATCH 4/4] chore(deps): bump quinn-proto to 0.11.19 and rustls to 0.23.46 Lockfile only. Clears the nine cargo audit findings that fail the Security Audit job on every PR: RUSTSEC-2026-0338 to 0342 (quinn-proto, fixed in 0.11.18) and RUSTSEC-2026-0346 to 0349 (rustls, fixed in 0.23.46). Both crates are transitive, pulled in through reqwest. (cherry picked from commit 0cd54a78ef17d5f431bb8b284daf186727514304) --- Cargo.lock | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b75e5424..84045d16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -991,11 +991,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", "rand_core 0.10.1", "wasip2", "wasip3", + "wasm-bindgen", ] [[package]] @@ -1790,15 +1792,16 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.15" +version = "0.11.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" +checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe" dependencies = [ "aws-lc-rs", "bytes", - "getrandom 0.3.4", + "getrandom 0.4.2", "lru-slab", - "rand 0.9.4", + "rand 0.10.1", + "rand_pcg", "ring", "rustc-hash", "rustls", @@ -1891,6 +1894,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + [[package]] name = "rcgen" version = "0.13.2" @@ -2070,9 +2082,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.45" +version = "0.23.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" dependencies = [ "aws-lc-rs", "log",