Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

131 changes: 130 additions & 1 deletion src/commands/ext/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,50 @@ use crate::utils::stamps::{
use crate::utils::target::resolve_target_required;
use crate::utils::tui::{TaskId, TuiGuard};

/// Does `extensions.<ext>` 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)
.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.
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,
Expand Down Expand Up @@ -289,13 +333,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(),
Expand Down Expand Up @@ -371,6 +416,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
Expand Down Expand Up @@ -1844,6 +1908,71 @@ 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: "*"
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();
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));
// 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));
}

/// 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 {
Expand Down
Loading
Loading